@tagma/sdk 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/sdk",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/engine.ts CHANGED
@@ -19,6 +19,26 @@ import {
19
19
  import { Logger, tailLines, clip, type LogLevel } from './logger';
20
20
  import { InMemoryApprovalGateway, type ApprovalGateway } from './approval';
21
21
 
22
+ // ═══ A7: Typed trigger errors ═══
23
+ // Replace string-matching on error messages with structured error types so
24
+ // coincidental substrings don't cause misclassification.
25
+
26
+ export class TriggerBlockedError extends Error {
27
+ readonly code = 'TRIGGER_BLOCKED' as const;
28
+ constructor(message: string) {
29
+ super(message);
30
+ this.name = 'TriggerBlockedError';
31
+ }
32
+ }
33
+
34
+ export class TriggerTimeoutError extends Error {
35
+ readonly code = 'TRIGGER_TIMEOUT' as const;
36
+ constructor(message: string) {
37
+ super(message);
38
+ this.name = 'TriggerTimeoutError';
39
+ }
40
+ }
41
+
22
42
  // ═══ Preflight Validation ═══
23
43
 
24
44
  function preflight(config: PipelineConfig, dag: Dag): void {
@@ -420,18 +440,26 @@ export async function runPipeline(
420
440
  });
421
441
  log.debug(`[task:${taskId}]`, `trigger fired`);
422
442
  } catch (err: unknown) {
423
- const msg = err instanceof Error ? err.message : String(err);
424
443
  // If pipeline was aborted while we were still waiting for the trigger,
425
444
  // this task never entered running state → skipped, not timeout.
426
445
  state.finishedAt = nowISO();
427
446
  if (pipelineAborted) {
428
447
  setTaskStatus(taskId, 'skipped');
429
- } else if (msg.includes('rejected') || msg.includes('denied')) {
448
+ } else if (err instanceof TriggerBlockedError) {
430
449
  setTaskStatus(taskId, 'blocked'); // user/policy rejection
431
- } else if (msg.includes('timeout')) {
450
+ } else if (err instanceof TriggerTimeoutError) {
432
451
  setTaskStatus(taskId, 'timeout'); // genuine trigger wait timeout
433
452
  } else {
434
- setTaskStatus(taskId, 'failed'); // plugin error, watcher crash, etc.
453
+ // A7 fallback: also check message strings for backward-compat with
454
+ // third-party trigger plugins that don't throw typed errors yet.
455
+ const msg = err instanceof Error ? err.message : String(err);
456
+ if (msg.includes('rejected') || msg.includes('denied')) {
457
+ setTaskStatus(taskId, 'blocked');
458
+ } else if (msg.includes('timeout')) {
459
+ setTaskStatus(taskId, 'timeout');
460
+ } else {
461
+ setTaskStatus(taskId, 'failed'); // plugin error, watcher crash, etc.
462
+ }
435
463
  }
436
464
  try {
437
465
  await fireHook(taskId, 'task_failure');
package/src/sdk.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // The CLI (src/index.ts in the CLI project) also imports from here.
5
5
 
6
6
  // ── Core engine ──
7
- export { runPipeline } from './engine';
7
+ export { runPipeline, TriggerBlockedError, TriggerTimeoutError } from './engine';
8
8
  export type { EngineResult, RunPipelineOptions, PipelineEvent } from './engine';
9
9
 
10
10
  // ── Pipeline runner (multi-pipeline lifecycle management) ──
@@ -3,6 +3,7 @@ import { resolve, dirname } from 'path';
3
3
  import { mkdir } from 'fs/promises';
4
4
  import type { TriggerPlugin, TriggerContext } from '../types';
5
5
  import { parseDuration, validatePath } from '../utils';
6
+ import { TriggerTimeoutError } from '../engine';
6
7
 
7
8
  const IS_WINDOWS = process.platform === 'win32';
8
9
 
@@ -118,7 +119,7 @@ export const FileTrigger: TriggerPlugin = {
118
119
  timer = setTimeout(() => {
119
120
  if (settled) return;
120
121
  cleanup();
121
- reject(new Error(`file trigger timeout: ${filePath} did not appear within ${config.timeout}`));
122
+ reject(new TriggerTimeoutError(`file trigger timeout: ${filePath} did not appear within ${config.timeout}`));
122
123
  }, timeoutMs);
123
124
  }
124
125
 
@@ -1,5 +1,6 @@
1
1
  import type { TriggerPlugin, TriggerContext } from '../types';
2
2
  import { parseDuration } from '../utils';
3
+ import { TriggerBlockedError, TriggerTimeoutError } from '../engine';
3
4
 
4
5
  export const ManualTrigger: TriggerPlugin = {
5
6
  name: 'manual',
@@ -66,14 +67,15 @@ export const ManualTrigger: TriggerPlugin = {
66
67
  case 'approved':
67
68
  return { confirmed: true, approvalId: decision.approvalId, actor: decision.actor };
68
69
  case 'rejected':
69
- throw new Error(
70
+ // A7: Use typed error for proper classification in the engine.
71
+ throw new TriggerBlockedError(
70
72
  `Manual trigger rejected by ${decision.actor ?? 'user'}` +
71
73
  (decision.reason ? `: ${decision.reason}` : ''),
72
74
  );
73
75
  case 'timeout':
74
- throw new Error(`Manual trigger timeout: ${decision.reason ?? 'no decision made'}`);
76
+ throw new TriggerTimeoutError(`Manual trigger timeout: ${decision.reason ?? 'no decision made'}`);
75
77
  case 'aborted':
76
- throw new Error(`Manual trigger aborted: ${decision.reason ?? 'pipeline aborted'}`);
78
+ throw new TriggerBlockedError(`Manual trigger aborted: ${decision.reason ?? 'pipeline aborted'}`);
77
79
  }
78
80
  },
79
81
  };