omkit 0.8.17 → 0.8.19
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 +313 -222
- package/dist/cli/ui/views/OmList.d.ts +8 -4
- package/dist/cli/ui/views/OmList.js +21 -7
- package/dist/cli/ui/views/SearchableList.d.ts +7 -1
- package/dist/cli/ui/views/SearchableList.js +14 -4
- package/dist/client/check.js +5 -8
- package/dist/client/discovery.js +16 -11
- package/dist/client/outline.d.ts +7 -0
- package/dist/client/outline.js +20 -0
- package/dist/client/registry.d.ts +6 -0
- package/dist/client/tsCache.d.ts +27 -0
- package/dist/client/tsCache.js +103 -0
- package/dist/output/writers/report/report.css +16 -20
- package/dist/output/writers/report/report.js +143 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,241 +5,372 @@
|
|
|
5
5
|
|
|
6
6
|
**The operational-model kit.** _Humans orchestrate. Runs narrate. AI assistants follow along._
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+

|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
omkit is a small TypeScript runtime for the workflows **around** your code: start the API, wait until it actually answers, seed it, open the app in a real browser, check what a user would see, and tear it all down together. You write that orchestration as ordinary TypeScript (`await`, `if`, loops, variables). omkit runs each step as a supervised, typed **action** and writes down everything the run did: what launched, what came up, what each step handed the next, what failed, and what the world looked like at the time.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
That record is the point. A human gets a report they can open from disk. An AI assistant gets files it can read and verify against, instead of guessing from interleaved terminal output. Where [tskb](https://www.npmjs.com/package/tskb) is the _knowledge_ layer (what your system **is**), omkit is the _operational_ one (what it's **doing right now**).
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
**What you get**
|
|
15
|
+
|
|
16
|
+
- **Orchestration as code.** Define an action once and launch it by calling it. No YAML, no DSL.
|
|
17
|
+
- **Typed handoffs between steps.** A step can publish a live value (a port, a client, a Playwright `Page`) and the next step receives it, typed. Nothing is scraped from stdout.
|
|
18
|
+
- **Gates that fail instead of hanging.** `.result`, `.ref` and `.once(event)` reject when the action fails or is cancelled.
|
|
19
|
+
- **Structured supervision.** A failure nobody is watching tears the whole run down, so a crashed dev server fails the run instead of leaving it wedged.
|
|
20
|
+
- **A durable run record.** Every run gets a folder with logs, a machine-readable tree, snapshots, artifacts and a self-contained HTML report.
|
|
21
|
+
- **Built for assistants.** An MCP server and a generated skill file let Claude (or any MCP client) discover, run and read your workflows.
|
|
22
|
+
|
|
23
|
+
## Contents
|
|
24
|
+
|
|
25
|
+
- [Install](#install)
|
|
26
|
+
- [Quick start](#quick-start)
|
|
27
|
+
- [Concepts](#concepts)
|
|
28
|
+
- [API reference](#api-reference): [`om`](#omname) · [`action`](#actionname) · [Activity](#activity) · [contexts](#contexts) · [`step`](#stepname-fn) · [errors](#errors)
|
|
29
|
+
- [Supervision and teardown](#supervision-and-teardown)
|
|
30
|
+
- [The run record](#the-run-record)
|
|
31
|
+
- [Batteries (`omkit/actions`)](#batteries-omkitactions)
|
|
32
|
+
- [Recipes](#recipes)
|
|
33
|
+
- [CLI](#cli)
|
|
34
|
+
- [MCP: let an assistant drive your oms](#mcp-let-an-assistant-drive-your-oms)
|
|
35
|
+
- [Skill file](#skill-file)
|
|
36
|
+
- [Migrating from `om(name, body)`](#migrating-from-omname-body)
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install omkit
|
|
19
42
|
```
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
Requires Node ≥ 20.11. ESM only. Batteries that drive a browser use `playwright-core` and need a local Chrome or Chromium.
|
|
22
45
|
|
|
23
|
-
##
|
|
46
|
+
## Quick start
|
|
24
47
|
|
|
25
|
-
|
|
48
|
+
Scaffold a project, or add the files by hand:
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
```sh
|
|
51
|
+
npx omkit init # tsconfig.omkit.json, oms/dev.ts, actions/hello.ts
|
|
52
|
+
```
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
An **om** is one runnable workflow. This one starts two dev servers, waits until the app really answers, then opens it in Chrome and hands the live page to the next step:
|
|
30
55
|
|
|
31
56
|
```ts
|
|
57
|
+
// oms/dev.ts
|
|
32
58
|
import { om } from "omkit";
|
|
33
|
-
import { command, healthcheck, browser, chromePage } from "omkit/actions";
|
|
59
|
+
import { command, healthcheck, browser, chromePage, drivePage } from "omkit/actions";
|
|
34
60
|
|
|
35
|
-
om("dev")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
om("dev")
|
|
62
|
+
.describe({ summary: "Start the API and web app, then check the home page." })
|
|
63
|
+
.run(async ({ assert }) => {
|
|
64
|
+
// Daemons: they keep running until the run is torn down.
|
|
65
|
+
command("npm run dev", { cwd: "api" }).tag("api");
|
|
66
|
+
command("npm run dev", { cwd: "web" }).tag("web");
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await healthcheck({ url: "http://localhost:3000" }).result;
|
|
43
|
-
} catch {
|
|
44
|
-
return; // never came up — the servers stay up so you can debug them
|
|
45
|
-
}
|
|
68
|
+
// A gate: resolves when the app answers, rejects if it never does.
|
|
69
|
+
await healthcheck({ url: "http://localhost:3000" }).tag("web:ready").result;
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.log(`opened ${(await page.ref).url()}`);
|
|
71
|
+
// A typed handoff: `.ref` is the live Browser, then the live Page.
|
|
72
|
+
const chrome = browser({ headless: false });
|
|
73
|
+
const page = chromePage("app", await chrome.ref, { url: "http://localhost:3000" });
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
const home = await drivePage({ target: await page.ref, js: "document.title", screenshot: true })
|
|
76
|
+
.result;
|
|
77
|
+
assert(home.title.length > 0, "the home page has a title");
|
|
78
|
+
|
|
79
|
+
console.log("stack is up, press Ctrl+C to stop");
|
|
80
|
+
});
|
|
54
81
|
```
|
|
55
82
|
|
|
56
|
-
|
|
83
|
+
Run it:
|
|
57
84
|
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
┌───────────┬────┴──────┬──────────────────┐
|
|
62
|
-
▼ ▼ ▼ ▼
|
|
63
|
-
command healthcheck browser ──ref──▶ chromePage
|
|
64
|
-
api · web (gate) (launch) .ref → live Page
|
|
65
|
-
(daemons) │
|
|
66
|
-
▼
|
|
67
|
-
your smoke test
|
|
85
|
+
```sh
|
|
86
|
+
npx omkit # interactive picker: browse, run, watch milestones live
|
|
87
|
+
npx omkit run dev # or run one om directly
|
|
68
88
|
```
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
Every run writes a folder such as `logs/dev-1a2b3c4d/2026-10-24/18-53-10/`, with `report.html`, `result.json`, `raw.jsonl` and one `.log` per action. See [The run record](#the-run-record).
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
- **AI assistants follow runs.** The run folder (`result.json`, `raw.jsonl`, per-action logs, snapshots) is a machine-readable account of what really happened — an assistant can verify outcomes, cite evidence, and pick up where a run left off.
|
|
74
|
-
- **Together, faster.** You express intent; the run records ground truth; the assistant acts on the record instead of re-running things to guess. Fewer cycles lost to "did that actually work?".
|
|
92
|
+
## Concepts
|
|
75
93
|
|
|
76
|
-
|
|
94
|
+
| Term | What it is |
|
|
95
|
+
| -------------- | --------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| **om** | One runnable workflow, `om(name).run(body)`. Its body is the root of a run. |
|
|
97
|
+
| **action** | A named, typed unit of work, `action(name).run(exec)`. Defining it doesn't run it. |
|
|
98
|
+
| **Activity** | What you get by **calling** an action: the live, running instance you observe and configure. |
|
|
99
|
+
| **step** | An inline, one-off action, `step(name, fn)`, with no reusable definition. |
|
|
100
|
+
| **ref** | A value an action publishes with `attach(...)` (a port, a client, a page). Others receive it from `.ref`. |
|
|
101
|
+
| **event** | A typed signal an action emits (`healthy`, `ready`). Observe it with `.on` or wait for it with `.once`. |
|
|
102
|
+
| **tag** | A label on an activity (`api`, `web:ready`), shown in logs, the report and the skill file. |
|
|
103
|
+
| **run folder** | The on-disk record of one run, keyed by the om's name and the file that defines it. |
|
|
77
104
|
|
|
78
|
-
|
|
105
|
+
Activities run in parallel. The om body stays in flight while you `await`, so the run never idles shut between steps.
|
|
79
106
|
|
|
80
|
-
|
|
107
|
+
## API reference
|
|
81
108
|
|
|
82
|
-
|
|
83
|
-
import { action } from "omkit";
|
|
109
|
+
### `om(name)`
|
|
84
110
|
|
|
85
|
-
|
|
111
|
+
```ts
|
|
112
|
+
import { om } from "omkit";
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
om(name: string): OmBuilder
|
|
88
115
|
```
|
|
89
116
|
|
|
90
|
-
|
|
117
|
+
Returns a builder. Chain configuration, then finish with `.run(body)`, which launches the run.
|
|
118
|
+
|
|
119
|
+
| Method | Purpose |
|
|
120
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
121
|
+
| `.describe({ summary })` | A one-line summary, shown by `omkit ls --describe`, the skill file, MCP and the report. |
|
|
122
|
+
| `.args(schema)` | A zod schema for the run's inputs. **Resolved at runtime**; see below. |
|
|
123
|
+
| `.mcp({ mode? })` | Expose this om over MCP. `mode` is `"settling"` (default) or `"long-lived"`. |
|
|
124
|
+
| `.run(body)` | Launch the run. `body(ctx, args?)` receives an [`OmContext`](#contexts) and, after `.args()`, the typed args. |
|
|
125
|
+
|
|
126
|
+
`.run()` returns a promise that resolves once the run has torn down and written its record. It never rejects: failures are recorded in the run and set the process exit code. Whatever the body returns is recorded as the run's `value` in `result.json`.
|
|
127
|
+
|
|
128
|
+
The run's identity is its name plus the file that defines it. Logs land in `logs/<name>-<hash8>/…`, so same-named oms in different files never share a folder, and moving the call within its file doesn't change the hash.
|
|
129
|
+
|
|
130
|
+
#### Run arguments
|
|
91
131
|
|
|
92
132
|
```ts
|
|
93
133
|
import { z } from "zod";
|
|
94
134
|
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
/* rows: number — typed; validated only when run as an MCP tool */
|
|
135
|
+
om("seed")
|
|
136
|
+
.args(z.object({ rows: z.number(), truncate: z.boolean().default(false) }))
|
|
137
|
+
.run(async (ctx, { rows, truncate }) => {
|
|
138
|
+
await command(`./seed.sh --rows ${rows}${truncate ? " --truncate" : ""}`).result;
|
|
100
139
|
});
|
|
101
|
-
|
|
102
|
-
seed({ rows: 500 }); // calling launches it; the shape is pinned, not checked at runtime
|
|
103
140
|
```
|
|
104
141
|
|
|
105
|
-
|
|
142
|
+
Each field is filled from the first source that can answer:
|
|
106
143
|
|
|
107
|
-
|
|
144
|
+
1. **Supplied:** `OMKIT_ARGS`, a JSON object in the environment. It is read once at run start and then cleared, so a subprocess doesn't inherit one om's args and resolve them against another's schema.
|
|
145
|
+
2. **Defaults:** anything the schema defaults is never asked about.
|
|
146
|
+
3. **Prompt:** whatever is still missing, one field at a time, with a one-line type sketch (`rows (number)`). Objects and arrays are asked for as a multiline JSON block; paste it, or answer with the path to a JSON file. A blank answer re-asks rather than coercing. Resolution gives up after three rounds.
|
|
147
|
+
4. **Fail:** if nobody can be asked (no supervising picker and no TTY), the run fails, naming every unresolved field.
|
|
108
148
|
|
|
109
|
-
|
|
149
|
+
Resolution happens inside the run, so prompts and answers land on the run's timeline, and the resolved values are recorded in `result.json` and the `main.log` header.
|
|
150
|
+
|
|
151
|
+
### `action(name)`
|
|
110
152
|
|
|
111
153
|
```ts
|
|
112
|
-
import {
|
|
154
|
+
import { action } from "omkit";
|
|
113
155
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
action(name: string): ActionBuilder
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Method | Purpose |
|
|
160
|
+
| ------------------------ | ------------------------------------------------------------------------------------- |
|
|
161
|
+
| `.describe({ summary })` | A one-line summary, carried onto every run of this action. |
|
|
162
|
+
| `.emits<Events>()` | Declare the events this action emits, as a type: `{ ready: number; error: string }`. |
|
|
163
|
+
| `.ref<Handle>()` | Declare the type of the value this action publishes with `attach`. |
|
|
164
|
+
| `.args(schema)` | Pin `.run`'s argument to a zod schema's type. **Type-level only** in code; see below. |
|
|
165
|
+
| `.mcp({ mode? })` | Expose this action over MCP as a runnable tool. |
|
|
166
|
+
| `.run(exec)` | Bind the implementation. Returns an **Action**: a function you call to launch it. |
|
|
167
|
+
|
|
168
|
+
`exec(ctx, ...args)` receives an [`ActionContext`](#contexts) and the arguments the action was called with. Its return value is the activity's result.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { action } from "omkit";
|
|
172
|
+
|
|
173
|
+
export const server = action("server")
|
|
174
|
+
.describe({ summary: "Serves the API and publishes its port." })
|
|
175
|
+
.emits<{ listening: number }>()
|
|
176
|
+
.ref<number>()
|
|
177
|
+
.run(async ({ attach, emit, signal }, port: number) => {
|
|
178
|
+
await listen(port); // start serving…
|
|
179
|
+
emit("listening", port); // …announce it…
|
|
180
|
+
attach(port); // …publish the port to whoever awaits `.ref`…
|
|
181
|
+
await new Promise((r) => signal.addEventListener("abort", r)); // …and stay up until teardown
|
|
119
182
|
});
|
|
183
|
+
|
|
184
|
+
const s = server(4000); // calling launches it and returns the live Activity
|
|
120
185
|
```
|
|
121
186
|
|
|
122
|
-
|
|
187
|
+
`.args(schema)` types the call site; launching an action in code neither validates nor prompts. The exception is an action run as an MCP tool: the JSON an assistant sends is parsed through the same schema, so `.default()`s are filled and bad values are refused before the body runs. Prompting for arguments is an om-level feature.
|
|
188
|
+
|
|
189
|
+
An Action also exposes `actionName`, `definedAt` (`file:line` of its `.run`), `description` and `mcp`, for tooling.
|
|
190
|
+
|
|
191
|
+
### Activity
|
|
123
192
|
|
|
124
|
-
|
|
125
|
-
2. **Defaults** — anything the schema defaults, which is therefore never asked about.
|
|
126
|
-
3. **Prompt** — whatever is still missing, one field at a time, with a one-line type sketch (`rows (number)`). Objects and arrays are asked for as a multiline JSON block — paste it, or answer with the path to a JSON file. A blank answer is _no answer_, not a value: it re-asks rather than coercing (`Number("")` is `0`). Resolution gives up after three rounds.
|
|
127
|
-
4. **Fail** — if nobody can be asked (no supervising picker and no TTY), the run fails naming every unresolved field at once, instead of prompting into the void.
|
|
193
|
+
Calling an action returns an `Activity<Result, Events, Handle>`:
|
|
128
194
|
|
|
129
|
-
|
|
195
|
+
| Member | Type / behavior |
|
|
196
|
+
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
197
|
+
| `id` | The node's id in the run, e.g. `server_9f3c1a2b`. |
|
|
198
|
+
| `result` | `Promise<Result>`. Resolves with the return value; **rejects** with the error on failure, or `CancelledError` on cancel. |
|
|
199
|
+
| `ref` | `Promise<Handle>`. Resolves when the action calls `attach`; **rejects** on failure or cancel. Resolves `undefined` on success if nothing was attached. |
|
|
200
|
+
| `once(event)` | `Promise<payload>`. The next emit of `event`. If the activity settles first, resolves `undefined` on success and **rejects** on failure. `once("done")` is `.result`. |
|
|
201
|
+
| `on(event, handler)` | Subscribe to every emit, including the system events `done`, `error` and `attached`. |
|
|
202
|
+
| `tag(name)` | Add a label. Chainable. |
|
|
203
|
+
| `withCache(...paths)` | Skip the body when these input paths are unchanged since the last success (the result is then `undefined`). Chain it before the first `await` after launching. |
|
|
204
|
+
| `cancel()` | Abort this activity and everything it launched. |
|
|
130
205
|
|
|
131
|
-
|
|
206
|
+
The body starts one microtask after the call, so `.tag(...)` and `.withCache(...)` chained in the same expression apply before it runs.
|
|
132
207
|
|
|
133
|
-
|
|
208
|
+
**Events replay their latest value.** An event's most recent payload (like `healthy`) is replayed to a listener that subscribes late, so `await probe.once("healthy")` works even after the probe passed.
|
|
134
209
|
|
|
135
|
-
The
|
|
210
|
+
**About `.ref`.** The value `.ref` resolves to is a thin, shallow wrapper over what the action attached:
|
|
211
|
+
|
|
212
|
+
- Every reader gets the same wrapper.
|
|
213
|
+
- Methods run bound to the real object, so private fields keep working.
|
|
214
|
+
- Each public method call that another action makes on it is recorded in the run: who called, from which line, and how long it took.
|
|
215
|
+
- Return values are not wrapped, and recording stops after 500 calls per handle.
|
|
216
|
+
|
|
217
|
+
The wrapper is not `===` to the original object, so code that compares it against a reference obtained some other way will see two different objects.
|
|
218
|
+
|
|
219
|
+
### Contexts
|
|
220
|
+
|
|
221
|
+
The action `exec` receives an `ActionContext`; the om body receives an `OmContext`.
|
|
222
|
+
|
|
223
|
+
| Member | `ActionContext` | `OmContext` | Purpose |
|
|
224
|
+
| ----------------------------- | :-------------: | :---------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
225
|
+
| `signal` | ✓ | ✓ | `AbortSignal` that fires on cancel or teardown. Honor it to stop early. |
|
|
226
|
+
| `logs` | ✓ | ✓ | The run's combined log, read-only: `logs.subscribe({ replay? })` is an async iterable of entries. |
|
|
227
|
+
| `tag(name)` | ✓ | ✓ | Tag this node. |
|
|
228
|
+
| `assert(cond, message)` | ✓ | ✓ | Record a pass or fail in the run's verdict. Never throws. |
|
|
229
|
+
| `snapshot(name, value)` | ✓ | ✓ | Write `value` as JSON into the run folder; resolves the file's path. |
|
|
230
|
+
| `artifact(name, file, opts?)` | ✓ | ✓ | Label a file you wrote (optional `description`, MIME inferred). It appears in `result.json` and the report. |
|
|
231
|
+
| `artifactsFolder` | ✓ | ✓ | Absolute path of the run's output folder. Write artifacts here. |
|
|
232
|
+
| `emit(event, payload)` | ✓ | | Emit one of the action's declared events. |
|
|
233
|
+
| `attach(handle)` | ✓ | | Publish the action's handle and resolve its `.ref`. The first call wins. |
|
|
234
|
+
| `proc(name, opts?)` | ✓ | | Spawn a child process: ``proc("tsc")`tsc -b` `` ([zx](https://github.com/google/zx) syntax). Its output goes to this action's log, and it is killed on teardown. |
|
|
235
|
+
| `cancel()` | | ✓ | Tear the whole run down. |
|
|
236
|
+
|
|
237
|
+
`console.*` inside an action or the om body is captured and attributed to that node's log.
|
|
238
|
+
|
|
239
|
+
### `step(name, fn)`
|
|
136
240
|
|
|
137
241
|
```ts
|
|
138
|
-
|
|
139
|
-
.ref<number>() // this action publishes a port
|
|
140
|
-
.run(async ({ attach, signal }) => {
|
|
141
|
-
const port = await listen(); // start listening…
|
|
142
|
-
attach(port); // …publish the port to downstream actions…
|
|
143
|
-
await until(signal); // …and stay up until teardown
|
|
144
|
-
});
|
|
242
|
+
import { step } from "omkit";
|
|
145
243
|
|
|
146
|
-
const
|
|
244
|
+
const rows = await step("seed catalog", async (ctx) => db.seed(40));
|
|
245
|
+
```
|
|
147
246
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
247
|
+
Runs `fn(ctx)` as its own node, with its own log, tags and error boundary, without a reusable definition. It returns a promise of `fn`'s result that rejects on failure or cancel, and its source location is where you wrote it.
|
|
248
|
+
|
|
249
|
+
### Errors
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { CancelledError, isCancelled } from "omkit";
|
|
152
253
|
```
|
|
153
254
|
|
|
154
|
-
|
|
255
|
+
A cancelled activity rejects `.result`, `.ref` and `.once` with `CancelledError`. `isCancelled(err)` tells an intentional stop apart from a real fault.
|
|
155
256
|
|
|
156
|
-
|
|
257
|
+
## Supervision and teardown
|
|
258
|
+
|
|
259
|
+
A failure that **nobody is watching** tears the whole run down and marks it failed. An activity is "watched" if, before it fails, you awaited its `.result`, `.ref` or `.once`, attached `.result.catch(...)`, or added an `on("error")` listener.
|
|
157
260
|
|
|
158
261
|
```ts
|
|
262
|
+
// Awaited: a failure throws here. Catch it to branch; the run stays green.
|
|
159
263
|
try {
|
|
160
|
-
await command("npm test").result;
|
|
161
|
-
} catch
|
|
162
|
-
|
|
264
|
+
await command("npm test").result;
|
|
265
|
+
} catch {
|
|
266
|
+
return;
|
|
163
267
|
}
|
|
164
268
|
|
|
165
|
-
//
|
|
166
|
-
|
|
269
|
+
// Fire-and-forget: if the dev server crashes, the run tears down. That's the guardrail.
|
|
270
|
+
command("npm run dev", { cwd: "web" }).tag("web");
|
|
271
|
+
|
|
272
|
+
// Owned: note the failure and carry on. (`fn` also runs on cancel; check `isCancelled`.)
|
|
273
|
+
flakyJob().result.catch((err) => console.warn("job failed, carrying on", err));
|
|
167
274
|
```
|
|
168
275
|
|
|
169
|
-
|
|
276
|
+
- **Success keeps the stack alive.** When the om body returns, the daemons it started keep running until Ctrl+C or `ctx.cancel()`.
|
|
277
|
+
- **Teardown is graceful.** Ctrl+C or `cancel()` aborts every signal, kills processes, unblocks waits, and always writes the run record. Nodes get a 5-second grace period to settle.
|
|
278
|
+
- **Write abort-aware daemons.** A daemon should stop when `signal` fires. Check `signal.aborted` before adding the listener, or a cancel that lands earlier is missed.
|
|
170
279
|
|
|
171
|
-
|
|
280
|
+
## The run record
|
|
172
281
|
|
|
173
|
-
|
|
282
|
+
Every run writes a folder under the project that owns the config: `logs/<name>-<hash8>/<date>/<time>/`.
|
|
174
283
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
284
|
+
| File | Contents |
|
|
285
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `report.html` | The whole run as one self-contained page; see below. |
|
|
287
|
+
| `result.json` | The run tree: each node's status, timing, args, tags, returned `value`, `calledAt` and `definedAt` sites, its `.describe()`/`.mcp()`/`.args()` metadata, and the verdict. |
|
|
288
|
+
| `raw.jsonl` | Every log entry, machine-readable, in order. |
|
|
289
|
+
| `main.log`, `<action>_<id>.log` | Human-readable timelines, one per node. |
|
|
290
|
+
| `events.log`, `asserts.log`, `snapshots.log`, `artifacts.log` | Cross-cutting rollups. |
|
|
291
|
+
| `snapshots/` | The JSON written by `snapshot(...)`. |
|
|
178
292
|
|
|
179
|
-
|
|
180
|
-
command("npm run dev", { cwd: "web" }).tag("web"); // if it crashes, the run tears down — you'll know
|
|
181
|
-
flakyBackgroundJob().result.catch((e) => console.warn("job failed, carrying on", e));
|
|
182
|
-
```
|
|
293
|
+
Because the folder is keyed by the om's name and its defining file, a script or an assistant can always find "the latest `dev` run", diff two runs of the same workflow, and answer questions with evidence: _did the server pass its healthcheck? which process died first? what config did the build run with?_
|
|
183
294
|
|
|
184
|
-
|
|
295
|
+
### `report.html`
|
|
185
296
|
|
|
186
|
-
|
|
297
|
+
The report needs no server: the data, images and viewer are all inlined, so it opens straight from disk and survives being zipped or attached.
|
|
187
298
|
|
|
188
|
-
|
|
299
|
+
- **Timeline:** every action as a bar. Each row shows how your code called it (`shop = api(…)`), its tags, status and duration, and points for its events, snapshots, assertions and errors.
|
|
300
|
+
- **Links between actions:** who waited on whose event (`once`), who cancelled whom, and the method calls made on another action's `.ref`. These show as points on the caller's row, with a dashed line to the target.
|
|
301
|
+
- **Flow:** the run as a graph of lanes. Snapshots and links ride along as chips.
|
|
302
|
+
- **Details:** for the selected action, its summary, tags and call site, plus sections for errors, args, the returned value, snapshots, events, links, logs and assertions.
|
|
303
|
+
- **Source:** the `<>` button next to an action opens its call site, its definition, its args and its returned value, editor-style.
|
|
304
|
+
- **Errors:** every failure, failed assertion and block of error output, with a switch that highlights where they came from.
|
|
189
305
|
|
|
190
|
-
|
|
191
|
-
- `raw.jsonl` — every entry, machine-readable.
|
|
192
|
-
- `main.log` + one `.log` per action — the human-readable timelines.
|
|
193
|
-
- `events.log` / `asserts.log` / `snapshots.log` / `artifacts.log` — cross-cutting rollups.
|
|
194
|
-
- `report.html` — the whole run as one self-contained page: a timeline of every action with its events, a flow graph of the tree, per-action details, logs, assertions, the run's snapshots and images, each action's source (the `<>` button next to a block), an Errors tab that collects failures, failed assertions and error output, and can highlight the actions they came from in every graph, and the links between actions: who waited on whose event (`once`), who cancelled whom, and the calls made on another action's `.ref`. Everything is inlined (data, base64 images, d3), so it opens straight from disk and survives being zipped or attached.
|
|
306
|
+

|
|
195
307
|
|
|
196
|
-
|
|
308
|
+

|
|
197
309
|
|
|
198
|
-
|
|
310
|
+
## Batteries (`omkit/actions`)
|
|
199
311
|
|
|
200
|
-
|
|
312
|
+
Ready-made actions built on the same engine. For anything else, write your own with `action(...)`.
|
|
201
313
|
|
|
202
|
-
|
|
314
|
+
| Battery | Does | Events | `.ref` / result |
|
|
315
|
+
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------- |
|
|
316
|
+
| `command(cmd, { args?, cwd?, env? })` | Runs a shell command, or an executable when `args` is given, as an action named after it. Output goes to its log; killed on teardown. | — | Rejects on a non-zero exit. |
|
|
317
|
+
| `healthcheck({ url \| host+port+path, status?, body?, intervalMs?, timeoutMs? })` | Polls until the endpoint answers with a matching status (and body). | `healthy` | Result `{ url, status, attempts }`. |
|
|
318
|
+
| `portFree(port, { host?, intervalMs?, timeoutMs? })` | Polls until nothing listens on the port, so a restart can rebind. | `free` | Result `{ port, host, attempts }`. |
|
|
319
|
+
| `untilLog(matcher, { timeoutMs?, replay? })` | Waits for the first run-log entry matching `matcher(entry)`. | `match` | The matching entry. |
|
|
320
|
+
| `tailLog(file, { fromStart?, encoding? })` | Tails a file another process writes and folds its lines into the run log. | — | — |
|
|
321
|
+
| `watch(file, { onCreate?, onUpdate?, onDelete?, onMissing? })` | Watches one file and survives it being deleted and recreated. | — | — |
|
|
322
|
+
| `watchDir(dir, { debounceMs? })` | Watches a directory tree and survives it being wiped and recreated. | `ready`, `create`, `update`, `delete`, `removed` | — |
|
|
323
|
+
| `prompt({ kind?, message, choices?, default?, timeoutMs?, until? })` | Asks in the terminal (or the picker, or the MCP client). `kind` is `"input"`, `"choice"` or `"multiline"`. Falls back to `default` on timeout. | — | Result: the answer. |
|
|
324
|
+
| `browser({ headless?, channel?, executablePath?, args? })` | Launches Chromium with Playwright (defaults to the installed Chrome); closed on teardown. | — | `.ref`: the live `Browser`. |
|
|
325
|
+
| `chromePage(label, source, { url?, match? })` | Opens or attaches to a page. `source` is a CDP address, a `Browser`, a `BrowserContext` or a `Page` (an Electron window works too). | `navigated`, `console`, `pageerror` | `.ref`: the live `Page`. |
|
|
326
|
+
| `drivePage({ target \| match \| url, js, screenshot? })` | Evaluates `js` in a page and optionally saves a screenshot as an artifact. | — | Result `{ value, title, url, screenshot? }`. |
|
|
203
327
|
|
|
204
|
-
##
|
|
328
|
+
## Recipes
|
|
205
329
|
|
|
206
|
-
**Skip work that's already done.** `withCache` fingerprints inputs and skips the body when nothing changed since the last
|
|
330
|
+
**Skip work that's already done.** `withCache` fingerprints the inputs and skips the body when nothing changed since the last success:
|
|
207
331
|
|
|
208
332
|
```ts
|
|
209
333
|
const build = action("build").run(({ proc }) => proc("tsc")`tsc -b`);
|
|
210
334
|
|
|
211
335
|
om("build").run(async () => {
|
|
212
336
|
const out = await build().withCache(`${process.cwd()}/src`).result;
|
|
213
|
-
if (out === undefined) console.log("no changes
|
|
337
|
+
if (out === undefined) console.log("no changes, skipped the build");
|
|
214
338
|
});
|
|
215
339
|
```
|
|
216
340
|
|
|
217
|
-
**
|
|
341
|
+
**Gate on an event.** `once` resolves when the event fires and rejects if the action fails first, so a gate never hangs:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
const db = database().tag("db");
|
|
345
|
+
await db.once("ready");
|
|
346
|
+
await seed(db.ref).result;
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Watch and react.**
|
|
218
350
|
|
|
219
351
|
```ts
|
|
220
352
|
import { watchDir } from "omkit/actions";
|
|
221
353
|
|
|
222
|
-
om("watch").run(
|
|
223
|
-
|
|
224
|
-
watcher
|
|
225
|
-
// body returns, but the watcher keeps the run alive until Ctrl+C
|
|
354
|
+
om("watch").run(() => {
|
|
355
|
+
watchDir("src").on("update", (file) => console.log(`changed: ${file}`));
|
|
356
|
+
// the body returns; the watcher keeps the run alive until Ctrl+C
|
|
226
357
|
});
|
|
227
358
|
```
|
|
228
359
|
|
|
229
|
-
**Gate a deploy on green tests.**
|
|
360
|
+
**Gate a deploy on green tests.**
|
|
230
361
|
|
|
231
362
|
```ts
|
|
232
363
|
om("ship").run(async () => {
|
|
233
364
|
try {
|
|
234
|
-
await command("npm test").result;
|
|
365
|
+
await command("npm test").result;
|
|
235
366
|
} catch {
|
|
236
|
-
return; // red
|
|
367
|
+
return; // red: stop here; the run stays green because the failure was observed
|
|
237
368
|
}
|
|
238
369
|
await command("./deploy.sh").result;
|
|
239
370
|
});
|
|
240
371
|
```
|
|
241
372
|
|
|
242
|
-
**Keep a human in the loop.** `prompt`
|
|
373
|
+
**Keep a human in the loop.** `prompt` falls back to its default when nobody answers, so it never blocks CI or an agent-driven run:
|
|
243
374
|
|
|
244
375
|
```ts
|
|
245
376
|
import { prompt } from "omkit/actions";
|
|
@@ -250,141 +381,98 @@ om("deploy").run(async () => {
|
|
|
250
381
|
message: "Deploy to production?",
|
|
251
382
|
choices: ["no", "yes"],
|
|
252
383
|
default: "no",
|
|
253
|
-
timeoutMs: 10_000,
|
|
384
|
+
timeoutMs: 10_000,
|
|
254
385
|
}).result;
|
|
255
386
|
if (answer === "yes") await command("./deploy.sh").result;
|
|
256
387
|
});
|
|
257
388
|
```
|
|
258
389
|
|
|
259
|
-
**Take a block of text.** `kind: "multiline"` reads
|
|
390
|
+
**Take a block of text.** `kind: "multiline"` reads until `until` says stop: parseable JSON (the default), a sentinel line, or a predicate:
|
|
260
391
|
|
|
261
392
|
```ts
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}).result;
|
|
269
|
-
const config = JSON.parse(blob);
|
|
270
|
-
await command(`./seed.sh --host ${config.host}`).result;
|
|
271
|
-
});
|
|
393
|
+
const blob = await prompt({
|
|
394
|
+
kind: "multiline",
|
|
395
|
+
message: "Paste the service config",
|
|
396
|
+
hint: "{ host: string; port: number }",
|
|
397
|
+
until: "json", // or "." to end on that line, or (text) => text.length > 500
|
|
398
|
+
}).result;
|
|
272
399
|
```
|
|
273
400
|
|
|
274
|
-
## Events
|
|
275
|
-
|
|
276
|
-
An Activity exposes its declared events plus the lifecycle ones (`done`, `error`, `attached`):
|
|
277
|
-
|
|
278
|
-
- **`activity.on(event, handler)`** — subscribe to every emit.
|
|
279
|
-
- **`activity.once(event)`** — a **promise** for the next emit. Resolves the payload when it fires; if the Activity settles first it resolves `undefined` on success and **rejects** on failure/cancel — so a gate on an event surfaces a failure instead of hanging. A retained snapshot (like `healthy`) is replayed immediately even if you subscribe late.
|
|
280
|
-
- **`activity.once("done")`** is the outcome itself — identical to `.result` (resolve the value, reject on failure). `await step().once("done")` and `await step().result` are the same hard gate.
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
const probe = healthcheck({ url: "http://localhost:3000" });
|
|
284
|
-
probe.on("healthy", (r) => console.log(`up after ${r.attempts} tries`));
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
## Batteries — `omkit/actions`
|
|
288
|
-
|
|
289
|
-
Reusable actions built on the core engine. For anything beyond these, drop down to `action(...).run(...)`.
|
|
290
|
-
|
|
291
|
-
- **`command`** — run a shell command (or, with `args`, an executable) as an action named after it; output streams to the log, killed on teardown.
|
|
292
|
-
- **`watch`** / **`watchDir`** — watch a file or directory for changes (survives wipe + recreate).
|
|
293
|
-
- **`untilLog`** — gate on the first log entry matching a predicate.
|
|
294
|
-
- **`tailLog`** — tail a file another process writes, folding its lines into the combined log.
|
|
295
|
-
- **`healthcheck`** — poll a URL/port until the status (and optionally body) matches.
|
|
296
|
-
- **`portFree`** — the mirror of `healthcheck`: poll a TCP port until nothing is listening, so a restart can rebind without racing the old process.
|
|
297
|
-
- **`prompt`** — ask the terminal for input, a choice, or a multiline block, with a timeout that falls back to a default; the answer is its handle.
|
|
298
|
-
- **`browser`** — launch a Chromium browser with Playwright and expose the live `Browser` as a handle (defaults to the installed Chrome); closed on teardown. Feed its `.ref` to `chromePage`.
|
|
299
|
-
- **`chromePage`** — attach to Chrome over CDP, or to an existing Playwright `Page`/`Browser`/`Context` (including a `browser` handle or an Electron window), and expose the live `Page` as a handle.
|
|
300
|
-
|
|
301
401
|
## CLI
|
|
302
402
|
|
|
303
|
-
|
|
403
|
+
The `omkit` bin works on a project described by a `tsconfig.omkit.json` that includes your `oms/` and `actions/`. Pass `--tsconfig <path>` to use another config.
|
|
404
|
+
|
|
405
|
+
| Command | What it does |
|
|
406
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
407
|
+
| `omkit` / `omkit run` | Interactive picker: search oms by name or summary, see the highlighted one's summary, file and the actions it calls, then run it, watch its milestones live, and answer prompts in place. |
|
|
408
|
+
| `omkit run <om>` | Run one om by name or file path, with your terminal attached. |
|
|
409
|
+
| `omkit ls` | List the discovered oms and actions (a static scan; it never imports your code). `--describe` adds summaries and MCP exposure. |
|
|
410
|
+
| `omkit check` | Typecheck the project (`tsc --noEmit`). |
|
|
411
|
+
| `omkit init` | Scaffold `tsconfig.omkit.json`, `oms/dev.ts` and `actions/hello.ts`. Never overwrites existing files. |
|
|
412
|
+
| `omkit skill` | Generate `.claude/skills/omkit-runs/SKILL.md`; see [Skill file](#skill-file). `--check` fails when it is out of date. |
|
|
413
|
+
| `omkit mcp` | Serve the project over MCP on stdio. |
|
|
414
|
+
| `omkit help` | Print the command overview (also `--help` / `-h`). |
|
|
304
415
|
|
|
305
|
-
|
|
306
|
-
- **`omkit run`** — with no argument, open the interactive picker: browse and search your oms, run one, and watch its milestones stream live — answering any `prompt` in-console. A bare `omkit` does the same (`run` is the default command).
|
|
307
|
-
- **`omkit run <om>`** — run one om directly by name or file path, inheriting the terminal.
|
|
308
|
-
- **`omkit ls`** — list the discovered oms and actions. Add `--describe` to show each one's `.describe({ summary })` and whether it's exposed; without it, `ls` is a static scan that never imports your code.
|
|
309
|
-
- **`omkit check`** — typecheck the project (`tsc --noEmit`) and report diagnostics.
|
|
310
|
-
- **`omkit skill`** — generate `.claude/skills/omkit-runs/SKILL.md`, a map of what's runnable (see below).
|
|
311
|
-
- **`omkit mcp`** — serve the project over the Model Context Protocol on stdio (see below).
|
|
312
|
-
- **`omkit help`** — print the command overview (also `--help` / `-h`).
|
|
416
|
+
Both run paths start the om in its own child process. The picker **supervises** it, piping its output into the live view and answering its prompts. `omkit run <om>` runs it **bare**, handing it your terminal.
|
|
313
417
|
|
|
314
|
-
|
|
418
|
+
Discovery and `omkit check` type-check the project and keep TypeScript's incremental state in `node_modules/.cache/omkit/`, so a file that hasn't changed isn't checked again on the next start. Set `OMKIT_CACHE_DIR` to keep it elsewhere, or `OMKIT_NO_CACHE=1` to turn it off.
|
|
315
419
|
|
|
316
|
-
## MCP
|
|
420
|
+
## MCP: let an assistant drive your oms
|
|
317
421
|
|
|
318
|
-
`omkit mcp` serves a project over the [Model Context Protocol](https://modelcontextprotocol.io)
|
|
422
|
+
`omkit mcp` serves a project over the [Model Context Protocol](https://modelcontextprotocol.io). Claude and other clients can discover your oms, run one, and read what the run produced.
|
|
319
423
|
|
|
320
|
-
**Nothing is exposed unless you say so.** Add `.mcp()` to an om or action; without it the server neither lists nor runs it.
|
|
424
|
+
**Nothing is exposed unless you say so.** Add `.mcp()` to an om or action; without it the server neither lists nor runs it.
|
|
321
425
|
|
|
322
426
|
```ts
|
|
323
427
|
om("smoke-test")
|
|
324
428
|
.describe({ summary: "Boot the app and run the smoke suite" })
|
|
325
429
|
.args(z.object({ headless: z.boolean().default(true) }))
|
|
326
|
-
.mcp() //
|
|
430
|
+
.mcp() // now an assistant can run it
|
|
327
431
|
.run(async (ctx, { headless }) => {
|
|
328
432
|
/* … */
|
|
329
433
|
});
|
|
330
434
|
```
|
|
331
435
|
|
|
332
|
-
`mode` defaults to `"settling"
|
|
436
|
+
`mode` defaults to `"settling"`, for a run that finishes on its own. Declare `.mcp({ mode: "long-lived" })` for a server or a watcher; `run_om` refuses those and points to `start_om`.
|
|
333
437
|
|
|
334
438
|
Point your client at the project:
|
|
335
439
|
|
|
336
440
|
```json
|
|
337
441
|
{
|
|
338
442
|
"mcpServers": {
|
|
339
|
-
"omkit": {
|
|
340
|
-
"command": "npx",
|
|
341
|
-
"args": ["--no", "--", "omkit", "mcp"]
|
|
342
|
-
}
|
|
443
|
+
"omkit": { "command": "npx", "args": ["--no", "--", "omkit", "mcp"] }
|
|
343
444
|
}
|
|
344
445
|
}
|
|
345
446
|
```
|
|
346
447
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
### Inspecting it
|
|
350
|
-
|
|
351
|
-
The [MCP Inspector](https://github.com/modelcontextprotocol/inspector) drives the same binary, so you can exercise every tool by hand before pointing an assistant at it:
|
|
352
|
-
|
|
353
|
-
```sh
|
|
354
|
-
npx -y @modelcontextprotocol/inspector npx --no -- omkit mcp
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
It serves a UI on `localhost:6274` and prints an auth token to paste in. Because omkit declares real input schemas, you get form fields per tool — and the history pane shows the full request and response for each call, `structuredContent` and resource links included. That is usually all the debugging you need: it is a client, so it shows you what _you_ sent, not what an assistant sent.
|
|
358
|
-
|
|
359
|
-
Two things to know. On Windows the Inspector cannot spawn a `.cmd` shim directly, so if `npx` fails there, point it at the binary instead — `node node_modules/omkit/dist/cli/index.js mcp`. And the Inspector owns the runs it starts: closing it tears down anything `start_om` left running, the same rule that applies to any client.
|
|
360
|
-
|
|
361
|
-
**Six tools**, however many oms you have — adding one changes what `list_oms` returns, never the tool list:
|
|
448
|
+
If the project sits in a subfolder, name its config: `["--no", "--", "omkit", "mcp", "--tsconfig", "om/tsconfig.omkit.json"]`. The server takes its project root from that config and resolves `logs/` under it, so runs started from the terminal and runs started by an assistant land in the same tree.
|
|
362
449
|
|
|
363
450
|
| Tool | What it does |
|
|
364
451
|
| ----------------------- | ------------------------------------------------------------------------------- |
|
|
365
|
-
| `list_oms` | Exposed oms and actions: summary, mode, and the JSON Schema
|
|
366
|
-
| `run_om(name, args)` |
|
|
367
|
-
| `start_om(name, args)` |
|
|
368
|
-
| `get_run(run)` | Status, verdict once settled, and links to the files the run produced
|
|
369
|
-
| `tail_run(run, cursor)` | Log lines since `cursor
|
|
370
|
-
| `cancel_run(runId)` |
|
|
452
|
+
| `list_oms` | Exposed oms and actions: summary, mode, and the JSON Schema of their arguments. |
|
|
453
|
+
| `run_om(name, args)` | Run a settling entry to completion; returns its verdict and assertion tally. |
|
|
454
|
+
| `start_om(name, args)` | Start anything and return a handle immediately. |
|
|
455
|
+
| `get_run(run)` | Status, verdict once settled, and links to the files the run produced. |
|
|
456
|
+
| `tail_run(run, cursor)` | Log lines since `cursor`: live while running, from `raw.jsonl` once settled. |
|
|
457
|
+
| `cancel_run(runId)` | Tear a run down; it still writes its run folder. |
|
|
371
458
|
|
|
372
|
-
|
|
459
|
+
The tool list stays at six however many oms you have, so call `list_oms` first to get each entry's argument schema.
|
|
373
460
|
|
|
374
|
-
**Run folders are resources.** `omkit://runs/{run}/latest/{file}` reads from
|
|
461
|
+
- **Run folders are resources.** `omkit://runs/{run}/latest/{file}` reads from an om's newest run, and `omkit://runs/{run}/{date}/{time}/{file}` from a specific one. Reads are confined to the project's `logs/`, size-capped and read-only.
|
|
462
|
+
- **While a run is going,** milestones arrive as progress notifications, cancelling the request tears the run down, and a `prompt` raised mid-run reaches the client as an elicitation.
|
|
375
463
|
|
|
376
|
-
|
|
464
|
+
To exercise the tools by hand, use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): `npx -y @modelcontextprotocol/inspector npx --no -- omkit mcp`. On Windows, if it can't spawn the `npx` shim, point it at `node node_modules/omkit/dist/cli/index.js mcp`. Closing the Inspector tears down anything it started.
|
|
377
465
|
|
|
378
|
-
##
|
|
466
|
+
## Skill file
|
|
379
467
|
|
|
380
|
-
`list_oms` answers "what can I run here?" at runtime,
|
|
468
|
+
`list_oms` answers "what can I run here?" at runtime, at the cost of a round trip. `omkit skill` writes the same answer to a file an assistant already has in context:
|
|
381
469
|
|
|
382
470
|
```sh
|
|
383
471
|
omkit skill # writes .claude/skills/omkit-runs/SKILL.md
|
|
384
472
|
omkit skill --check # exits non-zero when it no longer matches the project
|
|
385
473
|
```
|
|
386
474
|
|
|
387
|
-
Each exposed workflow gets its summary,
|
|
475
|
+
Each exposed workflow gets its summary, argument shape, source path and an outline of what it calls, read statically with the `.tag(...)` names you wrote:
|
|
388
476
|
|
|
389
477
|
```
|
|
390
478
|
### `tskb:build` — settling
|
|
@@ -396,17 +484,20 @@ Rebuild the tskb knowledge graph from this repo's .tskb.tsx docs.
|
|
|
396
484
|
- **Calls:** `watchDir` [watch:build:daemon] → `buildDocs` [build]
|
|
397
485
|
```
|
|
398
486
|
|
|
399
|
-
Generating it runs nothing
|
|
487
|
+
Generating it runs nothing. Commit it, and put `--check` in a pre-commit hook or CI. Its `registry-hash` covers your declarations rather than the rendered markdown, so reformatting doesn't trip the check. The one field to edit by hand is the frontmatter `description`, which decides when an assistant loads the file; regeneration keeps it.
|
|
400
488
|
|
|
401
|
-
|
|
489
|
+
## Migrating from `om(name, body)`
|
|
402
490
|
|
|
403
|
-
|
|
491
|
+
`om(name, body)` was removed in 0.5.0. `om(name)` now returns a builder, and `.run(body)` takes the second argument:
|
|
404
492
|
|
|
405
|
-
```
|
|
406
|
-
|
|
493
|
+
```diff
|
|
494
|
+
-om("dev", async (ctx) => {
|
|
495
|
+
+om("dev").run(async (ctx) => {
|
|
496
|
+
// ...
|
|
497
|
+
});
|
|
407
498
|
```
|
|
408
499
|
|
|
409
|
-
|
|
500
|
+
Run folders are unaffected: a run's identity is its name plus its defining file, and line numbers are not part of the hash.
|
|
410
501
|
|
|
411
502
|
## License
|
|
412
503
|
|