@tagma/sdk 0.4.12 → 0.4.14

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 +569 -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/src/hooks.ts CHANGED
@@ -1,179 +1,187 @@
1
- import type { HooksConfig, HookCommand } from './types';
2
- import { shellArgs } from './utils';
3
-
4
- type HookEvent =
5
- | 'pipeline_start' | 'task_start' | 'task_success'
6
- | 'task_failure' | 'pipeline_complete' | 'pipeline_error';
7
-
8
- const GATE_HOOKS: ReadonlySet<HookEvent> = new Set(['pipeline_start', 'task_start']);
9
-
10
- export interface HookResult {
11
- readonly allowed: boolean; // for gate hooks: true = proceed, false = block
12
- readonly exitCode: number;
13
- }
14
-
15
- function normalizeCommands(cmd: HookCommand | undefined): readonly string[] {
16
- if (!cmd) return [];
17
- if (typeof cmd === 'string') return [cmd];
18
- return cmd;
19
- }
20
-
21
- const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
22
-
23
- async function runSingleHook(
24
- command: string,
25
- context: unknown,
26
- cwd?: string,
27
- signal?: AbortSignal,
28
- timeoutMs: number = DEFAULT_HOOK_TIMEOUT_MS,
29
- ): Promise<number> {
30
- const jsonInput = JSON.stringify(context, null, 2);
31
-
32
- const controller = new AbortController();
33
- const timer = timeoutMs > 0
34
- ? setTimeout(() => controller.abort(), timeoutMs)
35
- : null;
36
-
37
- // Wire pipeline abort signal into hook process
38
- const onAbort = () => controller.abort();
39
- if (signal) {
40
- if (signal.aborted) {
41
- controller.abort();
42
- } else {
43
- signal.addEventListener('abort', onAbort, { once: true });
44
- }
45
- }
46
-
47
- try {
48
- const proc = Bun.spawn(shellArgs(command) as string[], {
49
- stdin: 'pipe',
50
- stdout: 'pipe',
51
- stderr: 'pipe',
52
- signal: controller.signal,
53
- ...(cwd ? { cwd } : {}),
54
- });
55
-
56
- if (proc.stdin) {
57
- try {
58
- proc.stdin.write(jsonInput);
59
- proc.stdin.end();
60
- } catch {
61
- // Process may exit before reading stdin (e.g. `exit 1`), ignore EPIPE
62
- }
63
- }
64
-
65
- // Consume stdout and stderr concurrently with waiting for exit.
66
- // Sequential reads after proc.exited risk a pipe-buffer deadlock when
67
- // hook output exceeds the ~64 KB kernel buffer.
68
- const [exitCode, stdout, stderr] = await Promise.all([
69
- proc.exited,
70
- new Response(proc.stdout).text(),
71
- new Response(proc.stderr).text(),
72
- ]);
73
-
74
- if (stdout.trim()) {
75
- console.warn(`[hook: ${command}] stdout: ${stdout.trim()}`);
76
- }
77
- if (stderr.trim()) {
78
- console.error(`[hook: ${command}] stderr: ${stderr.trim()}`);
79
- }
80
-
81
- return exitCode;
82
- } catch (err) {
83
- console.error(`[hook: ${command}] spawn error: ${err instanceof Error ? err.message : String(err)}`);
84
- return -1;
85
- } finally {
86
- if (timer) clearTimeout(timer);
87
- if (signal) signal.removeEventListener('abort', onAbort);
88
- }
89
- }
90
-
91
- export async function executeHook(
92
- hooks: HooksConfig | undefined,
93
- event: HookEvent,
94
- context: unknown,
95
- workDir?: string,
96
- signal?: AbortSignal,
97
- ): Promise<HookResult> {
98
- if (!hooks) return { allowed: true, exitCode: 0 };
99
-
100
- const commands = normalizeCommands(hooks[event]);
101
- if (commands.length === 0) return { allowed: true, exitCode: 0 };
102
-
103
- const isGate = GATE_HOOKS.has(event);
104
-
105
- for (const cmd of commands) {
106
- const exitCode = await runSingleHook(cmd, context, workDir, signal);
107
-
108
- if (isGate && exitCode === 1) {
109
- // Only exit code 1 has gate semantics (block execution)
110
- return { allowed: false, exitCode };
111
- }
112
-
113
- if (exitCode !== 0) {
114
- // Non-zero but not 1: hook itself had an error, log but don't block
115
- console.warn(`[hook: ${event}] "${cmd}" exited with code ${exitCode}`);
116
- }
117
- }
118
-
119
- return { allowed: true, exitCode: 0 };
120
- }
121
-
122
- // ═══ Context Builders ═══
123
-
124
- export interface PipelineInfo {
125
- readonly name: string;
126
- readonly run_id: string;
127
- readonly started_at: string;
128
- readonly finished_at?: string;
129
- readonly duration_ms?: number;
130
- }
131
-
132
- export interface TrackInfo {
133
- readonly id: string;
134
- readonly name: string;
135
- }
136
-
137
- export interface TaskInfo {
138
- readonly id: string;
139
- readonly name: string;
140
- readonly type: 'ai' | 'command';
141
- readonly status: string;
142
- readonly exit_code: number | null;
143
- readonly duration_ms: number | null;
144
- readonly stderr_path: string | null;
145
- readonly session_id: string | null;
146
- readonly started_at: string | null;
147
- readonly finished_at: string | null;
148
- }
149
-
150
- export function buildPipelineStartContext(pipeline: PipelineInfo) {
151
- return { event: 'pipeline_start', pipeline };
152
- }
153
-
154
- export function buildTaskContext(
155
- event: 'task_start' | 'task_success' | 'task_failure',
156
- pipeline: PipelineInfo,
157
- track: TrackInfo,
158
- task: TaskInfo,
159
- ) {
160
- return { event, pipeline, track, task };
161
- }
162
-
163
- export function buildPipelineCompleteContext(
164
- pipeline: PipelineInfo & { finished_at: string; duration_ms: number },
165
- summary: {
166
- total: number; success: number; failed: number;
167
- skipped: number; timeout: number; blocked: number;
168
- },
169
- ) {
170
- return { event: 'pipeline_complete', pipeline, summary };
171
- }
172
-
173
- export function buildPipelineErrorContext(
174
- pipeline: PipelineInfo,
175
- error: string,
176
- eventType?: string,
177
- ) {
178
- return { event: eventType ?? 'pipeline_error', pipeline, error };
179
- }
1
+ import type { HooksConfig, HookCommand } from './types';
2
+ import { shellArgs } from './utils';
3
+
4
+ type HookEvent =
5
+ | 'pipeline_start'
6
+ | 'task_start'
7
+ | 'task_success'
8
+ | 'task_failure'
9
+ | 'pipeline_complete'
10
+ | 'pipeline_error';
11
+
12
+ const GATE_HOOKS: ReadonlySet<HookEvent> = new Set(['pipeline_start', 'task_start']);
13
+
14
+ export interface HookResult {
15
+ readonly allowed: boolean; // for gate hooks: true = proceed, false = block
16
+ readonly exitCode: number;
17
+ }
18
+
19
+ function normalizeCommands(cmd: HookCommand | undefined): readonly string[] {
20
+ if (!cmd) return [];
21
+ if (typeof cmd === 'string') return [cmd];
22
+ return cmd;
23
+ }
24
+
25
+ const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
26
+
27
+ async function runSingleHook(
28
+ command: string,
29
+ context: unknown,
30
+ cwd?: string,
31
+ signal?: AbortSignal,
32
+ timeoutMs: number = DEFAULT_HOOK_TIMEOUT_MS,
33
+ ): Promise<number> {
34
+ const jsonInput = JSON.stringify(context, null, 2);
35
+
36
+ const controller = new AbortController();
37
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
38
+
39
+ // Wire pipeline abort signal into hook process
40
+ const onAbort = () => controller.abort();
41
+ if (signal) {
42
+ if (signal.aborted) {
43
+ controller.abort();
44
+ } else {
45
+ signal.addEventListener('abort', onAbort, { once: true });
46
+ }
47
+ }
48
+
49
+ try {
50
+ const proc = Bun.spawn(shellArgs(command) as string[], {
51
+ stdin: 'pipe',
52
+ stdout: 'pipe',
53
+ stderr: 'pipe',
54
+ signal: controller.signal,
55
+ ...(cwd ? { cwd } : {}),
56
+ });
57
+
58
+ if (proc.stdin) {
59
+ try {
60
+ proc.stdin.write(jsonInput);
61
+ proc.stdin.end();
62
+ } catch {
63
+ // Process may exit before reading stdin (e.g. `exit 1`), ignore EPIPE
64
+ }
65
+ }
66
+
67
+ // Consume stdout and stderr concurrently with waiting for exit.
68
+ // Sequential reads after proc.exited risk a pipe-buffer deadlock when
69
+ // hook output exceeds the ~64 KB kernel buffer.
70
+ const [exitCode, stdout, stderr] = await Promise.all([
71
+ proc.exited,
72
+ new Response(proc.stdout).text(),
73
+ new Response(proc.stderr).text(),
74
+ ]);
75
+
76
+ if (stdout.trim()) {
77
+ console.warn(`[hook: ${command}] stdout: ${stdout.trim()}`);
78
+ }
79
+ if (stderr.trim()) {
80
+ console.error(`[hook: ${command}] stderr: ${stderr.trim()}`);
81
+ }
82
+
83
+ return exitCode;
84
+ } catch (err) {
85
+ console.error(
86
+ `[hook: ${command}] spawn error: ${err instanceof Error ? err.message : String(err)}`,
87
+ );
88
+ return -1;
89
+ } finally {
90
+ if (timer) clearTimeout(timer);
91
+ if (signal) signal.removeEventListener('abort', onAbort);
92
+ }
93
+ }
94
+
95
+ export async function executeHook(
96
+ hooks: HooksConfig | undefined,
97
+ event: HookEvent,
98
+ context: unknown,
99
+ workDir?: string,
100
+ signal?: AbortSignal,
101
+ ): Promise<HookResult> {
102
+ if (!hooks) return { allowed: true, exitCode: 0 };
103
+
104
+ const commands = normalizeCommands(hooks[event]);
105
+ if (commands.length === 0) return { allowed: true, exitCode: 0 };
106
+
107
+ const isGate = GATE_HOOKS.has(event);
108
+
109
+ for (const cmd of commands) {
110
+ const exitCode = await runSingleHook(cmd, context, workDir, signal);
111
+
112
+ if (isGate && exitCode === 1) {
113
+ // Only exit code 1 has gate semantics (block execution)
114
+ return { allowed: false, exitCode };
115
+ }
116
+
117
+ if (exitCode !== 0) {
118
+ // Non-zero but not 1: hook itself had an error, log but don't block
119
+ console.warn(`[hook: ${event}] "${cmd}" exited with code ${exitCode}`);
120
+ }
121
+ }
122
+
123
+ return { allowed: true, exitCode: 0 };
124
+ }
125
+
126
+ // ═══ Context Builders ═══
127
+
128
+ export interface PipelineInfo {
129
+ readonly name: string;
130
+ readonly run_id: string;
131
+ readonly started_at: string;
132
+ readonly finished_at?: string;
133
+ readonly duration_ms?: number;
134
+ }
135
+
136
+ export interface TrackInfo {
137
+ readonly id: string;
138
+ readonly name: string;
139
+ }
140
+
141
+ export interface TaskInfo {
142
+ readonly id: string;
143
+ readonly name: string;
144
+ readonly type: 'ai' | 'command';
145
+ readonly status: string;
146
+ readonly exit_code: number | null;
147
+ readonly duration_ms: number | null;
148
+ readonly stderr_path: string | null;
149
+ readonly session_id: string | null;
150
+ readonly started_at: string | null;
151
+ readonly finished_at: string | null;
152
+ }
153
+
154
+ export function buildPipelineStartContext(pipeline: PipelineInfo) {
155
+ return { event: 'pipeline_start', pipeline };
156
+ }
157
+
158
+ export function buildTaskContext(
159
+ event: 'task_start' | 'task_success' | 'task_failure',
160
+ pipeline: PipelineInfo,
161
+ track: TrackInfo,
162
+ task: TaskInfo,
163
+ ) {
164
+ return { event, pipeline, track, task };
165
+ }
166
+
167
+ export function buildPipelineCompleteContext(
168
+ pipeline: PipelineInfo & { finished_at: string; duration_ms: number },
169
+ summary: {
170
+ total: number;
171
+ success: number;
172
+ failed: number;
173
+ skipped: number;
174
+ timeout: number;
175
+ blocked: number;
176
+ },
177
+ ) {
178
+ return { event: 'pipeline_complete', pipeline, summary };
179
+ }
180
+
181
+ export function buildPipelineErrorContext(
182
+ pipeline: PipelineInfo,
183
+ error: string,
184
+ eventType?: string,
185
+ ) {
186
+ return { event: eventType ?? 'pipeline_error', pipeline, error };
187
+ }