@tagma/sdk 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,7 +113,10 @@ Executes the pipeline. Returns `{ success, runId, logPath, summary, states }`.
113
113
  Options:
114
114
  - `approvalGateway` -- custom `ApprovalGateway` instance (defaults to `InMemoryApprovalGateway`)
115
115
  - `signal` -- `AbortSignal` to cancel the run externally
116
- - `onEvent` -- callback for real-time `PipelineEvent` updates (task status changes, pipeline start/end)
116
+ - `onEvent` -- callback for real-time `PipelineEvent` updates:
117
+ - `pipeline_start` — pipeline began; includes `states: ReadonlyMap<taskId, TaskState>` (initial snapshot of all tasks at `waiting`)
118
+ - `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)
119
+ - `pipeline_end` — pipeline finished; includes `success: boolean`
117
120
  - `maxLogRuns` -- number of per-run log directories to keep under `<workDir>/logs/` (default: 20)
118
121
 
119
122
  ### `PipelineRunner`
@@ -133,8 +136,9 @@ runner.start(); // returns Promise<EngineResult>, idempotent
133
136
  // Cancel from IPC
134
137
  runner.abort();
135
138
 
136
- // After completion
137
- const states = runner.getStates(); // ReadonlyMap<taskId, TaskState>
139
+ // Available from the first pipeline_start event onward (not just after completion)
140
+ // Returns null only if the pipeline has never started
141
+ const states = runner.getStates(); // ReadonlyMap<taskId, TaskState> | null
138
142
  ```
139
143
 
140
144
  Properties:
@@ -184,7 +188,7 @@ const yaml = serializePipeline(config);
184
188
  | `moveTrack(config, trackId, toIndex)` | Reorder a track |
185
189
  | `updateTrack(config, trackId, fields)` | Patch track fields (not tasks) |
186
190
  | `upsertTask(config, trackId, task)` | Insert or replace a task |
187
- | `removeTask(config, trackId, taskId)` | Remove a task |
191
+ | `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 |
188
192
  | `moveTask(config, trackId, taskId, toIndex)` | Reorder a task within its track |
189
193
  | `transferTask(config, fromTrackId, taskId, toTrackId)` | Move a task across tracks |
190
194
 
@@ -220,7 +224,7 @@ Use `validateRaw` for editing raw configs in a UI; use `validateConfig` after `r
220
224
 
221
225
  Validates a raw pipeline config without resolving inheritance or executing anything. Returns a flat list of `{ path, message }` objects — empty array means valid.
222
226
 
223
- Checks: required fields, `prompt`/`command` exclusivity, `depends_on`/`continue_from` reference integrity, circular dependency detection.
227
+ Checks: required fields, `prompt`/`command` exclusivity, `depends_on`/`continue_from` reference integrity (including ambiguous bare refs that exist in multiple tracks — use `trackId.taskId` to disambiguate), circular dependency detection.
224
228
 
225
229
  Does **not** check plugin registration (plugins may not be loaded at edit time).
226
230
 
@@ -231,6 +235,20 @@ if (errors.length > 0) {
231
235
  }
232
236
  ```
233
237
 
238
+ ### `buildRawDag(config: RawPipelineConfig): RawDag`
239
+
240
+ 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.
241
+
242
+ Returns `{ nodes: ReadonlyMap<taskId, RawDagNode>, edges: { from, to }[] }` where each edge represents a dependency (from must complete before to). Template-expansion tasks (`use:` field) and unresolvable refs are silently skipped.
243
+
244
+ ```ts
245
+ const { nodes, edges } = buildRawDag(draftConfig);
246
+ // nodes — keyed by "trackId.taskId"
247
+ // edges — [{ from: "track.taskA", to: "track.taskB" }, ...]
248
+ ```
249
+
250
+ Use `buildDag` instead when you have a fully resolved `PipelineConfig` and need topological sort order.
251
+
234
252
  ## Related Packages
235
253
 
236
254
  | Package | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/sdk",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "plugins/*"
@@ -1,117 +1,117 @@
1
- import * as readline from 'readline';
2
- import type { ApprovalGateway, ApprovalRequest } from '../approval';
3
-
4
- // ═══ CLI Stdin Adapter ═══
5
- //
6
- // Subscribes to the gateway's 'requested' events, prompts the user on stdout,
7
- // reads a line from stdin, and calls gateway.resolve(). Handles at most one
8
- // prompt at a time — additional requests queue up.
9
-
10
- export interface StdinApprovalAdapter {
11
- readonly detach: () => void;
12
- }
13
-
14
- export function attachStdinApprovalAdapter(gateway: ApprovalGateway): StdinApprovalAdapter {
15
- const queue: ApprovalRequest[] = [];
16
- let processing = false;
17
- let rl: readline.Interface | null = null;
18
-
19
- function ensureReadline(): readline.Interface {
20
- if (!rl) {
21
- rl = readline.createInterface({ input: process.stdin, terminal: false });
22
- }
23
- return rl;
24
- }
25
-
26
- function readOneLine(): Promise<string> {
27
- return new Promise((resolvePromise) => {
28
- const reader = ensureReadline();
29
- const handler = (line: string): void => {
30
- reader.off('line', handler);
31
- resolvePromise(line);
32
- };
33
- reader.on('line', handler);
34
- });
35
- }
36
-
37
- async function processNext(): Promise<void> {
38
- if (processing) return;
39
- processing = true;
40
- try {
41
- while (queue.length > 0) {
42
- const req = queue.shift()!;
43
- // If the request was already resolved by another path while queued, skip it.
44
- if (!gateway.pending().some((p) => p.id === req.id)) continue;
45
-
46
- const optionsStr = req.options.join(' / ');
47
- process.stdout.write(
48
- `\n[APPROVAL REQUIRED] ${req.message}\n` +
49
- ` id: ${req.id}\n` +
50
- ` task: ${req.taskId}${req.trackId ? ` (track: ${req.trackId})` : ''}\n` +
51
- ` options: ${optionsStr}\n` +
52
- ` > `,
53
- );
54
-
55
- const input = (await readOneLine()).trim().toLowerCase();
56
-
57
- const approveAliases = new Set(['approve', 'yes', 'y', 'ok', 'true', '1']);
58
- const rejectAliases = new Set(['reject', 'no', 'n', 'deny', 'false', '0']);
59
- const matchedOption = req.options.find((o) => o.toLowerCase() === input);
60
-
61
- if (matchedOption) {
62
- const isReject = rejectAliases.has(matchedOption.toLowerCase());
63
- gateway.resolve(req.id, {
64
- outcome: isReject ? 'rejected' : 'approved',
65
- choice: matchedOption,
66
- actor: 'cli',
67
- });
68
- } else if (approveAliases.has(input)) {
69
- gateway.resolve(req.id, { outcome: 'approved', choice: input, actor: 'cli' });
70
- } else if (rejectAliases.has(input)) {
71
- gateway.resolve(req.id, {
72
- outcome: 'rejected',
73
- choice: input,
74
- actor: 'cli',
75
- reason: 'user rejected via CLI',
76
- });
77
- } else {
78
- process.stdout.write(` unrecognized input "${input}" — treating as rejection\n`);
79
- gateway.resolve(req.id, {
80
- outcome: 'rejected',
81
- actor: 'cli',
82
- reason: `unrecognized CLI input: ${input}`,
83
- });
84
- }
85
- }
86
- } finally {
87
- processing = false;
88
- }
89
- }
90
-
91
- const unsubscribe = gateway.subscribe((event) => {
92
- switch (event.type) {
93
- case 'requested':
94
- queue.push(event.request);
95
- void processNext();
96
- return;
97
- case 'resolved':
98
- case 'expired':
99
- case 'aborted': {
100
- // Drop from queue if it's still waiting its turn.
101
- const idx = queue.findIndex((r) => r.id === event.request.id);
102
- if (idx >= 0) queue.splice(idx, 1);
103
- return;
104
- }
105
- }
106
- });
107
-
108
- return {
109
- detach: () => {
110
- unsubscribe();
111
- if (rl) {
112
- rl.close();
113
- rl = null;
114
- }
115
- },
116
- };
117
- }
1
+ import * as readline from 'readline';
2
+ import type { ApprovalGateway, ApprovalRequest } from '../approval';
3
+
4
+ // ═══ CLI Stdin Adapter ═══
5
+ //
6
+ // Subscribes to the gateway's 'requested' events, prompts the user on stdout,
7
+ // reads a line from stdin, and calls gateway.resolve(). Handles at most one
8
+ // prompt at a time — additional requests queue up.
9
+
10
+ export interface StdinApprovalAdapter {
11
+ readonly detach: () => void;
12
+ }
13
+
14
+ export function attachStdinApprovalAdapter(gateway: ApprovalGateway): StdinApprovalAdapter {
15
+ const queue: ApprovalRequest[] = [];
16
+ let processing = false;
17
+ let rl: readline.Interface | null = null;
18
+
19
+ function ensureReadline(): readline.Interface {
20
+ if (!rl) {
21
+ rl = readline.createInterface({ input: process.stdin, terminal: false });
22
+ }
23
+ return rl;
24
+ }
25
+
26
+ function readOneLine(): Promise<string> {
27
+ return new Promise((resolvePromise) => {
28
+ const reader = ensureReadline();
29
+ const handler = (line: string): void => {
30
+ reader.off('line', handler);
31
+ resolvePromise(line);
32
+ };
33
+ reader.on('line', handler);
34
+ });
35
+ }
36
+
37
+ async function processNext(): Promise<void> {
38
+ if (processing) return;
39
+ processing = true;
40
+ try {
41
+ while (queue.length > 0) {
42
+ const req = queue.shift()!;
43
+ // If the request was already resolved by another path while queued, skip it.
44
+ if (!gateway.pending().some((p) => p.id === req.id)) continue;
45
+
46
+ const optionsStr = req.options.join(' / ');
47
+ process.stdout.write(
48
+ `\n[APPROVAL REQUIRED] ${req.message}\n` +
49
+ ` id: ${req.id}\n` +
50
+ ` task: ${req.taskId}${req.trackId ? ` (track: ${req.trackId})` : ''}\n` +
51
+ ` options: ${optionsStr}\n` +
52
+ ` > `,
53
+ );
54
+
55
+ const input = (await readOneLine()).trim().toLowerCase();
56
+
57
+ const approveAliases = new Set(['approve', 'yes', 'y', 'ok', 'true', '1']);
58
+ const rejectAliases = new Set(['reject', 'no', 'n', 'deny', 'false', '0']);
59
+ const matchedOption = req.options.find((o) => o.toLowerCase() === input);
60
+
61
+ if (matchedOption) {
62
+ const isReject = rejectAliases.has(matchedOption.toLowerCase());
63
+ gateway.resolve(req.id, {
64
+ outcome: isReject ? 'rejected' : 'approved',
65
+ choice: matchedOption,
66
+ actor: 'cli',
67
+ });
68
+ } else if (approveAliases.has(input)) {
69
+ gateway.resolve(req.id, { outcome: 'approved', choice: input, actor: 'cli' });
70
+ } else if (rejectAliases.has(input)) {
71
+ gateway.resolve(req.id, {
72
+ outcome: 'rejected',
73
+ choice: input,
74
+ actor: 'cli',
75
+ reason: 'user rejected via CLI',
76
+ });
77
+ } else {
78
+ process.stdout.write(` unrecognized input "${input}" — treating as rejection\n`);
79
+ gateway.resolve(req.id, {
80
+ outcome: 'rejected',
81
+ actor: 'cli',
82
+ reason: `unrecognized CLI input: ${input}`,
83
+ });
84
+ }
85
+ }
86
+ } finally {
87
+ processing = false;
88
+ }
89
+ }
90
+
91
+ const unsubscribe = gateway.subscribe((event) => {
92
+ switch (event.type) {
93
+ case 'requested':
94
+ queue.push(event.request);
95
+ void processNext();
96
+ return;
97
+ case 'resolved':
98
+ case 'expired':
99
+ case 'aborted': {
100
+ // Drop from queue if it's still waiting its turn.
101
+ const idx = queue.findIndex((r) => r.id === event.request.id);
102
+ if (idx >= 0) queue.splice(idx, 1);
103
+ return;
104
+ }
105
+ }
106
+ });
107
+
108
+ return {
109
+ detach: () => {
110
+ unsubscribe();
111
+ if (rl) {
112
+ rl.close();
113
+ rl = null;
114
+ }
115
+ },
116
+ };
117
+ }
@@ -1,144 +1,175 @@
1
- import type { ApprovalGateway, ApprovalEvent } from '../approval';
2
-
3
- // ═══ WebSocket Approval Adapter ═══
4
- //
5
- // Bridges the ApprovalGateway to WebSocket clients (e.g. a frontend UI).
6
- // Mirrors the stdin-approval adapter pattern: subscribe to gateway events,
7
- // forward them as JSON to all connected clients, and call gateway.resolve()
8
- // when a client sends a resolution message.
9
- //
10
- // Protocol — server → client:
11
- // { type: 'pending', requests: ApprovalRequest[] } ← sent on connect
12
- // { type: 'approval_requested', request: ApprovalRequest }
13
- // { type: 'approval_resolved', request: ApprovalRequest, decision: ApprovalDecision }
14
- // { type: 'approval_expired', request: ApprovalRequest }
15
- // { type: 'approval_aborted', request: ApprovalRequest, reason: string }
16
- //
17
- // Protocol — client → server:
18
- // { type: 'resolve', approvalId: string, outcome: 'approved'|'rejected',
19
- // choice?: string, actor?: string, reason?: string }
20
-
21
- export interface WebSocketApprovalAdapterOptions {
22
- port?: number; // default: 3000
23
- hostname?: string; // default: 'localhost'
24
- }
25
-
26
- export interface WebSocketApprovalAdapter {
27
- readonly port: number;
28
- readonly detach: () => void;
29
- }
30
-
31
- export function attachWebSocketApprovalAdapter(
32
- gateway: ApprovalGateway,
33
- options: WebSocketApprovalAdapterOptions = {},
34
- ): WebSocketApprovalAdapter {
35
- const port = options.port ?? 3000;
36
- const hostname = options.hostname ?? 'localhost';
37
-
38
- const clients = new Set<import('bun').ServerWebSocket<unknown>>();
39
-
40
- function broadcast(msg: unknown): void {
41
- const text = JSON.stringify(msg);
42
- for (const ws of clients) {
43
- ws.send(text);
44
- }
45
- }
46
-
47
- const unsubscribe = gateway.subscribe((event: ApprovalEvent) => {
48
- switch (event.type) {
49
- case 'requested':
50
- broadcast({ type: 'approval_requested', request: event.request });
51
- break;
52
- case 'resolved':
53
- broadcast({ type: 'approval_resolved', request: event.request, decision: event.decision });
54
- break;
55
- case 'expired':
56
- broadcast({ type: 'approval_expired', request: event.request });
57
- break;
58
- case 'aborted':
59
- broadcast({ type: 'approval_aborted', request: event.request, reason: event.reason });
60
- break;
61
- }
62
- });
63
-
64
- const server = Bun.serve({
65
- port,
66
- hostname,
67
-
68
- fetch(req, server) {
69
- if (server.upgrade(req)) return undefined;
70
- return new Response('tagma-sdk WebSocket approval endpoint', { status: 426 });
71
- },
72
-
73
- websocket: {
74
- open(ws) {
75
- clients.add(ws);
76
- // Sync current pending approvals to newly connected client.
77
- ws.send(JSON.stringify({ type: 'pending', requests: gateway.pending() }));
78
- },
79
-
80
- message(ws, raw) {
81
- let msg: unknown;
82
- try {
83
- msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
84
- } catch {
85
- ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
86
- return;
87
- }
88
-
89
- if (!isResolveMessage(msg)) {
90
- ws.send(JSON.stringify({ type: 'error', message: 'unknown message type' }));
91
- return;
92
- }
93
-
94
- const ok = gateway.resolve(msg.approvalId, {
95
- outcome: msg.outcome,
96
- choice: msg.choice,
97
- actor: msg.actor ?? 'websocket',
98
- reason: msg.reason,
99
- });
100
-
101
- if (!ok) {
102
- ws.send(JSON.stringify({
103
- type: 'error',
104
- message: `approval ${msg.approvalId} not found or already resolved`,
105
- }));
106
- }
107
- },
108
-
109
- close(ws) {
110
- clients.delete(ws);
111
- },
112
- },
113
- });
114
-
115
- return {
116
- port: server.port!,
117
- detach() {
118
- unsubscribe();
119
- clients.clear();
120
- server.stop(true);
121
- },
122
- };
123
- }
124
-
125
- // ── Type guard ──
126
-
127
- interface ResolveMessage {
128
- type: 'resolve';
129
- approvalId: string;
130
- outcome: 'approved' | 'rejected';
131
- choice?: string;
132
- actor?: string;
133
- reason?: string;
134
- }
135
-
136
- function isResolveMessage(v: unknown): v is ResolveMessage {
137
- if (typeof v !== 'object' || v === null) return false;
138
- const m = v as Record<string, unknown>;
139
- return (
140
- m['type'] === 'resolve' &&
141
- typeof m['approvalId'] === 'string' &&
142
- (m['outcome'] === 'approved' || m['outcome'] === 'rejected')
143
- );
144
- }
1
+ import type { ApprovalGateway, ApprovalEvent } from '../approval';
2
+
3
+ // ═══ WebSocket Approval Adapter ═══
4
+ //
5
+ // Bridges the ApprovalGateway to WebSocket clients (e.g. a frontend UI).
6
+ // Mirrors the stdin-approval adapter pattern: subscribe to gateway events,
7
+ // forward them as JSON to all connected clients, and call gateway.resolve()
8
+ // when a client sends a resolution message.
9
+ //
10
+ // Protocol — server → client:
11
+ // { type: 'pending', requests: ApprovalRequest[] } ← sent on connect
12
+ // { type: 'approval_requested', request: ApprovalRequest }
13
+ // { type: 'approval_resolved', request: ApprovalRequest, decision: ApprovalDecision }
14
+ // { type: 'approval_expired', request: ApprovalRequest }
15
+ // { type: 'approval_aborted', request: ApprovalRequest, reason: string }
16
+ //
17
+ // Protocol — client → server:
18
+ // { type: 'resolve', approvalId: string, outcome: 'approved'|'rejected',
19
+ // choice?: string, actor?: string, reason?: string }
20
+
21
+ export interface WebSocketApprovalAdapterOptions {
22
+ port?: number; // default: 3000
23
+ hostname?: string; // default: 'localhost'
24
+ }
25
+
26
+ export interface WebSocketApprovalAdapter {
27
+ readonly port: number;
28
+ readonly detach: () => void;
29
+ }
30
+
31
+ // Maximum allowed message payload (bytes) to prevent DoS via oversized messages.
32
+ const MAX_PAYLOAD_BYTES = 4_096;
33
+ // Per-client rate limit: at most this many messages per window.
34
+ const RATE_LIMIT_MAX = 10;
35
+ const RATE_LIMIT_WINDOW_MS = 1_000;
36
+
37
+ export function attachWebSocketApprovalAdapter(
38
+ gateway: ApprovalGateway,
39
+ options: WebSocketApprovalAdapterOptions = {},
40
+ ): WebSocketApprovalAdapter {
41
+ const port = options.port ?? 3000;
42
+ const hostname = options.hostname ?? 'localhost';
43
+
44
+ type WS = import('bun').ServerWebSocket<unknown>;
45
+ const clients = new Set<WS>();
46
+ const clientRates = new Map<WS, { count: number; resetAt: number }>();
47
+
48
+ function broadcast(msg: unknown): void {
49
+ const text = JSON.stringify(msg);
50
+ for (const ws of clients) {
51
+ ws.send(text);
52
+ }
53
+ }
54
+
55
+ const unsubscribe = gateway.subscribe((event: ApprovalEvent) => {
56
+ switch (event.type) {
57
+ case 'requested':
58
+ broadcast({ type: 'approval_requested', request: event.request });
59
+ break;
60
+ case 'resolved':
61
+ broadcast({ type: 'approval_resolved', request: event.request, decision: event.decision });
62
+ break;
63
+ case 'expired':
64
+ broadcast({ type: 'approval_expired', request: event.request });
65
+ break;
66
+ case 'aborted':
67
+ broadcast({ type: 'approval_aborted', request: event.request, reason: event.reason });
68
+ break;
69
+ }
70
+ });
71
+
72
+ const server = Bun.serve({
73
+ port,
74
+ hostname,
75
+
76
+ fetch(req, server) {
77
+ if (server.upgrade(req)) return undefined;
78
+ return new Response('tagma-sdk WebSocket approval endpoint', { status: 426 });
79
+ },
80
+
81
+ websocket: {
82
+ open(ws) {
83
+ clients.add(ws);
84
+ // Sync current pending approvals to newly connected client.
85
+ ws.send(JSON.stringify({ type: 'pending', requests: gateway.pending() }));
86
+ },
87
+
88
+ message(ws, raw) {
89
+ const rawStr = typeof raw === 'string' ? raw : raw.toString();
90
+
91
+ // Payload size guard — reject oversized messages before parsing.
92
+ if (rawStr.length > MAX_PAYLOAD_BYTES) {
93
+ ws.send(JSON.stringify({ type: 'error', message: 'message too large' }));
94
+ return;
95
+ }
96
+
97
+ // Per-client rate limit.
98
+ const now = Date.now();
99
+ const rate = clientRates.get(ws) ?? { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
100
+ if (now >= rate.resetAt) {
101
+ rate.count = 0;
102
+ rate.resetAt = now + RATE_LIMIT_WINDOW_MS;
103
+ }
104
+ rate.count++;
105
+ clientRates.set(ws, rate);
106
+ if (rate.count > RATE_LIMIT_MAX) {
107
+ ws.send(JSON.stringify({ type: 'error', message: 'rate limit exceeded' }));
108
+ return;
109
+ }
110
+
111
+ let msg: unknown;
112
+ try {
113
+ msg = JSON.parse(rawStr);
114
+ } catch {
115
+ ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
116
+ return;
117
+ }
118
+
119
+ if (!isResolveMessage(msg)) {
120
+ ws.send(JSON.stringify({ type: 'error', message: 'unknown message type' }));
121
+ return;
122
+ }
123
+
124
+ const ok = gateway.resolve(msg.approvalId, {
125
+ outcome: msg.outcome,
126
+ choice: msg.choice,
127
+ actor: msg.actor ?? 'websocket',
128
+ reason: msg.reason,
129
+ });
130
+
131
+ if (!ok) {
132
+ ws.send(JSON.stringify({
133
+ type: 'error',
134
+ message: `approval ${msg.approvalId} not found or already resolved`,
135
+ }));
136
+ }
137
+ },
138
+
139
+ close(ws) {
140
+ clients.delete(ws);
141
+ clientRates.delete(ws);
142
+ },
143
+ },
144
+ });
145
+
146
+ return {
147
+ port: server.port!,
148
+ detach() {
149
+ unsubscribe();
150
+ clients.clear();
151
+ server.stop(true);
152
+ },
153
+ };
154
+ }
155
+
156
+ // ── Type guard ──
157
+
158
+ interface ResolveMessage {
159
+ type: 'resolve';
160
+ approvalId: string;
161
+ outcome: 'approved' | 'rejected';
162
+ choice?: string;
163
+ actor?: string;
164
+ reason?: string;
165
+ }
166
+
167
+ function isResolveMessage(v: unknown): v is ResolveMessage {
168
+ if (typeof v !== 'object' || v === null) return false;
169
+ const m = v as Record<string, unknown>;
170
+ return (
171
+ m['type'] === 'resolve' &&
172
+ typeof m['approvalId'] === 'string' &&
173
+ (m['outcome'] === 'approved' || m['outcome'] === 'rejected')
174
+ );
175
+ }
package/src/approval.ts CHANGED
@@ -16,6 +16,9 @@ export type {
16
16
  ApprovalListener, ApprovalGateway,
17
17
  } from '@tagma/types';
18
18
 
19
+ // Default options presented to the approver when the caller does not specify any.
20
+ const DEFAULT_APPROVAL_OPTIONS = ['approve', 'reject'] as const;
21
+
19
22
  // ═══ Default In-Memory Implementation ═══
20
23
 
21
24
  interface PendingEntry {
@@ -37,7 +40,7 @@ export class InMemoryApprovalGateway implements ApprovalGateway {
37
40
  taskId: req.taskId,
38
41
  trackId: req.trackId,
39
42
  message: req.message,
40
- options: req.options && req.options.length > 0 ? req.options : ['approve', 'reject'],
43
+ options: req.options && req.options.length > 0 ? req.options : DEFAULT_APPROVAL_OPTIONS,
41
44
  timeoutMs: req.timeoutMs,
42
45
  metadata: req.metadata,
43
46
  };