@tagma/sdk 0.6.11 → 0.7.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.
Files changed (132) hide show
  1. package/README.md +91 -18
  2. package/dist/bootstrap.d.ts +6 -6
  3. package/dist/bootstrap.d.ts.map +1 -1
  4. package/dist/bootstrap.js +5 -6
  5. package/dist/bootstrap.js.map +1 -1
  6. package/dist/config-ops.d.ts +4 -2
  7. package/dist/config-ops.d.ts.map +1 -1
  8. package/dist/config-ops.js +16 -2
  9. package/dist/config-ops.js.map +1 -1
  10. package/dist/config.d.ts +8 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +5 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/core/dataflow.d.ts +23 -0
  15. package/dist/core/dataflow.d.ts.map +1 -0
  16. package/dist/core/dataflow.js +63 -0
  17. package/dist/core/dataflow.js.map +1 -0
  18. package/dist/core/log-prune.d.ts +16 -0
  19. package/dist/core/log-prune.d.ts.map +1 -0
  20. package/dist/core/log-prune.js +34 -0
  21. package/dist/core/log-prune.js.map +1 -0
  22. package/dist/core/preflight.d.ts +13 -0
  23. package/dist/core/preflight.d.ts.map +1 -0
  24. package/dist/core/preflight.js +61 -0
  25. package/dist/core/preflight.js.map +1 -0
  26. package/dist/core/run-context.d.ts +52 -0
  27. package/dist/core/run-context.d.ts.map +1 -0
  28. package/dist/core/run-context.js +156 -0
  29. package/dist/core/run-context.js.map +1 -0
  30. package/dist/core/run-state.d.ts +25 -0
  31. package/dist/core/run-state.d.ts.map +1 -0
  32. package/dist/core/run-state.js +93 -0
  33. package/dist/core/run-state.js.map +1 -0
  34. package/dist/core/scheduler.d.ts +13 -0
  35. package/dist/core/scheduler.d.ts.map +1 -0
  36. package/dist/core/scheduler.js +35 -0
  37. package/dist/core/scheduler.js.map +1 -0
  38. package/dist/core/task-executor.d.ts +13 -0
  39. package/dist/core/task-executor.d.ts.map +1 -0
  40. package/dist/core/task-executor.js +639 -0
  41. package/dist/core/task-executor.js.map +1 -0
  42. package/dist/core/trigger-errors.d.ts +9 -0
  43. package/dist/core/trigger-errors.d.ts.map +1 -0
  44. package/dist/core/trigger-errors.js +15 -0
  45. package/dist/core/trigger-errors.js.map +1 -0
  46. package/dist/engine.d.ts +6 -14
  47. package/dist/engine.d.ts.map +1 -1
  48. package/dist/engine.js +71 -990
  49. package/dist/engine.js.map +1 -1
  50. package/dist/index.d.ts +9 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +6 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/pipeline-definition.d.ts +3 -0
  55. package/dist/pipeline-definition.d.ts.map +1 -0
  56. package/dist/pipeline-definition.js +4 -0
  57. package/dist/pipeline-definition.js.map +1 -0
  58. package/dist/pipeline-runner.d.ts +2 -1
  59. package/dist/pipeline-runner.d.ts.map +1 -1
  60. package/dist/pipeline-runner.js +2 -2
  61. package/dist/pipeline-runner.js.map +1 -1
  62. package/dist/plugins.d.ts +5 -0
  63. package/dist/plugins.d.ts.map +1 -0
  64. package/dist/plugins.js +3 -0
  65. package/dist/plugins.js.map +1 -0
  66. package/dist/ports.d.ts +23 -1
  67. package/dist/ports.d.ts.map +1 -1
  68. package/dist/ports.js +160 -0
  69. package/dist/ports.js.map +1 -1
  70. package/dist/registry.d.ts +3 -19
  71. package/dist/registry.d.ts.map +1 -1
  72. package/dist/registry.js +7 -35
  73. package/dist/registry.js.map +1 -1
  74. package/dist/schema.d.ts.map +1 -1
  75. package/dist/schema.js +7 -3
  76. package/dist/schema.js.map +1 -1
  77. package/dist/tagma.d.ts +24 -0
  78. package/dist/tagma.d.ts.map +1 -0
  79. package/dist/tagma.js +23 -0
  80. package/dist/tagma.js.map +1 -0
  81. package/dist/utils-api.d.ts +2 -0
  82. package/dist/utils-api.d.ts.map +1 -0
  83. package/dist/utils-api.js +2 -0
  84. package/dist/utils-api.js.map +1 -0
  85. package/dist/validate-raw.js +118 -0
  86. package/dist/validate-raw.js.map +1 -1
  87. package/dist/yaml.d.ts +4 -0
  88. package/dist/yaml.d.ts.map +1 -0
  89. package/dist/yaml.js +3 -0
  90. package/dist/yaml.js.map +1 -0
  91. package/package.json +53 -8
  92. package/src/bootstrap.ts +6 -6
  93. package/src/config-ops.ts +12 -2
  94. package/src/config.ts +26 -0
  95. package/src/core/dataflow.test.ts +167 -0
  96. package/src/core/dataflow.ts +118 -0
  97. package/src/core/log-prune.test.ts +58 -0
  98. package/src/core/log-prune.ts +43 -0
  99. package/src/core/preflight.test.ts +49 -0
  100. package/src/core/preflight.ts +89 -0
  101. package/src/core/run-context.test.ts +244 -0
  102. package/src/core/run-context.ts +207 -0
  103. package/src/core/run-state.test.ts +98 -0
  104. package/src/core/run-state.ts +122 -0
  105. package/src/core/scheduler.test.ts +83 -0
  106. package/src/core/scheduler.ts +42 -0
  107. package/src/core/task-executor.ts +803 -0
  108. package/src/core/trigger-errors.ts +15 -0
  109. package/src/engine-ports.test.ts +66 -0
  110. package/src/engine-task-type.test.ts +56 -0
  111. package/src/engine.ts +86 -1180
  112. package/src/index.ts +28 -0
  113. package/src/pipeline-definition.ts +5 -0
  114. package/src/pipeline-runner.ts +3 -2
  115. package/src/plugin-registry.test.ts +7 -10
  116. package/src/plugins.ts +18 -0
  117. package/src/ports.test.ts +127 -0
  118. package/src/ports.ts +224 -1
  119. package/src/registry.ts +7 -49
  120. package/src/schema-ports.test.ts +86 -0
  121. package/src/schema.ts +7 -3
  122. package/src/tagma.test.ts +84 -0
  123. package/src/tagma.ts +47 -0
  124. package/src/utils-api.ts +8 -0
  125. package/src/validate-raw-ports.test.ts +66 -0
  126. package/src/validate-raw.ts +137 -0
  127. package/src/yaml.ts +11 -0
  128. package/dist/sdk.d.ts +0 -32
  129. package/dist/sdk.d.ts.map +0 -1
  130. package/dist/sdk.js +0 -41
  131. package/dist/sdk.js.map +0 -1
  132. package/src/sdk.ts +0 -147
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ export { createTagma } from './tagma';
2
+ export type { CreateTagmaOptions, Tagma, TagmaRunOptions } from './tagma';
3
+ export { definePipeline } from './pipeline-definition';
4
+ export { PluginRegistry } from './registry';
5
+ export { TriggerBlockedError, TriggerTimeoutError } from './engine';
6
+ export type { EngineResult, RunEventPayload } from './engine';
7
+ export { RUN_PROTOCOL_VERSION, TASK_LOG_CAP } from './types';
8
+ export type {
9
+ PipelineConfig,
10
+ RawPipelineConfig,
11
+ RawTrackConfig,
12
+ RawTaskConfig,
13
+ TrackConfig,
14
+ TaskConfig,
15
+ RunSnapshotPayload,
16
+ WireRunEvent,
17
+ RunTaskState,
18
+ TaskLogLine,
19
+ ApprovalRequestInfo,
20
+ TaskStatus,
21
+ ApprovalRequest,
22
+ PluginCategory,
23
+ DriverPlugin,
24
+ TriggerPlugin,
25
+ CompletionPlugin,
26
+ MiddlewarePlugin,
27
+ RunEventPayload as PipelineRunEventPayload,
28
+ } from '@tagma/types';
@@ -0,0 +1,5 @@
1
+ import type { RawPipelineConfig } from './types';
2
+
3
+ export function definePipeline<T extends RawPipelineConfig>(pipeline: T): T {
4
+ return pipeline;
5
+ }
@@ -13,7 +13,7 @@
13
13
  //
14
14
  // const runners = new Map<string, PipelineRunner>();
15
15
  //
16
- // const runner = new PipelineRunner(config, workDir);
16
+ // const runner = new PipelineRunner(config, workDir, { registry });
17
17
  // runner.subscribe(event => ipcEmit('run_event', event));
18
18
  // runner.start();
19
19
  // runners.set(runner.instanceId, runner);
@@ -29,6 +29,7 @@ import { generateRunId } from './utils';
29
29
  export type { EngineResult };
30
30
 
31
31
  export type PipelineRunnerStatus = 'idle' | 'running' | 'done' | 'aborted';
32
+ export type PipelineRunnerOptions = Omit<RunPipelineOptions, 'signal' | 'onEvent'>;
32
33
 
33
34
  export class PipelineRunner {
34
35
  /**
@@ -57,7 +58,7 @@ export class PipelineRunner {
57
58
  constructor(
58
59
  private readonly config: PipelineConfig,
59
60
  private readonly workDir: string,
60
- private readonly opts: Omit<RunPipelineOptions, 'signal' | 'onEvent'> = {},
61
+ private readonly opts: PipelineRunnerOptions,
61
62
  ) {
62
63
  this.instanceId = generateRunId();
63
64
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { PluginRegistry, defaultRegistry } from './registry';
2
+ import { PluginRegistry } from './registry';
3
3
  import { bootstrapBuiltins } from './bootstrap';
4
4
  import { runPipeline } from './engine';
5
5
  import type { DriverPlugin, TriggerPlugin, PipelineConfig } from './types';
@@ -80,7 +80,7 @@ describe('PluginRegistry — instance isolation', () => {
80
80
  expect(reg.getHandler<DriverPlugin>('drivers', 'mock').name).toBe('two');
81
81
  });
82
82
 
83
- test('bootstrapBuiltins(target) populates a specific instance without touching the default', () => {
83
+ test('bootstrapBuiltins(target) populates a specific instance', () => {
84
84
  const fresh = new PluginRegistry();
85
85
  expect(fresh.hasHandler('drivers', 'opencode')).toBe(false);
86
86
 
@@ -222,13 +222,9 @@ describe('runPipeline — options.registry isolation', () => {
222
222
  }
223
223
  });
224
224
 
225
- test('omitting options.registry falls back to defaultRegistry', async () => {
226
- // bootstrapBuiltins into default happens in most host callers; do it
227
- // explicitly here so the test is independent of module-load order.
228
- bootstrapBuiltins(defaultRegistry);
229
-
225
+ test('runPipeline rejects missing explicit registry', async () => {
230
226
  const config: PipelineConfig = {
231
- name: 'default-fallback',
227
+ name: 'missing-registry',
232
228
  tracks: [
233
229
  {
234
230
  id: 't',
@@ -239,8 +235,9 @@ describe('runPipeline — options.registry isolation', () => {
239
235
  };
240
236
  const tmp = mkdtempSync(join(tmpdir(), 'tagma-default-'));
241
237
  try {
242
- const res = await runPipeline(config, tmp, { skipPluginLoading: true });
243
- expect(res.success).toBe(true);
238
+ await expect(
239
+ runPipeline(config, tmp, { skipPluginLoading: true } as never),
240
+ ).rejects.toThrow(/requires options\.registry/);
244
241
  } finally {
245
242
  rmSync(tmp, { recursive: true, force: true });
246
243
  }
package/src/plugins.ts ADDED
@@ -0,0 +1,18 @@
1
+ export { bootstrapBuiltins } from './bootstrap';
2
+ export {
3
+ PluginRegistry,
4
+ isValidPluginName,
5
+ PLUGIN_NAME_RE,
6
+ readPluginManifest,
7
+ } from './registry';
8
+ export type { RegisterResult } from './registry';
9
+ export type {
10
+ PluginCategory,
11
+ PluginModule,
12
+ PluginManifest,
13
+ DriverPlugin,
14
+ TriggerPlugin,
15
+ CompletionPlugin,
16
+ MiddlewarePlugin,
17
+ } from './types';
18
+
package/src/ports.test.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import {
3
3
  extractInputReferences,
4
+ extractTaskBindingOutputs,
4
5
  extractTaskOutputs,
5
6
  inferPromptPorts,
7
+ resolveTaskBindingInputs,
6
8
  resolveTaskInputs,
7
9
  substituteInputs,
8
10
  } from './ports';
@@ -244,6 +246,87 @@ describe('resolveTaskInputs', () => {
244
246
  });
245
247
  });
246
248
 
249
+ // ─── resolveTaskBindingInputs ────────────────────────────────────────
250
+
251
+ describe('resolveTaskBindingInputs', () => {
252
+ test('resolves literal values and defaults without requiring ports', () => {
253
+ const t = task({
254
+ id: 'downstream',
255
+ command: 'echo',
256
+ inputs: {
257
+ city: { value: 'Shanghai' },
258
+ mode: { from: 't.up.outputs.missing', default: 'quick' },
259
+ },
260
+ });
261
+ const res = resolveTaskBindingInputs(t, new Map(), ['t.up']);
262
+ expect(res).toEqual({
263
+ kind: 'ready',
264
+ inputs: { city: 'Shanghai', mode: 'quick' },
265
+ missingOptional: [],
266
+ });
267
+ });
268
+
269
+ test('resolves values from a direct upstream output and stdout', () => {
270
+ const t = task({
271
+ id: 'downstream',
272
+ command: 'echo',
273
+ inputs: {
274
+ city: { from: 't.up.outputs.city' },
275
+ raw: { from: 't.up.stdout' },
276
+ },
277
+ });
278
+ const upstream = new Map([
279
+ [
280
+ 't.up',
281
+ {
282
+ outputs: { city: 'Shanghai' },
283
+ stdout: 'raw text\n',
284
+ stderr: '',
285
+ normalizedOutput: null,
286
+ exitCode: 0,
287
+ },
288
+ ],
289
+ ]);
290
+ const res = resolveTaskBindingInputs(t, upstream, ['t.up']);
291
+ expect(res.kind).toBe('ready');
292
+ if (res.kind !== 'ready') return;
293
+ expect(res.inputs).toEqual({ city: 'Shanghai', raw: 'raw text\n' });
294
+ });
295
+
296
+ test('blocks required missing bindings with a readable reason', () => {
297
+ const t = task({
298
+ id: 'downstream',
299
+ command: 'echo',
300
+ inputs: {
301
+ city: { from: 't.up.outputs.city', required: true },
302
+ },
303
+ });
304
+ const res = resolveTaskBindingInputs(t, new Map(), ['t.up']);
305
+ expect(res.kind).toBe('blocked');
306
+ if (res.kind !== 'blocked') return;
307
+ expect(res.missingRequired).toEqual(['city']);
308
+ expect(res.reason).toContain('missing required binding input(s): city');
309
+ });
310
+
311
+ test('detects ambiguous loose output name matches', () => {
312
+ const t = task({
313
+ id: 'downstream',
314
+ command: 'echo',
315
+ inputs: {
316
+ val: { from: 'outputs.val', required: true },
317
+ },
318
+ });
319
+ const upstream = new Map([
320
+ ['t.a', { outputs: { val: 'a' }, stdout: '', stderr: '', normalizedOutput: null, exitCode: 0 }],
321
+ ['t.b', { outputs: { val: 'b' }, stdout: '', stderr: '', normalizedOutput: null, exitCode: 0 }],
322
+ ]);
323
+ const res = resolveTaskBindingInputs(t, upstream, ['t.a', 't.b']);
324
+ expect(res.kind).toBe('blocked');
325
+ if (res.kind !== 'blocked') return;
326
+ expect(res.ambiguous[0]).toEqual({ input: 'val', producers: ['t.a', 't.b'] });
327
+ });
328
+ });
329
+
247
330
  // ─── extractTaskOutputs ──────────────────────────────────────────────
248
331
 
249
332
  describe('extractTaskOutputs', () => {
@@ -301,6 +384,50 @@ describe('extractTaskOutputs', () => {
301
384
  });
302
385
  });
303
386
 
387
+ // ─── extractTaskBindingOutputs ───────────────────────────────────────
388
+
389
+ describe('extractTaskBindingOutputs', () => {
390
+ test('extracts loose outputs from final-line JSON by default', () => {
391
+ const r = extractTaskBindingOutputs(
392
+ {
393
+ city: {},
394
+ temp: { from: 'json.temperature' },
395
+ },
396
+ 'log\n{"city":"Shanghai","temperature":23}\n',
397
+ '',
398
+ null,
399
+ );
400
+ expect(r.outputs).toEqual({ city: 'Shanghai', temp: 23 });
401
+ expect(r.diagnostic).toBeNull();
402
+ });
403
+
404
+ test('can publish whole stdout and normalizedOutput as named outputs', () => {
405
+ const r = extractTaskBindingOutputs(
406
+ {
407
+ raw: { from: 'stdout' },
408
+ normalized: { from: 'normalizedOutput' },
409
+ },
410
+ 'raw text\n',
411
+ '',
412
+ 'normalized text',
413
+ );
414
+ expect(r.outputs).toEqual({ raw: 'raw text\n', normalized: 'normalized text' });
415
+ });
416
+
417
+ test('uses defaults for missing loose outputs without failing extraction', () => {
418
+ const r = extractTaskBindingOutputs(
419
+ {
420
+ city: { default: 'Unknown' },
421
+ },
422
+ 'not json\n',
423
+ '',
424
+ null,
425
+ );
426
+ expect(r.outputs).toEqual({ city: 'Unknown' });
427
+ expect(r.diagnostic).toBeNull();
428
+ });
429
+ });
430
+
304
431
  // ─── inferPromptPorts ───────────────────────────────────────────────
305
432
 
306
433
  describe('inferPromptPorts', () => {
package/src/ports.ts CHANGED
@@ -34,7 +34,13 @@
34
34
  // Everything here is pure / deterministic so it can be reused by the CLI,
35
35
  // the editor (for preview/simulation), and the engine without side effects.
36
36
 
37
- import type { PortDef, PortType, TaskConfig, TaskPorts } from './types';
37
+ import type {
38
+ PortDef,
39
+ PortType,
40
+ TaskConfig,
41
+ TaskOutputBindings,
42
+ TaskPorts,
43
+ } from './types';
38
44
 
39
45
  // ─── Template substitution ────────────────────────────────────────────
40
46
 
@@ -244,6 +250,157 @@ export function resolveTaskInputs(
244
250
  return { kind: 'ready', inputs, missingOptional };
245
251
  }
246
252
 
253
+ // ─── Lightweight binding resolution ──────────────────────────────────
254
+
255
+ export interface UpstreamBindingData {
256
+ readonly outputs?: Readonly<Record<string, unknown>> | null;
257
+ readonly stdout?: string;
258
+ readonly stderr?: string;
259
+ readonly normalizedOutput?: string | null;
260
+ readonly exitCode?: number | null;
261
+ }
262
+
263
+ export type BindingInputResolution =
264
+ | {
265
+ readonly kind: 'ready';
266
+ readonly inputs: Readonly<Record<string, unknown>>;
267
+ readonly missingOptional: readonly string[];
268
+ }
269
+ | {
270
+ readonly kind: 'blocked';
271
+ readonly missingRequired: readonly string[];
272
+ readonly ambiguous: readonly { input: string; producers: readonly string[] }[];
273
+ readonly reason: string;
274
+ };
275
+
276
+ export function resolveTaskBindingInputs(
277
+ task: Pick<TaskConfig, 'inputs'>,
278
+ upstreamData: ReadonlyMap<string, UpstreamBindingData>,
279
+ dependsOn: readonly string[],
280
+ ): BindingInputResolution {
281
+ const bindings = task.inputs;
282
+ if (!bindings || Object.keys(bindings).length === 0) {
283
+ return { kind: 'ready', inputs: {}, missingOptional: [] };
284
+ }
285
+
286
+ const inputs: Record<string, unknown> = {};
287
+ const missingRequired: string[] = [];
288
+ const missingOptional: string[] = [];
289
+ const ambiguous: { input: string; producers: string[] }[] = [];
290
+
291
+ for (const [name, binding] of Object.entries(bindings)) {
292
+ let value: unknown;
293
+ let present = false;
294
+
295
+ if ('value' in binding) {
296
+ value = binding.value;
297
+ present = true;
298
+ } else if (binding.from) {
299
+ const found = resolveBindingSource(binding.from, upstreamData, dependsOn);
300
+ if (found.kind === 'ambiguous') {
301
+ ambiguous.push({ input: name, producers: found.producers });
302
+ continue;
303
+ }
304
+ if (found.kind === 'hit') {
305
+ value = found.value;
306
+ present = true;
307
+ }
308
+ }
309
+
310
+ if (!present && 'default' in binding) {
311
+ value = binding.default;
312
+ present = true;
313
+ }
314
+
315
+ if (!present || value === undefined || value === null) {
316
+ if (binding.required === true) {
317
+ missingRequired.push(name);
318
+ } else {
319
+ missingOptional.push(name);
320
+ }
321
+ continue;
322
+ }
323
+
324
+ inputs[name] = value;
325
+ }
326
+
327
+ if (missingRequired.length > 0 || ambiguous.length > 0) {
328
+ const lines: string[] = [];
329
+ if (missingRequired.length > 0) {
330
+ lines.push(`missing required binding input(s): ${missingRequired.join(', ')}`);
331
+ }
332
+ for (const amb of ambiguous) {
333
+ lines.push(
334
+ `binding input "${amb.input}" is produced by multiple upstreams ` +
335
+ `(${amb.producers.join(', ')}) — use "taskId.outputs.${amb.input}"`,
336
+ );
337
+ }
338
+ return { kind: 'blocked', missingRequired, ambiguous, reason: lines.join('\n') };
339
+ }
340
+
341
+ return { kind: 'ready', inputs, missingOptional };
342
+ }
343
+
344
+ type BindingLookup =
345
+ | { kind: 'hit'; producer: string; value: unknown }
346
+ | { kind: 'miss' }
347
+ | { kind: 'ambiguous'; producers: string[] };
348
+
349
+ function resolveBindingSource(
350
+ source: string,
351
+ upstreamData: ReadonlyMap<string, UpstreamBindingData>,
352
+ dependsOn: readonly string[],
353
+ ): BindingLookup {
354
+ if (source.startsWith('outputs.')) {
355
+ return findOutputByName(source.slice('outputs.'.length), upstreamData, dependsOn);
356
+ }
357
+
358
+ const outputMarker = '.outputs.';
359
+ const outputIdx = source.lastIndexOf(outputMarker);
360
+ if (outputIdx > 0) {
361
+ const upstreamId = source.slice(0, outputIdx);
362
+ const outputName = source.slice(outputIdx + outputMarker.length);
363
+ if (!dependsOn.includes(upstreamId)) return { kind: 'miss' };
364
+ const upstream = upstreamData.get(upstreamId);
365
+ if (upstream?.outputs && outputName in upstream.outputs) {
366
+ return { kind: 'hit', producer: upstreamId, value: upstream.outputs[outputName] };
367
+ }
368
+ return { kind: 'miss' };
369
+ }
370
+
371
+ for (const field of ['stdout', 'stderr', 'normalizedOutput', 'exitCode'] as const) {
372
+ const suffix = `.${field}`;
373
+ if (!source.endsWith(suffix)) continue;
374
+ const upstreamId = source.slice(0, -suffix.length);
375
+ if (!dependsOn.includes(upstreamId)) return { kind: 'miss' };
376
+ const upstream = upstreamData.get(upstreamId);
377
+ if (!upstream) return { kind: 'miss' };
378
+ const value = upstream[field];
379
+ return value === undefined || value === null
380
+ ? { kind: 'miss' }
381
+ : { kind: 'hit', producer: upstreamId, value };
382
+ }
383
+
384
+ return { kind: 'miss' };
385
+ }
386
+
387
+ function findOutputByName(
388
+ name: string,
389
+ upstreamData: ReadonlyMap<string, UpstreamBindingData>,
390
+ dependsOn: readonly string[],
391
+ ): BindingLookup {
392
+ const hits: { producer: string; value: unknown }[] = [];
393
+ for (const upstreamId of dependsOn) {
394
+ const upstream = upstreamData.get(upstreamId);
395
+ if (upstream?.outputs && name in upstream.outputs) {
396
+ hits.push({ producer: upstreamId, value: upstream.outputs[name] });
397
+ }
398
+ }
399
+ if (hits.length === 0) return { kind: 'miss' };
400
+ if (hits.length === 1) return { kind: 'hit', producer: hits[0]!.producer, value: hits[0]!.value };
401
+ return { kind: 'ambiguous', producers: hits.map((h) => h.producer) };
402
+ }
403
+
247
404
  type UpstreamLookup =
248
405
  | { kind: 'hit'; producer: string; value: unknown }
249
406
  | { kind: 'miss' }
@@ -416,6 +573,72 @@ export function extractTaskOutputs(
416
573
  return { outputs, diagnostic };
417
574
  }
418
575
 
576
+ export function extractTaskBindingOutputs(
577
+ bindings: TaskOutputBindings | undefined,
578
+ stdout: string,
579
+ stderr: string,
580
+ normalizedOutput: string | null,
581
+ ): ExtractResult {
582
+ if (!bindings || Object.keys(bindings).length === 0) {
583
+ return { outputs: {}, diagnostic: null };
584
+ }
585
+
586
+ const outputs: Record<string, unknown> = {};
587
+ const missing: string[] = [];
588
+ let record: Record<string, unknown> | null | undefined;
589
+
590
+ for (const [name, binding] of Object.entries(bindings)) {
591
+ let value: unknown;
592
+ let present = false;
593
+
594
+ if ('value' in binding) {
595
+ value = binding.value;
596
+ present = true;
597
+ } else {
598
+ const source = binding.from ?? `json.${name}`;
599
+ if (source === 'stdout') {
600
+ value = stdout;
601
+ present = true;
602
+ } else if (source === 'stderr') {
603
+ value = stderr;
604
+ present = true;
605
+ } else if (source === 'normalizedOutput') {
606
+ if (normalizedOutput !== null) {
607
+ value = normalizedOutput;
608
+ present = true;
609
+ }
610
+ } else if (source.startsWith('json.')) {
611
+ if (record === undefined) {
612
+ const jsonSource = (normalizedOutput ?? '').length > 0 ? normalizedOutput! : stdout;
613
+ record = parseJsonTail(jsonSource);
614
+ }
615
+ const key = source.slice('json.'.length);
616
+ if (record && key in record) {
617
+ value = record[key];
618
+ present = true;
619
+ }
620
+ }
621
+ }
622
+
623
+ if (!present && 'default' in binding) {
624
+ value = binding.default;
625
+ present = true;
626
+ }
627
+
628
+ if (!present || value === undefined || value === null) {
629
+ missing.push(name);
630
+ continue;
631
+ }
632
+
633
+ outputs[name] = value;
634
+ }
635
+
636
+ return {
637
+ outputs,
638
+ diagnostic: missing.length > 0 ? `outputs: unresolved binding output(s): ${missing.join(', ')}` : null,
639
+ };
640
+ }
641
+
419
642
  /**
420
643
  * Find the last non-empty line that parses as a JSON object. Returns
421
644
  * null when no such line exists. Also tries the whole source as a
package/src/registry.ts CHANGED
@@ -37,7 +37,7 @@ function singularCategory(category: PluginCategory): string {
37
37
  * registration time rather than crashing the engine mid-run.
38
38
  *
39
39
  * For drivers we materialize `capabilities` and assert each field is a
40
- * boolean otherwise a plugin author can write
40
+ * boolean �?otherwise a plugin author can write
41
41
  * get capabilities() { throw new Error('boom') }
42
42
  * and pass the basic typeof check, then crash preflight when the engine
43
43
  * touches `driver.capabilities.sessionResume`. (R8)
@@ -55,7 +55,7 @@ function validateContract(category: PluginCategory, handler: unknown): void {
55
55
  if (typeof h.buildCommand !== 'function') {
56
56
  throw new Error(`drivers plugin "${h.name}" must export buildCommand()`);
57
57
  }
58
- // Materialize capabilities this triggers any throwing getter NOW
58
+ // Materialize capabilities �?this triggers any throwing getter NOW
59
59
  // instead of during preflight.
60
60
  let caps: unknown;
61
61
  try {
@@ -132,7 +132,7 @@ export function isValidPluginName(name: unknown): name is string {
132
132
  *
133
133
  * Returns the strongly-typed manifest if the field is present and
134
134
  * well-formed (`category` is one of the four known categories and `type`
135
- * is a non-empty string). Returns `null` if the field is absent that
135
+ * is a non-empty string). Returns `null` if the field is absent �?that
136
136
  * is the host's signal that the package is a library, not a plugin.
137
137
  *
138
138
  * Throws if the field is present but malformed: that's a packaging bug
@@ -170,9 +170,7 @@ export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
170
170
  /**
171
171
  * Instance-scoped plugin registry. Each workspace in a multi-tenant sidecar
172
172
  * owns its own PluginRegistry, so installing/uninstalling a driver in one
173
- * workspace cannot clobber another. The process-wide `defaultRegistry`
174
- * exported at the bottom of this file preserves the historical free-function
175
- * API (registerPlugin / getHandler / …) for CLI and single-tenant hosts.
173
+ * workspace cannot clobber another.
176
174
  */
177
175
  export class PluginRegistry {
178
176
  private readonly registries = {
@@ -216,12 +214,12 @@ export class PluginRegistry {
216
214
  if (wasReplaced) {
217
215
  // D18: surface silent shadowing. Hot-reload flows legitimately replace
218
216
  // handlers; installing two different plugin packages that both claim
219
- // the same (category, type) does not the second wins and breaks the
217
+ // the same (category, type) does not �?the second wins and breaks the
220
218
  // first's consumers with no audit trail. A console.warn is cheap,
221
219
  // respects existing callers that rely on 'replaced', and gives ops a
222
220
  // grep-able signal when registrations collide unexpectedly.
223
221
  console.warn(
224
- `[tagma-sdk] registerPlugin: replaced existing ${category}/${type} ` +
222
+ `[tagma-sdk] registerPlugin: replaced existing ${category}/${type} �?` +
225
223
  `check for duplicate plugin packages claiming the same type.`,
226
224
  );
227
225
  }
@@ -231,8 +229,7 @@ export class PluginRegistry {
231
229
  /**
232
230
  * Remove a plugin from the in-process registry. Returns true if a plugin
233
231
  * was actually removed. Note: ESM module caching is not affected, so
234
- * re-importing the same file after unregister will yield the cached module
235
- * callers wanting a fresh load must restart the host process.
232
+ * re-importing the same file after unregister will yield the cached module �? * callers wanting a fresh load must restart the host process.
236
233
  */
237
234
  unregisterPlugin(category: PluginCategory, type: string): boolean {
238
235
  if (!VALID_CATEGORIES.has(category)) return false;
@@ -299,42 +296,3 @@ export class PluginRegistry {
299
296
  }
300
297
  }
301
298
  }
302
-
303
- /**
304
- * Process-wide default registry. Preserves the historical free-function API
305
- * for CLI and single-tenant hosts. Multi-tenant hosts (the editor sidecar
306
- * after the one-sidecar refactor) build their own `PluginRegistry` per
307
- * workspace and pass it through `RunPipelineOptions.registry`.
308
- */
309
- export const defaultRegistry = new PluginRegistry();
310
-
311
- export function registerPlugin<T extends PluginType>(
312
- category: PluginCategory,
313
- type: string,
314
- handler: T,
315
- ): RegisterResult {
316
- return defaultRegistry.registerPlugin(category, type, handler);
317
- }
318
-
319
- export function unregisterPlugin(category: PluginCategory, type: string): boolean {
320
- return defaultRegistry.unregisterPlugin(category, type);
321
- }
322
-
323
- export function getHandler<T extends PluginType>(category: PluginCategory, type: string): T {
324
- return defaultRegistry.getHandler<T>(category, type);
325
- }
326
-
327
- export function hasHandler(category: PluginCategory, type: string): boolean {
328
- return defaultRegistry.hasHandler(category, type);
329
- }
330
-
331
- export function listRegistered(category: PluginCategory): string[] {
332
- return defaultRegistry.listRegistered(category);
333
- }
334
-
335
- export function loadPlugins(
336
- pluginNames: readonly string[],
337
- resolveFrom?: string,
338
- ): Promise<void> {
339
- return defaultRegistry.loadPlugins(pluginNames, resolveFrom);
340
- }