@tagma/sdk 0.4.8 → 0.4.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.
@@ -0,0 +1,93 @@
1
+ // ═══ Template Discovery (F1) ═══
2
+ //
3
+ // Public helpers so editors / UIs can enumerate installed `@tagma/template-*`
4
+ // packages in a workspace and read each template's declarative metadata
5
+ // (name, description, params) without actually expanding the template.
6
+ //
7
+ // The legacy private `loadTemplate` in schema.ts uses Bun-specific APIs
8
+ // (Bun.file, require.resolve). These helpers are Node-compatible because
9
+ // the editor server runs on Node, not Bun.
10
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
11
+ import { join } from 'path';
12
+ import yaml from 'js-yaml';
13
+ /**
14
+ * Scan the workspace's `node_modules/@tagma/*` for packages whose name starts
15
+ * with `template-` and load each one's manifest. Packages without a valid
16
+ * `template.yaml` (or that fail to parse) are silently skipped.
17
+ *
18
+ * Returns an empty array when `workDir` doesn't exist or has no such packages.
19
+ */
20
+ export function discoverTemplates(workDir) {
21
+ const out = [];
22
+ const scopeDir = join(workDir, 'node_modules', '@tagma');
23
+ if (!existsSync(scopeDir))
24
+ return out;
25
+ let entries = [];
26
+ try {
27
+ entries = readdirSync(scopeDir);
28
+ }
29
+ catch {
30
+ return out;
31
+ }
32
+ for (const entry of entries) {
33
+ if (!entry.startsWith('template-'))
34
+ continue;
35
+ const pkgDir = join(scopeDir, entry);
36
+ try {
37
+ const st = statSync(pkgDir);
38
+ if (!st.isDirectory())
39
+ continue;
40
+ }
41
+ catch {
42
+ continue;
43
+ }
44
+ const ref = `@tagma/${entry}`;
45
+ const manifest = loadTemplateManifestFromDir(pkgDir, ref);
46
+ if (manifest)
47
+ out.push(manifest);
48
+ }
49
+ // Sort alphabetically for deterministic UI rendering.
50
+ out.sort((a, b) => a.ref.localeCompare(b.ref));
51
+ return out;
52
+ }
53
+ /**
54
+ * Load a single template's manifest by its ref (e.g. `@tagma/template-review`)
55
+ * from the given workspace's `node_modules`. Returns `null` if the package
56
+ * isn't installed or its manifest can't be parsed.
57
+ */
58
+ export function loadTemplateManifest(ref, workDir) {
59
+ // Only @tagma/template-* refs are supported (matches SDK validateTemplateRef).
60
+ const stripped = ref.replace(/@v\d+$/, '');
61
+ if (!stripped.startsWith('@tagma/template-'))
62
+ return null;
63
+ const pkgDir = join(workDir, 'node_modules', stripped);
64
+ if (!existsSync(pkgDir))
65
+ return null;
66
+ return loadTemplateManifestFromDir(pkgDir, stripped);
67
+ }
68
+ /**
69
+ * Resolve a template manifest from an absolute package directory. Tries
70
+ * `template.yaml` first (the documented convention), then a `template` export
71
+ * from `package.json`'s `main`. Returns `null` on any failure so discovery
72
+ * stays robust against malformed packages.
73
+ */
74
+ function loadTemplateManifestFromDir(pkgDir, ref) {
75
+ const yamlPath = join(pkgDir, 'template.yaml');
76
+ if (existsSync(yamlPath)) {
77
+ try {
78
+ const content = readFileSync(yamlPath, 'utf-8');
79
+ const doc = yaml.load(content);
80
+ const tpl = (doc && typeof doc === 'object' && 'template' in doc
81
+ ? doc.template
82
+ : doc);
83
+ if (tpl && typeof tpl === 'object' && tpl.name && Array.isArray(tpl.tasks)) {
84
+ return { ...tpl, ref };
85
+ }
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/sdk",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Local AI task orchestration engine — core SDK for tagma pipelines",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/dag.ts CHANGED
@@ -178,22 +178,22 @@ export interface RawDag {
178
178
  }
179
179
 
180
180
  /**
181
- * Build a lightweight DAG from a raw (unresolved) pipeline config.
182
- * Unlike buildDag, this function:
183
- * - Does not require a workDir or resolved PipelineConfig
184
- * - Is lenient: missing or ambiguous refs are silently skipped
185
- *
186
- * Intended for the visual editor to render the flow graph before a pipeline is run.
187
- */
188
- export function buildRawDag(config: RawPipelineConfig): RawDag {
181
+ * Build a lightweight DAG from a raw (unresolved) pipeline config.
182
+ * Unlike buildDag, this function:
183
+ * - Does not require a workDir or resolved PipelineConfig
184
+ * - Is lenient: missing or ambiguous refs are silently skipped
185
+ *
186
+ * Intended for the visual editor to render the flow graph before a pipeline is run.
187
+ */
188
+ export function buildRawDag(config: RawPipelineConfig): RawDag {
189
189
  const nodes = new Map<string, RawDagNode>();
190
190
  const bareToQualified = new Map<string, string>();
191
191
 
192
- // 1. Register all concrete tasks
193
- for (const track of config.tracks) {
194
- for (const task of track.tasks) {
195
- const qid = `${track.id}.${task.id}`;
196
- if (nodes.has(qid)) continue; // skip duplicates silently
192
+ // 1. Register all concrete tasks
193
+ for (const track of config.tracks) {
194
+ for (const task of track.tasks) {
195
+ const qid = `${track.id}.${task.id}`;
196
+ if (nodes.has(qid)) continue; // skip duplicates silently
197
197
 
198
198
  if (bareToQualified.has(task.id)) {
199
199
  bareToQualified.set(task.id, '__ambiguous__');
@@ -216,10 +216,10 @@ export function buildRawDag(config: RawPipelineConfig): RawDag {
216
216
 
217
217
  const edges: { from: string; to: string }[] = [];
218
218
 
219
- for (const track of config.tracks) {
220
- for (const task of track.tasks) {
221
- const qid = `${track.id}.${task.id}`;
222
- const deps: string[] = [];
219
+ for (const track of config.tracks) {
220
+ for (const task of track.tasks) {
221
+ const qid = `${track.id}.${task.id}`;
222
+ const deps: string[] = [];
223
223
 
224
224
  for (const ref of task.depends_on ?? []) {
225
225
  const resolved = tryResolve(ref, track.id);
package/src/engine.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { resolve, dirname } from 'path';
2
- import { mkdir, readdir, rm } from 'fs/promises';
1
+ import { resolve } from 'path';
2
+ import { readdir, rm } from 'fs/promises';
3
3
  import type {
4
4
  PipelineConfig, TaskConfig, TrackConfig, TaskState, TaskStatus,
5
5
  TaskResult, DriverPlugin, TriggerPlugin, CompletionPlugin,
@@ -9,7 +9,7 @@ import type {
9
9
  import { buildDag, type Dag, type DagNode } from './dag';
10
10
  import { getHandler, hasHandler, loadPlugins } from './registry';
11
11
  import { runSpawn, runCommand } from './runner';
12
- import { parseDuration, nowISO, generateRunId, validatePath } from './utils';
12
+ import { parseDuration, nowISO, generateRunId } from './utils';
13
13
  import {
14
14
  executeHook,
15
15
  buildPipelineStartContext, buildTaskContext,
@@ -79,7 +79,7 @@ function preflight(config: PipelineConfig, dag: Dag): void {
79
79
  const upstream = dag.nodes.get(upstreamId);
80
80
  if (upstream) {
81
81
  // A handoff is possible via session resume (already ruled out above),
82
- // an output file, OR in-memory text injection through normalizedMap
82
+ // OR in-memory text injection through normalizedMap
83
83
  // (when the upstream driver implements parseResult and returns normalizedOutput).
84
84
  const upstreamDriverName = upstream.task.driver ?? upstream.track.driver
85
85
  ?? config.driver ?? 'claude-code';
@@ -88,12 +88,12 @@ function preflight(config: PipelineConfig, dag: Dag): void {
88
88
  : null;
89
89
  const canNormalize = typeof upstreamDriver?.parseResult === 'function';
90
90
 
91
- if (!upstream.task.output && !canNormalize) {
91
+ if (!canNormalize) {
92
92
  errors.push(
93
93
  `Task "${node.taskId}" uses continue_from: "${task.continue_from}", ` +
94
- `but upstream task "${upstreamId}" has no "output" field and its driver ` +
94
+ `but upstream task "${upstreamId}" its driver ` +
95
95
  `does not implement parseResult for text-injection handoff. ` +
96
- `Add output to the upstream task, use a driver with parseResult, or remove continue_from.`
96
+ `Use a driver with parseResult, or remove continue_from.`
97
97
  );
98
98
  }
99
99
  }
@@ -297,7 +297,6 @@ export async function runPipeline(
297
297
  options.onEvent?.({ type: 'pipeline_start', runId, states: statesSnapshot });
298
298
 
299
299
  const sessionMap = new Map<string, string>();
300
- const outputMap = new Map<string, string>();
301
300
  const normalizedMap = new Map<string, string>();
302
301
 
303
302
  // Pipeline timeout
@@ -407,7 +406,6 @@ export async function runPipeline(
407
406
  status: state.status,
408
407
  exit_code: state.result?.exitCode ?? null,
409
408
  duration_ms: state.result?.durationMs ?? null,
410
- output_path: state.result?.outputPath ?? null,
411
409
  stderr_path: state.result?.stderrPath ?? null,
412
410
  session_id: state.result?.sessionId ?? null,
413
411
  started_at: state.startedAt,
@@ -585,7 +583,7 @@ export async function runPipeline(
585
583
  log.debug(`[task:${taskId}]`,
586
584
  `middleware chain: ${mws.map(m => m.type).join(' → ')}`);
587
585
  const mwCtx: MiddlewareContext = {
588
- task, track, outputMap, workDir: task.cwd ?? workDir,
586
+ task, track, workDir: task.cwd ?? workDir,
589
587
  };
590
588
  for (const mwConfig of mws) {
591
589
  const before = prompt.length;
@@ -611,7 +609,7 @@ export async function runPipeline(
611
609
 
612
610
  // H1: hand the driver a continue_from that has already been
613
611
  // qualified by dag.ts. Without this, drivers like codex/opencode/
614
- // claude-code do `outputMap.get(task.continue_from)` directly with
612
+ // claude-code look up maps directly with
615
613
  // the user's raw (possibly bare) string, which races whenever two
616
614
  // tracks share a task name. dag.ts has the only authoritative
617
615
  // resolver, so we use its precomputed answer here.
@@ -621,7 +619,7 @@ export async function runPipeline(
621
619
  continue_from: node.resolvedContinueFrom ?? task.continue_from,
622
620
  };
623
621
  const driverCtx: DriverContext = {
624
- sessionMap, outputMap, normalizedMap, workDir: task.cwd ?? workDir,
622
+ sessionMap, normalizedMap, workDir: task.cwd ?? workDir,
625
623
  };
626
624
  const spec = await driver.buildCommand(enrichedTask, track, driverCtx);
627
625
  log.debug(`[task:${taskId}]`, `driver=${driverName}`);
@@ -635,27 +633,6 @@ export async function runPipeline(
635
633
  result = await runSpawn(spec, driver, runOpts);
636
634
  }
637
635
 
638
- // 5. Write output file with RAW stdout (preserves driver output format).
639
- // Done BEFORE the completion check so a `file_exists` completion pointing
640
- // at `task.output` observes the AI-generated artefact. Writes happen
641
- // regardless of exit code so failed/timed-out tasks still leave a
642
- // debuggable artefact on disk.
643
- if (task.output) {
644
- // validatePath enforces no .. traversal and no absolute paths escaping workDir.
645
- const outPath = validatePath(task.output, workDir);
646
- await mkdir(dirname(outPath), { recursive: true });
647
- await Bun.write(outPath, result.stdout);
648
- result = { ...result, outputPath: outPath };
649
- // H1: only write the fully-qualified taskId. The previous "also store
650
- // bare id when not yet present" trick produced non-deterministic
651
- // continue_from lookups when two tracks shared a task name —
652
- // whichever finished first won the bare key. dag.ts now resolves
653
- // continue_from to a qualified id (DagNode.resolvedContinueFrom),
654
- // and the enrichedTask handed to drivers carries that qualified
655
- // version, so bare keys are no longer needed.
656
- outputMap.set(taskId, outPath);
657
- }
658
-
659
636
  // 6. Determine terminal status (without emitting yet — result must be complete first)
660
637
  // H2: branch on failureKind so spawn errors no longer masquerade as
661
638
  // timeouts. Old runners that don't set failureKind still work — we
@@ -697,7 +674,6 @@ export async function runPipeline(
697
674
  ? result.normalizedOutput.slice(0, MAX_NORMALIZED_BYTES) +
698
675
  `\n[…clipped at ${MAX_NORMALIZED_BYTES} bytes]`
699
676
  : result.normalizedOutput;
700
- // H1: qualified-only key (see comment near outputMap above).
701
677
  normalizedMap.set(taskId, clipped);
702
678
  }
703
679
 
@@ -708,7 +684,7 @@ export async function runPipeline(
708
684
  }
709
685
 
710
686
  if (result.sessionId) {
711
- // H1: qualified-only key (see comment near outputMap above).
687
+ // H1: qualified-only key.
712
688
  sessionMap.set(taskId, result.sessionId);
713
689
  }
714
690
 
@@ -736,9 +712,6 @@ export async function runPipeline(
736
712
  if (result.sessionId) {
737
713
  log.debug(`[task:${taskId}]`, `sessionId: ${result.sessionId}`);
738
714
  }
739
- if (result.outputPath) {
740
- log.debug(`[task:${taskId}]`, `wrote output: ${result.outputPath}`);
741
- }
742
715
  if (result.stderrPath) {
743
716
  log.debug(`[task:${taskId}]`, `wrote stderr: ${result.stderrPath}`);
744
717
  }
@@ -760,7 +733,7 @@ export async function runPipeline(
760
733
  exitCode: -1,
761
734
  stdout: '',
762
735
  stderr: errMsg,
763
- outputPath: null, stderrPath: null, durationMs: 0,
736
+ stderrPath: null, durationMs: 0,
764
737
  sessionId: null, normalizedOutput: null,
765
738
  // H2: Engine-level pre-execution errors (driver throw, middleware
766
739
  // throw, getHandler 404) classify as spawn_error — the process never
package/src/hooks.ts CHANGED
@@ -141,7 +141,6 @@ export interface TaskInfo {
141
141
  readonly status: string;
142
142
  readonly exit_code: number | null;
143
143
  readonly duration_ms: number | null;
144
- readonly output_path: string | null;
145
144
  readonly stderr_path: string | null;
146
145
  readonly session_id: string | null;
147
146
  readonly started_at: string | null;