@tagma/sdk 0.4.12 → 0.4.13

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.
Files changed (88) hide show
  1. package/README.md +572 -566
  2. package/dist/adapters/websocket-approval.d.ts.map +1 -1
  3. package/dist/adapters/websocket-approval.js +3 -1
  4. package/dist/adapters/websocket-approval.js.map +1 -1
  5. package/dist/approval.d.ts.map +1 -1
  6. package/dist/approval.js.map +1 -1
  7. package/dist/completions/exit-code.d.ts.map +1 -1
  8. package/dist/completions/exit-code.js.map +1 -1
  9. package/dist/completions/file-exists.d.ts.map +1 -1
  10. package/dist/completions/file-exists.js.map +1 -1
  11. package/dist/completions/output-check.js +2 -7
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config-ops.d.ts.map +1 -1
  14. package/dist/config-ops.js +24 -26
  15. package/dist/config-ops.js.map +1 -1
  16. package/dist/dag.d.ts.map +1 -1
  17. package/dist/dag.js +1 -1
  18. package/dist/dag.js.map +1 -1
  19. package/dist/drivers/claude-code.d.ts.map +1 -1
  20. package/dist/drivers/claude-code.js +10 -5
  21. package/dist/drivers/claude-code.js.map +1 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +54 -27
  24. package/dist/engine.js.map +1 -1
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/hooks.js +1 -3
  27. package/dist/hooks.js.map +1 -1
  28. package/dist/logger.d.ts.map +1 -1
  29. package/dist/logger.js +4 -2
  30. package/dist/logger.js.map +1 -1
  31. package/dist/pipeline-runner.d.ts.map +1 -1
  32. package/dist/pipeline-runner.js +10 -4
  33. package/dist/pipeline-runner.js.map +1 -1
  34. package/dist/registry.d.ts +11 -1
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +28 -3
  37. package/dist/registry.js.map +1 -1
  38. package/dist/runner.d.ts.map +1 -1
  39. package/dist/runner.js +18 -13
  40. package/dist/runner.js.map +1 -1
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +14 -14
  43. package/dist/schema.js.map +1 -1
  44. package/dist/schema.test.js +5 -1
  45. package/dist/schema.test.js.map +1 -1
  46. package/dist/sdk.d.ts +2 -2
  47. package/dist/sdk.d.ts.map +1 -1
  48. package/dist/sdk.js +1 -1
  49. package/dist/sdk.js.map +1 -1
  50. package/dist/triggers/file.d.ts.map +1 -1
  51. package/dist/triggers/file.js +11 -4
  52. package/dist/triggers/file.js.map +1 -1
  53. package/dist/triggers/manual.d.ts.map +1 -1
  54. package/dist/triggers/manual.js +2 -1
  55. package/dist/triggers/manual.js.map +1 -1
  56. package/dist/utils.d.ts.map +1 -1
  57. package/dist/utils.js +13 -6
  58. package/dist/utils.js.map +1 -1
  59. package/dist/validate-raw.d.ts.map +1 -1
  60. package/dist/validate-raw.js +40 -11
  61. package/dist/validate-raw.js.map +1 -1
  62. package/package.json +2 -2
  63. package/scripts/preinstall.js +1 -1
  64. package/src/adapters/stdin-approval.ts +106 -106
  65. package/src/adapters/websocket-approval.ts +224 -220
  66. package/src/approval.ts +131 -125
  67. package/src/bootstrap.ts +37 -37
  68. package/src/completions/exit-code.ts +34 -30
  69. package/src/completions/file-exists.ts +66 -60
  70. package/src/completions/output-check.ts +86 -86
  71. package/src/config-ops.ts +307 -322
  72. package/src/dag.ts +234 -228
  73. package/src/drivers/claude-code.ts +250 -240
  74. package/src/engine.ts +1098 -935
  75. package/src/hooks.ts +187 -179
  76. package/src/logger.ts +182 -178
  77. package/src/middlewares/static-context.ts +45 -45
  78. package/src/pipeline-runner.ts +156 -150
  79. package/src/registry.ts +51 -23
  80. package/src/runner.ts +395 -397
  81. package/src/schema.test.ts +5 -1
  82. package/src/schema.ts +338 -328
  83. package/src/sdk.ts +91 -81
  84. package/src/triggers/file.ts +33 -14
  85. package/src/triggers/manual.ts +86 -81
  86. package/src/types.ts +18 -18
  87. package/src/utils.ts +202 -191
  88. package/src/validate-raw.ts +442 -409
package/README.md CHANGED
@@ -1,566 +1,572 @@
1
- # @tagma/sdk
2
-
3
- > ## ⚠️ Bun-only runtime
4
- >
5
- > ```bash
6
- > bun add @tagma/sdk
7
- > ```
8
- >
9
- > This package is published as pre-built JavaScript (`dist/`), but the runtime
10
- > uses Bun-only APIs (`Bun.spawn`, `Bun.file`, `Bun.serve`). It will install
11
- > under `npm` / `yarn` / `pnpm` without error, but **crash at runtime on Node**
12
- > the first time a pipeline spawns a task. Get Bun at <https://bun.sh>.
13
- >
14
- > *(The `npm i @tagma/sdk` line in the npmjs.com sidebar is auto-generated by
15
- > the npm registry website and cannot be removed — please ignore it and use
16
- > the command above.)*
17
-
18
- A local AI task orchestration SDK for [Bun](https://bun.sh). Define multi-track pipelines in YAML, run AI coding agents (Claude Code, Codex, OpenCode) and shell commands in parallel with dependency resolution, approval gates, and lifecycle hooks.
19
-
20
- ## Install
21
-
22
- **Requires Bun ≥ 1.3.** Node/npm are not supported.
23
-
24
- ```bash
25
- bun add @tagma/sdk
26
- ```
27
-
28
- ## Quick Start
29
-
30
- **1. Define a pipeline** (`pipeline.yaml`)
31
-
32
- ```yaml
33
- pipeline:
34
- name: build-and-test
35
- tracks:
36
- - id: backend
37
- name: Backend
38
- driver: claude-code
39
- permissions: { read: true, write: true, execute: false }
40
- tasks:
41
- - id: implement
42
- name: Implement feature
43
- prompt: "Add a /health endpoint to src/server.ts"
44
- output: ./output/implement.txt
45
- - id: test
46
- name: Run tests
47
- command: "bun test"
48
- depends_on: [implement]
49
- ```
50
-
51
- **2. Run it programmatically**
52
-
53
- ```ts
54
- import {
55
- bootstrapBuiltins,
56
- loadPipeline,
57
- runPipeline,
58
- InMemoryApprovalGateway,
59
- } from '@tagma/sdk';
60
-
61
- // Register built-in drivers, triggers, completions
62
- bootstrapBuiltins();
63
-
64
- const yaml = await Bun.file('pipeline.yaml').text();
65
- const config = await loadPipeline(yaml, process.cwd());
66
-
67
- const result = await runPipeline(config, process.cwd());
68
- console.log(result.success ? 'Done' : 'Failed');
69
- ```
70
-
71
- ## Features
72
-
73
- - **Multi-track DAG execution** -- tasks run in parallel across tracks, respecting `depends_on` ordering
74
- - **Driver plugins** -- built-in `claude-code` driver; install `@tagma/driver-codex` or `@tagma/driver-opencode` for other agents
75
- - **Session handoff** -- `continue_from` passes context between tasks (session resume or text injection)
76
- - **Approval gates** -- trigger-based approval with stdin and WebSocket adapters
77
- - **Lifecycle hooks** -- `pipeline_start`, `task_start`, `task_success`, `task_failure`, `pipeline_complete`, `pipeline_error`
78
- - **Middleware** -- enrich prompts before execution (e.g. inject static context)
79
- - **Completion checks** -- validate task output with `exit_code`, `file_exists`, or `output_check` plugins
80
- - **Plugin schemas** -- triggers/completions/middlewares can declare a `PluginSchema` so visual editors render typed forms for their config
81
-
82
- ## Pipeline YAML Reference
83
-
84
- ### Full Structure
85
-
86
- ```yaml
87
- pipeline:
88
- name: my-pipeline
89
- driver: claude-code
90
- timeout: 30m
91
- plugins:
92
- - "@tagma/driver-codex"
93
- hooks:
94
- pipeline_start: "echo starting"
95
- task_start: "echo task begin"
96
- task_success: "echo task ok"
97
- task_failure: "notify-slack.sh"
98
- pipeline_complete: "echo done"
99
- pipeline_error: "alert.sh"
100
- tracks:
101
- - id: track-1
102
- name: Track One
103
- color: "#3b82f6"
104
- driver: claude-code
105
- model: claude-sonnet-4-6
106
- agent_profile: senior
107
- cwd: ./services/backend
108
- permissions:
109
- read: true
110
- write: true
111
- execute: false
112
- on_failure: skip_downstream
113
- middlewares:
114
- - type: static_context
115
- file: ./context.md
116
- label: Architecture Guide
117
- tasks:
118
- - id: task-a
119
- name: Do something
120
- prompt: "Your prompt here"
121
- output: ./output/task-a.txt
122
- timeout: 10m
123
- driver: claude-code
124
- model: claude-sonnet-4-6
125
- agent_profile: senior
126
- cwd: ./src
127
- permissions:
128
- read: true
129
- write: true
130
- execute: false
131
- middlewares:
132
- - type: static_context
133
- file: ./ref.md
134
- trigger:
135
- type: manual
136
- message: "Approve before running"
137
- timeout: 5m
138
- completion:
139
- type: exit_code
140
- expect: 0
141
- - id: task-b
142
- name: Follow up
143
- prompt: "Continue the work"
144
- continue_from: task-a
145
- depends_on: [task-a]
146
- ```
147
-
148
- ### Pipeline Fields
149
-
150
- | Field | Type | Required | Description |
151
- |---|---|---|---|
152
- | `name` | `string` | Yes | Pipeline name, used in logs and run IDs |
153
- | `driver` | `string` | No | Default driver for all tracks/tasks (inherited). Built-in: `claude-code` |
154
- | `model` | `string` | No | Default model for all tracks/tasks (inherited). Exact model name, e.g. `claude-sonnet-4-6` |
155
- | `timeout` | `string` | No | Pipeline-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
156
- | `plugins` | `string[]` | No | External plugin packages to load, e.g. `["@tagma/driver-codex"]` |
157
- | `hooks` | `HooksConfig` | No | Shell commands to run at lifecycle events (see Hooks below) |
158
- | `tracks` | `TrackConfig[]` | Yes | List of parallel execution tracks |
159
-
160
- ### Hooks Fields
161
-
162
- Each hook value can be a single command string or an array of commands.
163
-
164
- | Field | Type | Description |
165
- |---|---|---|
166
- | `pipeline_start` | `string \| string[]` | Runs when the pipeline begins |
167
- | `task_start` | `string \| string[]` | Runs before each task starts |
168
- | `task_success` | `string \| string[]` | Runs after a task succeeds |
169
- | `task_failure` | `string \| string[]` | Runs after a task fails |
170
- | `pipeline_complete` | `string \| string[]` | Runs when the pipeline finishes successfully |
171
- | `pipeline_error` | `string \| string[]` | Runs when the pipeline finishes with errors |
172
-
173
- ### Track Fields
174
-
175
- | Field | Type | Required | Default | Description |
176
- |---|---|---|---|---|
177
- | `id` | `string` | Yes | — | Unique track identifier |
178
- | `name` | `string` | Yes | | Display name |
179
- | `color` | `string` | No | | Color hint for UI rendering (e.g. `"#3b82f6"`) |
180
- | `driver` | `string` | No | Inherited from pipeline | Driver for all tasks in this track |
181
- | `model` | `string` | No | Inherited from pipeline | Exact model name passed to the driver CLI (e.g. `claude-sonnet-4-6`) |
182
- | `agent_profile` | `string` | No | — | Named agent configuration profile |
183
- | `cwd` | `string` | No | Pipeline workDir | Working directory for tasks in this track (relative path) |
184
- | `permissions` | `Permissions` | No | Inherited from pipeline | Default permissions for tasks (see Permissions) |
185
- | `on_failure` | `OnFailure` | No | `skip_downstream` | Failure strategy: `skip_downstream`, `stop_all`, `ignore` |
186
- | `middlewares` | `MiddlewareConfig[]` | No | | Middlewares applied to all tasks (task-level overrides track-level) |
187
- | `tasks` | `TaskConfig[]` | Yes | | Ordered list of tasks in this track |
188
-
189
- ### Task Fields
190
-
191
- | Field | Type | Required | Default | Description |
192
- |---|---|---|---|---|
193
- | `id` | `string` | Yes | — | Unique task identifier (unique within the pipeline) |
194
- | `name` | `string` | No | — | Display name |
195
- | `prompt` | `string` | No* | | AI prompt to send to the driver. *Mutually exclusive with `command` |
196
- | `command` | `string` | No* | | Shell command to execute directly. *Mutually exclusive with `prompt` |
197
- | `depends_on` | `string[]` | No | | Task IDs that must complete before this task runs. Cross-track refs use `trackId.taskId` |
198
- | `continue_from` | `string` | No | | Task ID whose output/session to continue from (session handoff). Cross-track refs use `trackId.taskId` |
199
- | `output` | `string` | No | — | File path to write task stdout to (relative to workDir) |
200
- | `driver` | `string` | No | Inherited from track | Driver override for this task |
201
- | `model` | `string` | No | Inherited from track | Model name override for this task |
202
- | `agent_profile` | `string` | No | Inherited from track | Agent profile override |
203
- | `cwd` | `string` | No | Inherited from track | Working directory override (relative path) |
204
- | `timeout` | `string` | No | — | Task-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
205
- | `permissions` | `Permissions` | No | Inherited from track | Permission override (see Permissions) |
206
- | `middlewares` | `MiddlewareConfig[]` | No | Inherited from track | Middleware override. Set `[]` to disable inherited middlewares |
207
- | `trigger` | `TriggerConfig` | No | | Gate that must resolve before the task runs (see Triggers) |
208
- | `completion` | `CompletionConfig` | No | | Post-execution check to validate task output (see Completions) |
209
-
210
- ### Permissions
211
-
212
- | Field | Type | Default | Description |
213
- |---|---|---|---|
214
- | `read` | `boolean` | — | Allow the agent to read files |
215
- | `write` | `boolean` | | Allow the agent to write files |
216
- | `execute` | `boolean` | — | Allow the agent to execute commands |
217
-
218
- ### Inheritance
219
-
220
- Fields are inherited top-down: **pipeline → track → task**. A value set at a lower level overrides the inherited value.
221
-
222
- Inherited fields: `driver`, `model`, `permissions`, `cwd`, `middlewares`.
223
-
224
- Track-level `middlewares` apply to all tasks in the track. Setting task-level `middlewares` **replaces** (not appends) the track-level list. Use `middlewares: []` to disable all inherited middlewares for a task.
225
-
226
- ---
227
-
228
- ### Built-in Triggers
229
-
230
- #### `manual` Human approval gate
231
-
232
- | Field | Type | Required | Default | Description |
233
- |---|---|---|---|---|
234
- | `type` | `"manual"` | Yes | | Trigger type |
235
- | `message` | `string` | No | `"Manual confirmation required for task \"{taskId}\""` | Message shown to the approver |
236
- | `timeout` | `string` | No | | How long to wait for a decision before timing out |
237
- | `metadata` | `object` | No | | Arbitrary metadata passed to the approval gateway |
238
-
239
- #### `file` — File watcher gate
240
-
241
- | Field | Type | Required | Default | Description |
242
- |---|---|---|---|---|
243
- | `type` | `"file"` | Yes | — | Trigger type |
244
- | `path` | `string` | Yes | — | File path to watch (relative to workDir) |
245
- | `timeout` | `string` | No | — | How long to wait for the file to appear/change |
246
-
247
- ---
248
-
249
- ### Built-in Completions
250
-
251
- #### `exit_code` Exit code check
252
-
253
- | Field | Type | Required | Default | Description |
254
- |---|---|---|---|---|
255
- | `type` | `"exit_code"` | Yes | | Completion type |
256
- | `expect` | `number \| number[]` | No | `0` | Expected exit code(s). Pass an array for multiple acceptable codes |
257
-
258
- #### `file_exists` — File existence check
259
-
260
- | Field | Type | Required | Default | Description |
261
- |---|---|---|---|---|
262
- | `type` | `"file_exists"` | Yes | | Completion type |
263
- | `path` | `string` | Yes | — | File or directory path to check (relative to workDir) |
264
- | `kind` | `"file" \| "dir" \| "any"` | No | `"any"` | Entity type constraint |
265
- | `min_size` | `number` | No | | Minimum file size in bytes (files only) |
266
-
267
- #### `output_check` — Command-based output validation
268
-
269
- | Field | Type | Required | Default | Description |
270
- |---|---|---|---|---|
271
- | `type` | `"output_check"` | Yes | — | Completion type |
272
- | `check` | `string` | Yes | — | Shell command to run. Task stdout is piped to its stdin; exits 0 = pass |
273
- | `timeout` | `string` | No | `"30s"` | Max time to wait for the check command |
274
-
275
- ---
276
-
277
- ### Built-in Middlewares
278
-
279
- #### `static_context` — Prepend file content to prompt
280
-
281
- | Field | Type | Required | Default | Description |
282
- |---|---|---|---|---|
283
- | `type` | `"static_context"` | Yes | — | Middleware type |
284
- | `file` | `string` | Yes | — | Path to the context file (relative to workDir) |
285
- | `label` | `string` | No | `"Reference: {filename}"` | Label for the injected context section |
286
-
287
- ## API
288
-
289
- ### `bootstrapBuiltins()`
290
-
291
- Registers all built-in plugins (claude-code driver, file/manual triggers, completion checks, static-context middleware).
292
-
293
- ### `loadPipeline(yaml: string, workDir: string): Promise<PipelineConfig>`
294
-
295
- Parses YAML, resolves inheritance, and validates the configuration.
296
-
297
- ### `runPipeline(config, workDir, options?): Promise<EngineResult>`
298
-
299
- Executes the pipeline. Returns `{ success, runId, logPath, summary, states }`.
300
-
301
- Options:
302
- - `approvalGateway` -- custom `ApprovalGateway` instance (defaults to `InMemoryApprovalGateway`)
303
- - `signal` -- `AbortSignal` to cancel the run externally
304
- - `onEvent` -- callback for real-time `PipelineEvent` updates:
305
- - `pipeline_start` pipeline began; includes `states: ReadonlyMap<taskId, TaskState>` (initial snapshot of all tasks at `waiting`)
306
- - `task_status_change` a task changed status; includes `state: TaskState` (complete snapshot at the time of change: `startedAt` is populated before the `running` event; `result` and `finishedAt` are populated before any terminal-status event)
307
- - `task_log` — a structured log line was written to `pipeline.log`. Mirrors every `Logger` call (info/warn/error/debug/section/quiet) and carries `{ taskId: string | null, level, timestamp, text }`. `taskId` is non-null for lines tagged with a `[task:<id>]` prefix (or passed explicitly to `section`/`quiet`) and `null` for pipeline-wide messages such as the configuration dump and DAG topology. Use this to stream the full run process into UIs without tailing the log file.
308
- - `pipeline_end` — pipeline finished; includes `success: boolean`
309
- - `runId` -- caller-supplied run ID. When provided the engine uses this instead of generating its own, keeping the caller and the SDK log directories aligned on the same ID
310
- - `maxLogRuns` -- number of per-run log directories to keep under `<workDir>/.tagma/logs/` (default: 20)
311
-
312
- ### `PipelineRunner`
313
-
314
- Higher-level wrapper for managing multiple concurrent pipeline runs — designed for sidecar / Tauri IPC scenarios where the frontend controls pipeline lifecycle by ID.
315
-
316
- ```ts
317
- const runner = new PipelineRunner(config, workDir);
318
-
319
- // Subscribe before start — handler is called for every PipelineEvent
320
- const unsubscribe = runner.subscribe(event => {
321
- tauriEmit('pipeline_event', { id: runner.instanceId, event });
322
- });
323
-
324
- runner.start(); // returns Promise<EngineResult>, idempotent
325
-
326
- // Cancel from IPC
327
- runner.abort();
328
-
329
- // Available from the first pipeline_start event onward (not just after completion)
330
- // Returns null only if the pipeline has never started
331
- const states = runner.getStates(); // ReadonlyMap<taskId, TaskState> | null
332
- ```
333
-
334
- Properties:
335
- - `instanceId` — stable ID assigned at construction, safe to use as a Map key before `start()`
336
- - `runId` engine-assigned run ID, available after the first `pipeline_start` event (`null` until then)
337
- - `status` — `'idle' | 'running' | 'done' | 'aborted'`
338
-
339
- ### `TriggerBlockedError` / `TriggerTimeoutError`
340
-
341
- Typed error classes for trigger plugin error classification. The engine uses `instanceof` checks on these to set the correct task status (`blocked` or `timeout`) instead of matching on error message substrings.
342
-
343
- Built-in triggers (`manual`, `file`) throw these automatically. Third-party trigger plugins should throw `TriggerBlockedError` for user/policy rejections and `TriggerTimeoutError` for genuine wait timeouts. Plugins that throw plain `Error` still work — the engine falls back to string matching for backward compatibility, but typed errors are preferred to avoid misclassification from coincidental substrings.
344
-
345
- ```ts
346
- import { TriggerBlockedError, TriggerTimeoutError } from '@tagma/sdk';
347
-
348
- // In a custom trigger plugin:
349
- throw new TriggerBlockedError('Access denied by policy');
350
- throw new TriggerTimeoutError('File did not appear within 30s');
351
- ```
352
-
353
- ### `loadPlugins(names: string[]): Promise<void>`
354
-
355
- Dynamically loads and registers external plugin packages.
356
-
357
- ### `registerPlugin(category, type, handler): void`
358
-
359
- Registers a plugin handler manually. Idempotent — duplicate registrations are silently ignored.
360
-
361
- Plugin handlers (`TriggerPlugin`, `CompletionPlugin`, `MiddlewarePlugin`) may optionally expose a declarative `schema: PluginSchema` field so visual editors can render a typed form for the plugin's config instead of a raw key/value editor:
362
-
363
- ```ts
364
- import type { TriggerPlugin } from '@tagma/types';
365
-
366
- export const HttpTrigger: TriggerPlugin = {
367
- name: 'http',
368
- schema: {
369
- description: 'Wait for an HTTP endpoint to return 2xx before the task runs.',
370
- fields: {
371
- url: { type: 'string', required: true, placeholder: 'https://...' },
372
- method: { type: 'enum', enum: ['GET', 'POST'], default: 'GET' },
373
- timeout:{ type: 'duration', description: 'Give up after this long.' },
374
- },
375
- },
376
- async watch(config, ctx) { /* ... */ },
377
- };
378
- ```
379
-
380
- The schema is purely descriptive — plugins still perform their own runtime validation. Supported field types: `string`, `number`, `boolean`, `enum`, `path`, `duration`, `number-or-list`. Each field can declare `required`, `default`, `description`, `enum`, `min`/`max`, `placeholder`. Built-in plugins (`file`/`manual` triggers; `exit_code`/`file_exists`/`output_check` completions; `static_context` middleware) all ship with schemas so editors can generate forms out of the box.
381
-
382
- ### `getHandler(category, type): PluginType`
383
-
384
- Retrieves a registered plugin handler. Throws if the plugin is not registered.
385
-
386
- ### `hasHandler(category, type): boolean`
387
-
388
- Returns `true` if a handler is registered for the given category and type.
389
-
390
- ### `listRegistered(category): string[]`
391
-
392
- Lists all registered handler type names for a plugin category (`'drivers'`, `'triggers'`, `'completions'`, `'middlewares'`).
393
-
394
- ### `resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig`
395
-
396
- Resolves a raw pipeline config into a fully resolved `PipelineConfig` — applies inheritance (pipeline → track → task) for driver, model, permissions, and cwd. Validates and resolves all file paths against `workDir`.
397
-
398
- Use `loadPipeline` for the common parse-and-resolve flow. Use `resolveConfig` directly when you need to manipulate the raw config between parsing and resolution.
399
-
400
- ### `attachStdinApprovalAdapter(gateway): StdinApprovalAdapter`
401
-
402
- Attaches an interactive stdin-based approval handler.
403
-
404
- ### `attachWebSocketApprovalAdapter(gateway, options?): WebSocketApprovalAdapter`
405
-
406
- Starts a WebSocket server for remote approval decisions.
407
-
408
- ### Config CRUD (`config-ops`)
409
-
410
- Pure, immutable helper functions for building and editing `RawPipelineConfig` in a visual editor. No runtime dependencies — safe to use in renderer processes.
411
-
412
- ```ts
413
- import {
414
- createEmptyPipeline, setPipelineField,
415
- upsertTrack, removeTrack, moveTrack, updateTrack,
416
- upsertTask, removeTask, moveTask, transferTask,
417
- serializePipeline,
418
- } from '@tagma/sdk';
419
-
420
- // Build a config programmatically
421
- let config = createEmptyPipeline('my-pipeline');
422
- config = upsertTrack(config, { id: 'backend', name: 'Backend', tasks: [] });
423
- config = upsertTask(config, 'backend', { id: 'implement', prompt: 'Add /health endpoint' });
424
-
425
- // Sync back to YAML
426
- const yaml = serializePipeline(config);
427
- ```
428
-
429
- | Function | Description |
430
- |---|---|
431
- | `createEmptyPipeline(name)` | Create a minimal pipeline config |
432
- | `setPipelineField(config, fields)` | Update top-level pipeline fields |
433
- | `upsertTrack(config, track)` | Insert or replace a track by id |
434
- | `removeTrack(config, trackId)` | Remove a track |
435
- | `moveTrack(config, trackId, toIndex)` | Reorder a track |
436
- | `updateTrack(config, trackId, fields)` | Patch track fields (not tasks) |
437
- | `upsertTask(config, trackId, task)` | Insert or replace a task |
438
- | `removeTask(config, trackId, taskId, cleanRefs?)` | Remove a task; pass `cleanRefs: true` to also strip dangling `depends_on` / `continue_from` references. Only refs that resolve to the deleted task are removed — same-named tasks in other tracks are unaffected |
439
- | `moveTask(config, trackId, taskId, toIndex)` | Reorder a task within its track |
440
- | `transferTask(config, fromTrackId, taskId, toTrackId, qualifyRefs?)` | Move a task across tracks. When `qualifyRefs` is `true` (default), bare `depends_on` / `continue_from` references to the moved task are converted to fully-qualified form (`toTrackId.taskId`) so same-track resolution stays correct |
441
-
442
- ### `parseYaml(content: string): RawPipelineConfig`
443
-
444
- Parses a YAML string and returns the raw (unresolved) pipeline config. Use this when you need to edit and re-save YAML without losing relative paths or user-authored formattingpass the result to `serializePipeline()` rather than going through `loadPipeline()`.
445
-
446
- ### `deresolvePipeline(config: PipelineConfig, workDir: string): RawPipelineConfig`
447
-
448
- Converts a resolved `PipelineConfig` back to a `RawPipelineConfig` suitable for serialization. Strips injected defaults and converts absolute `cwd` paths back to relative so the output YAML is portable across machines.
449
-
450
- Use this when you have a programmatically modified resolved config and need to save it back to YAML:
451
-
452
- ```ts
453
- // Correct: load → modify resolved config → deresolve → save
454
- const config = await loadPipeline(yaml, workDir);
455
- const modified = { ...config, name: 'renamed' };
456
- const savedYaml = serializePipeline(deresolvePipeline(modified, workDir));
457
-
458
- // Also correct: work entirely in raw space (preferred for visual editors)
459
- const raw = parseYaml(yaml);
460
- const updatedRaw = setPipelineField(raw, { name: 'renamed' });
461
- const savedYaml = serializePipeline(updatedRaw);
462
- ```
463
-
464
- ### `validateConfig(config: PipelineConfig): string[]`
465
-
466
- Validates a resolved pipeline config without executing it. Checks DAG structure (cycles, missing dependencies). Returns an array of error message strings — empty means valid.
467
-
468
- Use `validateRaw` for editing raw configs in a UI; use `validateConfig` after `resolveConfig` for a final pre-run check.
469
-
470
- ### `validateRaw(config: RawPipelineConfig): ValidationError[]`
471
-
472
- Validates a raw pipeline config without resolving inheritance or executing anything. Returns a flat list of `{ path, message }` objects — empty array means valid.
473
-
474
- Checks: required fields, `prompt`/`command` exclusivity, duplicate task IDs within a track, `depends_on`/`continue_from` reference integrity (including ambiguous bare refs that exist in multiple tracks — use `trackId.taskId` to disambiguate), circular dependency detection.
475
-
476
- Does **not** check plugin registration (plugins may not be loaded at edit time).
477
-
478
- ```ts
479
- const errors = validateRaw(draftConfig);
480
- if (errors.length > 0) {
481
- errors.forEach(e => highlightNode(e.path, e.message));
482
- }
483
- ```
484
-
485
- ### `buildRawDag(config: RawPipelineConfig): RawDag`
486
-
487
- Extracts the topology of a raw (unresolved) pipeline config as a graph — no `workDir` or plugin registration required. Intended for the visual editor to render the flow graph during editing.
488
-
489
- Returns `{ nodes: ReadonlyMap<taskId, RawDagNode>, edges: { from, to }[] }` where each edge represents a dependency (from must complete before to). Unresolvable refs are silently skipped.
490
-
491
- ```ts
492
- const { nodes, edges } = buildRawDag(draftConfig);
493
- // nodeskeyed by "trackId.taskId"
494
- // edges — [{ from: "track.taskA", to: "track.taskB" }, ...]
495
- ```
496
-
497
- Use `buildDag` instead when you have a fully resolved `PipelineConfig` and need topological sort order.
498
-
499
- ### `Logger`
500
-
501
- Dual-channel logger — console + file. Creates a per-run log file at `<workDir>/.tagma/logs/<runId>/pipeline.log`.
502
-
503
- ```ts
504
- const logger = new Logger(workDir, runId);
505
- logger.info('[track]', 'message'); // console + file
506
- logger.warn('[track]', 'message'); // console + file
507
- logger.error('[track]', 'message'); // console + file
508
- logger.debug('[track]', 'message'); // file only
509
- logger.section('Title'); // file only — visual separator
510
- logger.quiet(bulkText); // file only bulk payload
511
- logger.path; // log file path
512
- logger.dir; // run artifact directory
513
- logger.close(); // close the persistent file handle (called automatically by runPipeline at run completion)
514
- ```
515
-
516
- Pass an optional third argument to stream every appended line out as a
517
- structured `LogRecord` `runPipeline` uses this to emit `task_log` events:
518
-
519
- ```ts
520
- import { Logger, type LogRecord } from '@tagma/sdk';
521
-
522
- const logger = new Logger(workDir, runId, (record: LogRecord) => {
523
- // record = { level, taskId, timestamp, text }
524
- // level = 'info' | 'warn' | 'error' | 'debug' | 'section' | 'quiet'
525
- // taskId is extracted from a '[task:<id>]' prefix, or null for untagged lines
526
- forwardToUI(record);
527
- });
528
- ```
529
-
530
- `section` and `quiet` carry no prefix, so pass an explicit `taskId` when the
531
- line logically belongs to a task the extractor cannot infer one otherwise:
532
-
533
- ```ts
534
- logger.section(`Task ${taskId}`, taskId);
535
- logger.quiet(`--- stdout (${taskId}) ---\n${body}\n--- end stdout ---`, taskId);
536
- ```
537
-
538
- ### `tailLines(text: string, n: number): string`
539
-
540
- Returns the last `n` non-empty lines of `text`, joined with newlines.
541
-
542
- ### `clip(text: string, maxBytes?: number): string`
543
-
544
- Truncates `text` to at most `maxBytes` UTF-8 bytes (default 16 KB), appending a `…[truncated N bytes]` marker when truncation occurs. Multi-byte characters (CJK, emoji) are counted correctly.
545
-
546
- ### Utilities
547
-
548
- | Function | Description |
549
- |---|---|
550
- | `parseDuration(input)` | Parses `"30s"`, `"5m"`, `"2h"` milliseconds |
551
- | `validatePath(filePath, projectRoot)` | Resolves path, throws if it escapes project root |
552
- | `generateRunId()` | Generates a unique run ID (`run_<ts>_<seq>_<rand>`) |
553
- | `nowISO()` | Returns `new Date().toISOString()` |
554
- | `truncateForName(text, maxLen?)` | Truncates first line to `maxLen` (default 40) for display |
555
-
556
- ## Related Packages
557
-
558
- | Package | Description |
559
- |---|---|
560
- | [@tagma/types](https://www.npmjs.com/package/@tagma/types) | Shared TypeScript types |
561
- | [@tagma/driver-codex](https://www.npmjs.com/package/@tagma/driver-codex) | Codex CLI driver plugin |
562
- | [@tagma/driver-opencode](https://www.npmjs.com/package/@tagma/driver-opencode) | OpenCode CLI driver plugin |
563
-
564
- ## License
565
-
566
- MIT
1
+ # @tagma/sdk
2
+
3
+ > ## ⚠️ Bun-only runtime
4
+ >
5
+ > ```bash
6
+ > bun add @tagma/sdk
7
+ > ```
8
+ >
9
+ > This package is published as pre-built JavaScript (`dist/`), but the runtime
10
+ > uses Bun-only APIs (`Bun.spawn`, `Bun.file`, `Bun.serve`). It will install
11
+ > under `npm` / `yarn` / `pnpm` without error, but **crash at runtime on Node**
12
+ > the first time a pipeline spawns a task. Get Bun at <https://bun.sh>.
13
+ >
14
+ > _(The `npm i @tagma/sdk` line in the npmjs.com sidebar is auto-generated by
15
+ > the npm registry website and cannot be removed — please ignore it and use
16
+ > the command above.)_
17
+
18
+ A local AI task orchestration SDK for [Bun](https://bun.sh). Define multi-track pipelines in YAML, run AI coding agents (Claude Code, Codex, OpenCode) and shell commands in parallel with dependency resolution, approval gates, and lifecycle hooks.
19
+
20
+ ## Install
21
+
22
+ **Requires Bun ≥ 1.3.** Node/npm are not supported.
23
+
24
+ ```bash
25
+ bun add @tagma/sdk
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ **1. Define a pipeline** (`pipeline.yaml`)
31
+
32
+ ```yaml
33
+ pipeline:
34
+ name: build-and-test
35
+ tracks:
36
+ - id: backend
37
+ name: Backend
38
+ driver: claude-code
39
+ permissions: { read: true, write: true, execute: false }
40
+ tasks:
41
+ - id: implement
42
+ name: Implement feature
43
+ prompt: 'Add a /health endpoint to src/server.ts'
44
+ output: ./output/implement.txt
45
+ - id: test
46
+ name: Run tests
47
+ command: 'bun test'
48
+ depends_on: [implement]
49
+ ```
50
+
51
+ **2. Run it programmatically**
52
+
53
+ ```ts
54
+ import { bootstrapBuiltins, loadPipeline, runPipeline, InMemoryApprovalGateway } from '@tagma/sdk';
55
+
56
+ // Register built-in drivers, triggers, completions
57
+ bootstrapBuiltins();
58
+
59
+ const yaml = await Bun.file('pipeline.yaml').text();
60
+ const config = await loadPipeline(yaml, process.cwd());
61
+
62
+ const result = await runPipeline(config, process.cwd());
63
+ console.log(result.success ? 'Done' : 'Failed');
64
+ ```
65
+
66
+ ## Features
67
+
68
+ - **Multi-track DAG execution** -- tasks run in parallel across tracks, respecting `depends_on` ordering
69
+ - **Driver plugins** -- built-in `claude-code` driver; install `@tagma/driver-codex` or `@tagma/driver-opencode` for other agents
70
+ - **Session handoff** -- `continue_from` passes context between tasks (session resume or text injection)
71
+ - **Approval gates** -- trigger-based approval with stdin and WebSocket adapters
72
+ - **Lifecycle hooks** -- `pipeline_start`, `task_start`, `task_success`, `task_failure`, `pipeline_complete`, `pipeline_error`
73
+ - **Middleware** -- enrich prompts before execution (e.g. inject static context)
74
+ - **Completion checks** -- validate task output with `exit_code`, `file_exists`, or `output_check` plugins
75
+ - **Plugin schemas** -- triggers/completions/middlewares can declare a `PluginSchema` so visual editors render typed forms for their config
76
+
77
+ ## Pipeline YAML Reference
78
+
79
+ ### Full Structure
80
+
81
+ ```yaml
82
+ pipeline:
83
+ name: my-pipeline
84
+ driver: claude-code
85
+ timeout: 30m
86
+ plugins:
87
+ - '@tagma/driver-codex'
88
+ hooks:
89
+ pipeline_start: 'echo starting'
90
+ task_start: 'echo task begin'
91
+ task_success: 'echo task ok'
92
+ task_failure: 'notify-slack.sh'
93
+ pipeline_complete: 'echo done'
94
+ pipeline_error: 'alert.sh'
95
+ tracks:
96
+ - id: track-1
97
+ name: Track One
98
+ color: '#3b82f6'
99
+ driver: claude-code
100
+ model: claude-sonnet-4-6
101
+ agent_profile: senior
102
+ cwd: ./services/backend
103
+ permissions:
104
+ read: true
105
+ write: true
106
+ execute: false
107
+ on_failure: skip_downstream
108
+ middlewares:
109
+ - type: static_context
110
+ file: ./context.md
111
+ label: Architecture Guide
112
+ tasks:
113
+ - id: task-a
114
+ name: Do something
115
+ prompt: 'Your prompt here'
116
+ output: ./output/task-a.txt
117
+ timeout: 10m
118
+ driver: claude-code
119
+ model: claude-sonnet-4-6
120
+ agent_profile: senior
121
+ cwd: ./src
122
+ permissions:
123
+ read: true
124
+ write: true
125
+ execute: false
126
+ middlewares:
127
+ - type: static_context
128
+ file: ./ref.md
129
+ trigger:
130
+ type: manual
131
+ message: 'Approve before running'
132
+ timeout: 5m
133
+ completion:
134
+ type: exit_code
135
+ expect: 0
136
+ - id: task-b
137
+ name: Follow up
138
+ prompt: 'Continue the work'
139
+ continue_from: task-a
140
+ depends_on: [task-a]
141
+ ```
142
+
143
+ ### Pipeline Fields
144
+
145
+ | Field | Type | Required | Description |
146
+ | --------- | --------------- | -------- | ------------------------------------------------------------------------------------------ |
147
+ | `name` | `string` | Yes | Pipeline name, used in logs and run IDs |
148
+ | `driver` | `string` | No | Default driver for all tracks/tasks (inherited). Built-in: `claude-code` |
149
+ | `model` | `string` | No | Default model for all tracks/tasks (inherited). Exact model name, e.g. `claude-sonnet-4-6` |
150
+ | `timeout` | `string` | No | Pipeline-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
151
+ | `plugins` | `string[]` | No | External plugin packages to load, e.g. `["@tagma/driver-codex"]` |
152
+ | `hooks` | `HooksConfig` | No | Shell commands to run at lifecycle events (see Hooks below) |
153
+ | `tracks` | `TrackConfig[]` | Yes | List of parallel execution tracks |
154
+
155
+ ### Hooks Fields
156
+
157
+ Each hook value can be a single command string or an array of commands.
158
+
159
+ | Field | Type | Description |
160
+ | ------------------- | -------------------- | -------------------------------------------- |
161
+ | `pipeline_start` | `string \| string[]` | Runs when the pipeline begins |
162
+ | `task_start` | `string \| string[]` | Runs before each task starts |
163
+ | `task_success` | `string \| string[]` | Runs after a task succeeds |
164
+ | `task_failure` | `string \| string[]` | Runs after a task fails |
165
+ | `pipeline_complete` | `string \| string[]` | Runs when the pipeline finishes successfully |
166
+ | `pipeline_error` | `string \| string[]` | Runs when the pipeline finishes with errors |
167
+
168
+ ### Track Fields
169
+
170
+ | Field | Type | Required | Default | Description |
171
+ | --------------- | -------------------- | -------- | ----------------------- | -------------------------------------------------------------------- |
172
+ | `id` | `string` | Yes | — | Unique track identifier |
173
+ | `name` | `string` | Yes | — | Display name |
174
+ | `color` | `string` | No | — | Color hint for UI rendering (e.g. `"#3b82f6"`) |
175
+ | `driver` | `string` | No | Inherited from pipeline | Driver for all tasks in this track |
176
+ | `model` | `string` | No | Inherited from pipeline | Exact model name passed to the driver CLI (e.g. `claude-sonnet-4-6`) |
177
+ | `agent_profile` | `string` | No | — | Named agent configuration profile |
178
+ | `cwd` | `string` | No | Pipeline workDir | Working directory for tasks in this track (relative path) |
179
+ | `permissions` | `Permissions` | No | Inherited from pipeline | Default permissions for tasks (see Permissions) |
180
+ | `on_failure` | `OnFailure` | No | `skip_downstream` | Failure strategy: `skip_downstream`, `stop_all`, `ignore` |
181
+ | `middlewares` | `MiddlewareConfig[]` | No | | Middlewares applied to all tasks (task-level overrides track-level) |
182
+ | `tasks` | `TaskConfig[]` | Yes | — | Ordered list of tasks in this track |
183
+
184
+ ### Task Fields
185
+
186
+ | Field | Type | Required | Default | Description |
187
+ | --------------- | -------------------- | -------- | -------------------- | ------------------------------------------------------------------------------------------------------ |
188
+ | `id` | `string` | Yes | — | Unique task identifier (unique within the pipeline) |
189
+ | `name` | `string` | No | — | Display name |
190
+ | `prompt` | `string` | No\* | — | AI prompt to send to the driver. \*Mutually exclusive with `command` |
191
+ | `command` | `string` | No\* | | Shell command to execute directly. \*Mutually exclusive with `prompt` |
192
+ | `depends_on` | `string[]` | No | — | Task IDs that must complete before this task runs. Cross-track refs use `trackId.taskId` |
193
+ | `continue_from` | `string` | No | — | Task ID whose output/session to continue from (session handoff). Cross-track refs use `trackId.taskId` |
194
+ | `output` | `string` | No | — | File path to write task stdout to (relative to workDir) |
195
+ | `driver` | `string` | No | Inherited from track | Driver override for this task |
196
+ | `model` | `string` | No | Inherited from track | Model name override for this task |
197
+ | `agent_profile` | `string` | No | Inherited from track | Agent profile override |
198
+ | `cwd` | `string` | No | Inherited from track | Working directory override (relative path) |
199
+ | `timeout` | `string` | No | — | Task-level timeout. Format: `"30s"`, `"5m"`, `"2h"` |
200
+ | `permissions` | `Permissions` | No | Inherited from track | Permission override (see Permissions) |
201
+ | `middlewares` | `MiddlewareConfig[]` | No | Inherited from track | Middleware override. Set `[]` to disable inherited middlewares |
202
+ | `trigger` | `TriggerConfig` | No | | Gate that must resolve before the task runs (see Triggers) |
203
+ | `completion` | `CompletionConfig` | No | | Post-execution check to validate task output (see Completions) |
204
+
205
+ ### Permissions
206
+
207
+ | Field | Type | Default | Description |
208
+ | --------- | --------- | ------- | ----------------------------------- |
209
+ | `read` | `boolean` | — | Allow the agent to read files |
210
+ | `write` | `boolean` | — | Allow the agent to write files |
211
+ | `execute` | `boolean` | — | Allow the agent to execute commands |
212
+
213
+ ### Inheritance
214
+
215
+ Fields are inherited top-down: **pipeline track task**. A value set at a lower level overrides the inherited value.
216
+
217
+ Inherited fields: `driver`, `model`, `permissions`, `cwd`, `middlewares`.
218
+
219
+ Track-level `middlewares` apply to all tasks in the track. Setting task-level `middlewares` **replaces** (not appends) the track-level list. Use `middlewares: []` to disable all inherited middlewares for a task.
220
+
221
+ ---
222
+
223
+ ### Built-in Triggers
224
+
225
+ #### `manual` — Human approval gate
226
+
227
+ | Field | Type | Required | Default | Description |
228
+ | ---------- | ---------- | -------- | ------------------------------------------------------ | ------------------------------------------------- |
229
+ | `type` | `"manual"` | Yes | — | Trigger type |
230
+ | `message` | `string` | No | `"Manual confirmation required for task \"{taskId}\""` | Message shown to the approver |
231
+ | `timeout` | `string` | No | — | How long to wait for a decision before timing out |
232
+ | `metadata` | `object` | No | | Arbitrary metadata passed to the approval gateway |
233
+
234
+ #### `file` — File watcher gate
235
+
236
+ | Field | Type | Required | Default | Description |
237
+ | --------- | -------- | -------- | ------- | ---------------------------------------------- |
238
+ | `type` | `"file"` | Yes | — | Trigger type |
239
+ | `path` | `string` | Yes | | File path to watch (relative to workDir) |
240
+ | `timeout` | `string` | No | — | How long to wait for the file to appear/change |
241
+
242
+ ---
243
+
244
+ ### Built-in Completions
245
+
246
+ #### `exit_code` — Exit code check
247
+
248
+ | Field | Type | Required | Default | Description |
249
+ | -------- | -------------------- | -------- | ------- | ------------------------------------------------------------------ |
250
+ | `type` | `"exit_code"` | Yes | — | Completion type |
251
+ | `expect` | `number \| number[]` | No | `0` | Expected exit code(s). Pass an array for multiple acceptable codes |
252
+
253
+ #### `file_exists` File existence check
254
+
255
+ | Field | Type | Required | Default | Description |
256
+ | ---------- | -------------------------- | -------- | ------- | ----------------------------------------------------- |
257
+ | `type` | `"file_exists"` | Yes | — | Completion type |
258
+ | `path` | `string` | Yes | | File or directory path to check (relative to workDir) |
259
+ | `kind` | `"file" \| "dir" \| "any"` | No | `"any"` | Entity type constraint |
260
+ | `min_size` | `number` | No | | Minimum file size in bytes (files only) |
261
+
262
+ #### `output_check` — Command-based output validation
263
+
264
+ | Field | Type | Required | Default | Description |
265
+ | --------- | ---------------- | -------- | ------- | ----------------------------------------------------------------------- |
266
+ | `type` | `"output_check"` | Yes | — | Completion type |
267
+ | `check` | `string` | Yes | | Shell command to run. Task stdout is piped to its stdin; exits 0 = pass |
268
+ | `timeout` | `string` | No | `"30s"` | Max time to wait for the check command |
269
+
270
+ ---
271
+
272
+ ### Built-in Middlewares
273
+
274
+ #### `static_context` — Prepend file content to prompt
275
+
276
+ | Field | Type | Required | Default | Description |
277
+ | ------- | ------------------ | -------- | ------------------------- | ---------------------------------------------- |
278
+ | `type` | `"static_context"` | Yes | — | Middleware type |
279
+ | `file` | `string` | Yes | | Path to the context file (relative to workDir) |
280
+ | `label` | `string` | No | `"Reference: {filename}"` | Label for the injected context section |
281
+
282
+ ## API
283
+
284
+ ### `bootstrapBuiltins()`
285
+
286
+ Registers all built-in plugins (claude-code driver, file/manual triggers, completion checks, static-context middleware).
287
+
288
+ ### `loadPipeline(yaml: string, workDir: string): Promise<PipelineConfig>`
289
+
290
+ Parses YAML, resolves inheritance, and validates the configuration.
291
+
292
+ ### `runPipeline(config, workDir, options?): Promise<EngineResult>`
293
+
294
+ Executes the pipeline. Returns `{ success, runId, logPath, summary, states }`.
295
+
296
+ Options:
297
+
298
+ - `approvalGateway` -- custom `ApprovalGateway` instance (defaults to `InMemoryApprovalGateway`)
299
+ - `signal` -- `AbortSignal` to cancel the run externally
300
+ - `onEvent` -- callback for real-time `PipelineEvent` updates:
301
+ - `pipeline_start` — pipeline began; includes `states: ReadonlyMap<taskId, TaskState>` (initial snapshot of all tasks at `waiting`)
302
+ - `task_status_change` a task changed status; includes `state: TaskState` (complete snapshot at the time of change: `startedAt` is populated before the `running` event; `result` and `finishedAt` are populated before any terminal-status event)
303
+ - `task_log` a structured log line was written to `pipeline.log`. Mirrors every `Logger` call (info/warn/error/debug/section/quiet) and carries `{ taskId: string | null, level, timestamp, text }`. `taskId` is non-null for lines tagged with a `[task:<id>]` prefix (or passed explicitly to `section`/`quiet`) and `null` for pipeline-wide messages such as the configuration dump and DAG topology. Use this to stream the full run process into UIs without tailing the log file.
304
+ - `pipeline_end` pipeline finished; includes `success: boolean`
305
+ - `runId` -- caller-supplied run ID. When provided the engine uses this instead of generating its own, keeping the caller and the SDK log directories aligned on the same ID
306
+ - `maxLogRuns` -- number of per-run log directories to keep under `<workDir>/.tagma/logs/` (default: 20)
307
+
308
+ ### `PipelineRunner`
309
+
310
+ Higher-level wrapper for managing multiple concurrent pipeline runs designed for sidecar / Tauri IPC scenarios where the frontend controls pipeline lifecycle by ID.
311
+
312
+ ```ts
313
+ const runner = new PipelineRunner(config, workDir);
314
+
315
+ // Subscribe before start — handler is called for every PipelineEvent
316
+ const unsubscribe = runner.subscribe((event) => {
317
+ tauriEmit('pipeline_event', { id: runner.instanceId, event });
318
+ });
319
+
320
+ runner.start(); // returns Promise<EngineResult>, idempotent
321
+
322
+ // Cancel from IPC
323
+ runner.abort();
324
+
325
+ // Available from the first pipeline_start event onward (not just after completion)
326
+ // Returns null only if the pipeline has never started
327
+ const states = runner.getStates(); // ReadonlyMap<taskId, TaskState> | null
328
+ ```
329
+
330
+ Properties:
331
+
332
+ - `instanceId` — stable ID assigned at construction, safe to use as a Map key before `start()`
333
+ - `runId` — engine-assigned run ID, available after the first `pipeline_start` event (`null` until then)
334
+ - `status` — `'idle' | 'running' | 'done' | 'aborted'`
335
+
336
+ ### `TriggerBlockedError` / `TriggerTimeoutError`
337
+
338
+ Typed error classes for trigger plugin error classification. The engine uses `instanceof` checks on these to set the correct task status (`blocked` or `timeout`) instead of matching on error message substrings.
339
+
340
+ Built-in triggers (`manual`, `file`) throw these automatically. Third-party trigger plugins should throw `TriggerBlockedError` for user/policy rejections and `TriggerTimeoutError` for genuine wait timeouts. Plugins that throw plain `Error` still work — the engine falls back to string matching for backward compatibility, but typed errors are preferred to avoid misclassification from coincidental substrings.
341
+
342
+ ```ts
343
+ import { TriggerBlockedError, TriggerTimeoutError } from '@tagma/sdk';
344
+
345
+ // In a custom trigger plugin:
346
+ throw new TriggerBlockedError('Access denied by policy');
347
+ throw new TriggerTimeoutError('File did not appear within 30s');
348
+ ```
349
+
350
+ ### `loadPlugins(names: string[]): Promise<void>`
351
+
352
+ Dynamically loads and registers external plugin packages.
353
+
354
+ ### `registerPlugin(category, type, handler): void`
355
+
356
+ Registers a plugin handler manually. Idempotent — duplicate registrations are silently ignored.
357
+
358
+ Plugin handlers (`TriggerPlugin`, `CompletionPlugin`, `MiddlewarePlugin`) may optionally expose a declarative `schema: PluginSchema` field so visual editors can render a typed form for the plugin's config instead of a raw key/value editor:
359
+
360
+ ```ts
361
+ import type { TriggerPlugin } from '@tagma/types';
362
+
363
+ export const HttpTrigger: TriggerPlugin = {
364
+ name: 'http',
365
+ schema: {
366
+ description: 'Wait for an HTTP endpoint to return 2xx before the task runs.',
367
+ fields: {
368
+ url: { type: 'string', required: true, placeholder: 'https://...' },
369
+ method: { type: 'enum', enum: ['GET', 'POST'], default: 'GET' },
370
+ timeout: { type: 'duration', description: 'Give up after this long.' },
371
+ },
372
+ },
373
+ async watch(config, ctx) {
374
+ /* ... */
375
+ },
376
+ };
377
+ ```
378
+
379
+ The schema is purely descriptive — plugins still perform their own runtime validation. Supported field types: `string`, `number`, `boolean`, `enum`, `path`, `duration`, `number-or-list`. Each field can declare `required`, `default`, `description`, `enum`, `min`/`max`, `placeholder`. Built-in plugins (`file`/`manual` triggers; `exit_code`/`file_exists`/`output_check` completions; `static_context` middleware) all ship with schemas so editors can generate forms out of the box.
380
+
381
+ ### `getHandler(category, type): PluginType`
382
+
383
+ Retrieves a registered plugin handler. Throws if the plugin is not registered.
384
+
385
+ ### `hasHandler(category, type): boolean`
386
+
387
+ Returns `true` if a handler is registered for the given category and type.
388
+
389
+ ### `listRegistered(category): string[]`
390
+
391
+ Lists all registered handler type names for a plugin category (`'drivers'`, `'triggers'`, `'completions'`, `'middlewares'`).
392
+
393
+ ### `resolveConfig(raw: RawPipelineConfig, workDir: string): PipelineConfig`
394
+
395
+ Resolves a raw pipeline config into a fully resolved `PipelineConfig` — applies inheritance (pipeline → track → task) for driver, model, permissions, and cwd. Validates and resolves all file paths against `workDir`.
396
+
397
+ Use `loadPipeline` for the common parse-and-resolve flow. Use `resolveConfig` directly when you need to manipulate the raw config between parsing and resolution.
398
+
399
+ ### `attachStdinApprovalAdapter(gateway): StdinApprovalAdapter`
400
+
401
+ Attaches an interactive stdin-based approval handler.
402
+
403
+ ### `attachWebSocketApprovalAdapter(gateway, options?): WebSocketApprovalAdapter`
404
+
405
+ Starts a WebSocket server for remote approval decisions.
406
+
407
+ ### Config CRUD (`config-ops`)
408
+
409
+ Pure, immutable helper functions for building and editing `RawPipelineConfig` in a visual editor. No runtime dependencies — safe to use in renderer processes.
410
+
411
+ ```ts
412
+ import {
413
+ createEmptyPipeline,
414
+ setPipelineField,
415
+ upsertTrack,
416
+ removeTrack,
417
+ moveTrack,
418
+ updateTrack,
419
+ upsertTask,
420
+ removeTask,
421
+ moveTask,
422
+ transferTask,
423
+ serializePipeline,
424
+ } from '@tagma/sdk';
425
+
426
+ // Build a config programmatically
427
+ let config = createEmptyPipeline('my-pipeline');
428
+ config = upsertTrack(config, { id: 'backend', name: 'Backend', tasks: [] });
429
+ config = upsertTask(config, 'backend', { id: 'implement', prompt: 'Add /health endpoint' });
430
+
431
+ // Sync back to YAML
432
+ const yaml = serializePipeline(config);
433
+ ```
434
+
435
+ | Function | Description |
436
+ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
437
+ | `createEmptyPipeline(name)` | Create a minimal pipeline config |
438
+ | `setPipelineField(config, fields)` | Update top-level pipeline fields |
439
+ | `upsertTrack(config, track)` | Insert or replace a track by id |
440
+ | `removeTrack(config, trackId)` | Remove a track |
441
+ | `moveTrack(config, trackId, toIndex)` | Reorder a track |
442
+ | `updateTrack(config, trackId, fields)` | Patch track fields (not tasks) |
443
+ | `upsertTask(config, trackId, task)` | Insert or replace a task |
444
+ | `removeTask(config, trackId, taskId, cleanRefs?)` | Remove a task; pass `cleanRefs: true` to also strip dangling `depends_on` / `continue_from` references. Only refs that resolve to the deleted task are removed same-named tasks in other tracks are unaffected |
445
+ | `moveTask(config, trackId, taskId, toIndex)` | Reorder a task within its track |
446
+ | `transferTask(config, fromTrackId, taskId, toTrackId, qualifyRefs?)` | Move a task across tracks. When `qualifyRefs` is `true` (default), bare `depends_on` / `continue_from` references to the moved task are converted to fully-qualified form (`toTrackId.taskId`) so same-track resolution stays correct |
447
+
448
+ ### `parseYaml(content: string): RawPipelineConfig`
449
+
450
+ Parses a YAML string and returns the raw (unresolved) pipeline config. Use this when you need to edit and re-save YAML without losing relative paths or user-authored formatting — pass the result to `serializePipeline()` rather than going through `loadPipeline()`.
451
+
452
+ ### `deresolvePipeline(config: PipelineConfig, workDir: string): RawPipelineConfig`
453
+
454
+ Converts a resolved `PipelineConfig` back to a `RawPipelineConfig` suitable for serialization. Strips injected defaults and converts absolute `cwd` paths back to relative so the output YAML is portable across machines.
455
+
456
+ Use this when you have a programmatically modified resolved config and need to save it back to YAML:
457
+
458
+ ```ts
459
+ // Correct: load → modify resolved config → deresolve → save
460
+ const config = await loadPipeline(yaml, workDir);
461
+ const modified = { ...config, name: 'renamed' };
462
+ const savedYaml = serializePipeline(deresolvePipeline(modified, workDir));
463
+
464
+ // Also correct: work entirely in raw space (preferred for visual editors)
465
+ const raw = parseYaml(yaml);
466
+ const updatedRaw = setPipelineField(raw, { name: 'renamed' });
467
+ const savedYaml = serializePipeline(updatedRaw);
468
+ ```
469
+
470
+ ### `validateConfig(config: PipelineConfig): string[]`
471
+
472
+ Validates a resolved pipeline config without executing it. Checks DAG structure (cycles, missing dependencies). Returns an array of error message strings — empty means valid.
473
+
474
+ Use `validateRaw` for editing raw configs in a UI; use `validateConfig` after `resolveConfig` for a final pre-run check.
475
+
476
+ ### `validateRaw(config: RawPipelineConfig): ValidationError[]`
477
+
478
+ Validates a raw pipeline config without resolving inheritance or executing anything. Returns a flat list of `{ path, message }` objects — empty array means valid.
479
+
480
+ Checks: required fields, `prompt`/`command` exclusivity, duplicate task IDs within a track, `depends_on`/`continue_from` reference integrity (including ambiguous bare refs that exist in multiple tracks — use `trackId.taskId` to disambiguate), circular dependency detection.
481
+
482
+ Does **not** check plugin registration (plugins may not be loaded at edit time).
483
+
484
+ ```ts
485
+ const errors = validateRaw(draftConfig);
486
+ if (errors.length > 0) {
487
+ errors.forEach((e) => highlightNode(e.path, e.message));
488
+ }
489
+ ```
490
+
491
+ ### `buildRawDag(config: RawPipelineConfig): RawDag`
492
+
493
+ Extracts the topology of a raw (unresolved) pipeline config as a graph no `workDir` or plugin registration required. Intended for the visual editor to render the flow graph during editing.
494
+
495
+ Returns `{ nodes: ReadonlyMap<taskId, RawDagNode>, edges: { from, to }[] }` where each edge represents a dependency (from must complete before to). Unresolvable refs are silently skipped.
496
+
497
+ ```ts
498
+ const { nodes, edges } = buildRawDag(draftConfig);
499
+ // nodes — keyed by "trackId.taskId"
500
+ // edges — [{ from: "track.taskA", to: "track.taskB" }, ...]
501
+ ```
502
+
503
+ Use `buildDag` instead when you have a fully resolved `PipelineConfig` and need topological sort order.
504
+
505
+ ### `Logger`
506
+
507
+ Dual-channel logger console + file. Creates a per-run log file at `<workDir>/.tagma/logs/<runId>/pipeline.log`.
508
+
509
+ ```ts
510
+ const logger = new Logger(workDir, runId);
511
+ logger.info('[track]', 'message'); // console + file
512
+ logger.warn('[track]', 'message'); // console + file
513
+ logger.error('[track]', 'message'); // console + file
514
+ logger.debug('[track]', 'message'); // file only
515
+ logger.section('Title'); // file only — visual separator
516
+ logger.quiet(bulkText); // file only bulk payload
517
+ logger.path; // log file path
518
+ logger.dir; // run artifact directory
519
+ logger.close(); // close the persistent file handle (called automatically by runPipeline at run completion)
520
+ ```
521
+
522
+ Pass an optional third argument to stream every appended line out as a
523
+ structured `LogRecord` `runPipeline` uses this to emit `task_log` events:
524
+
525
+ ```ts
526
+ import { Logger, type LogRecord } from '@tagma/sdk';
527
+
528
+ const logger = new Logger(workDir, runId, (record: LogRecord) => {
529
+ // record = { level, taskId, timestamp, text }
530
+ // level = 'info' | 'warn' | 'error' | 'debug' | 'section' | 'quiet'
531
+ // taskId is extracted from a '[task:<id>]' prefix, or null for untagged lines
532
+ forwardToUI(record);
533
+ });
534
+ ```
535
+
536
+ `section` and `quiet` carry no prefix, so pass an explicit `taskId` when the
537
+ line logically belongs to a task — the extractor cannot infer one otherwise:
538
+
539
+ ```ts
540
+ logger.section(`Task ${taskId}`, taskId);
541
+ logger.quiet(`--- stdout (${taskId}) ---\n${body}\n--- end stdout ---`, taskId);
542
+ ```
543
+
544
+ ### `tailLines(text: string, n: number): string`
545
+
546
+ Returns the last `n` non-empty lines of `text`, joined with newlines.
547
+
548
+ ### `clip(text: string, maxBytes?: number): string`
549
+
550
+ Truncates `text` to at most `maxBytes` UTF-8 bytes (default 16 KB), appending a `…[truncated N bytes]` marker when truncation occurs. Multi-byte characters (CJK, emoji) are counted correctly.
551
+
552
+ ### Utilities
553
+
554
+ | Function | Description |
555
+ | ------------------------------------- | --------------------------------------------------------- |
556
+ | `parseDuration(input)` | Parses `"30s"`, `"5m"`, `"2h"` → milliseconds |
557
+ | `validatePath(filePath, projectRoot)` | Resolves path, throws if it escapes project root |
558
+ | `generateRunId()` | Generates a unique run ID (`run_<ts>_<seq>_<rand>`) |
559
+ | `nowISO()` | Returns `new Date().toISOString()` |
560
+ | `truncateForName(text, maxLen?)` | Truncates first line to `maxLen` (default 40) for display |
561
+
562
+ ## Related Packages
563
+
564
+ | Package | Description |
565
+ | ------------------------------------------------------------------------------ | -------------------------- |
566
+ | [@tagma/types](https://www.npmjs.com/package/@tagma/types) | Shared TypeScript types |
567
+ | [@tagma/driver-codex](https://www.npmjs.com/package/@tagma/driver-codex) | Codex CLI driver plugin |
568
+ | [@tagma/driver-opencode](https://www.npmjs.com/package/@tagma/driver-opencode) | OpenCode CLI driver plugin |
569
+
570
+ ## License
571
+
572
+ MIT