@tagma/sdk 0.7.3 → 0.7.5
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 +85 -57
- package/dist/approval.d.ts +2 -12
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +1 -90
- package/dist/approval.js.map +1 -1
- package/dist/bootstrap.d.ts +1 -1
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/completions/file-exists.js +1 -1
- package/dist/completions/file-exists.js.map +1 -1
- package/dist/completions/output-check.d.ts.map +1 -1
- package/dist/completions/output-check.js +17 -4
- package/dist/completions/output-check.js.map +1 -1
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/dataflow.d.ts +3 -0
- package/dist/dataflow.d.ts.map +1 -0
- package/dist/dataflow.js +2 -0
- package/dist/dataflow.js.map +1 -0
- package/dist/drivers/opencode.d.ts.map +1 -1
- package/dist/drivers/opencode.js +23 -71
- package/dist/drivers/opencode.js.map +1 -1
- package/dist/engine.d.ts +5 -56
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +7 -297
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +2 -60
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -153
- package/dist/logger.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +1 -2
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/runner.d.ts +1 -35
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -610
- package/dist/runner.js.map +1 -1
- package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
- package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/stdin-approval.js +2 -0
- package/dist/runtime/adapters/stdin-approval.js.map +1 -0
- package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
- package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
- package/dist/runtime/adapters/websocket-approval.js +2 -0
- package/dist/runtime/adapters/websocket-approval.js.map +1 -0
- package/dist/runtime/bun-process-runner.d.ts +2 -0
- package/dist/runtime/bun-process-runner.d.ts.map +1 -0
- package/dist/runtime/bun-process-runner.js +2 -0
- package/dist/runtime/bun-process-runner.js.map +1 -0
- package/dist/runtime.d.ts +2 -8
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +1 -7
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -4
- package/dist/schema.js.map +1 -1
- package/dist/tagma.d.ts +3 -4
- package/dist/tagma.d.ts.map +1 -1
- package/dist/tagma.js +2 -3
- package/dist/tagma.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +74 -108
- package/dist/triggers/file.js.map +1 -1
- package/dist/triggers/manual.d.ts.map +1 -1
- package/dist/triggers/manual.js +1 -2
- package/dist/triggers/manual.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -12
- package/dist/types.js.map +1 -1
- package/dist/utils-api.d.ts +1 -1
- package/dist/utils-api.d.ts.map +1 -1
- package/dist/utils-api.js +1 -1
- package/dist/utils-api.js.map +1 -1
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +5 -12
- package/dist/validate-raw.js.map +1 -1
- package/package.json +20 -22
- package/dist/adapters/stdin-approval.d.ts +0 -6
- package/dist/adapters/stdin-approval.d.ts.map +0 -1
- package/dist/adapters/stdin-approval.js +0 -90
- package/dist/adapters/stdin-approval.js.map +0 -1
- package/dist/adapters/websocket-approval.d.ts +0 -28
- package/dist/adapters/websocket-approval.d.ts.map +0 -1
- package/dist/adapters/websocket-approval.js +0 -147
- package/dist/adapters/websocket-approval.js.map +0 -1
- package/dist/core/dataflow.d.ts +0 -23
- package/dist/core/dataflow.d.ts.map +0 -1
- package/dist/core/dataflow.js +0 -99
- package/dist/core/dataflow.js.map +0 -1
- package/dist/core/log-prune.d.ts +0 -16
- package/dist/core/log-prune.d.ts.map +0 -1
- package/dist/core/log-prune.js +0 -34
- package/dist/core/log-prune.js.map +0 -1
- package/dist/core/preflight.d.ts +0 -13
- package/dist/core/preflight.d.ts.map +0 -1
- package/dist/core/preflight.js +0 -61
- package/dist/core/preflight.js.map +0 -1
- package/dist/core/run-context.d.ts +0 -55
- package/dist/core/run-context.d.ts.map +0 -1
- package/dist/core/run-context.js +0 -158
- package/dist/core/run-context.js.map +0 -1
- package/dist/core/run-state.d.ts +0 -25
- package/dist/core/run-state.d.ts.map +0 -1
- package/dist/core/run-state.js +0 -93
- package/dist/core/run-state.js.map +0 -1
- package/dist/core/scheduler.d.ts +0 -13
- package/dist/core/scheduler.d.ts.map +0 -1
- package/dist/core/scheduler.js +0 -35
- package/dist/core/scheduler.js.map +0 -1
- package/dist/core/task-executor.d.ts +0 -13
- package/dist/core/task-executor.d.ts.map +0 -1
- package/dist/core/task-executor.js +0 -601
- package/dist/core/task-executor.js.map +0 -1
- package/dist/core/trigger-errors.d.ts +0 -9
- package/dist/core/trigger-errors.d.ts.map +0 -1
- package/dist/core/trigger-errors.js +0 -15
- package/dist/core/trigger-errors.js.map +0 -1
- package/dist/dag.d.ts +0 -45
- package/dist/dag.d.ts.map +0 -1
- package/dist/dag.js +0 -177
- package/dist/dag.js.map +0 -1
- package/dist/hooks.d.ts +0 -73
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -106
- package/dist/hooks.js.map +0 -1
- package/dist/pipeline-definition.d.ts +0 -3
- package/dist/pipeline-definition.d.ts.map +0 -1
- package/dist/pipeline-definition.js +0 -4
- package/dist/pipeline-definition.js.map +0 -1
- package/dist/ports.d.ts +0 -196
- package/dist/ports.d.ts.map +0 -1
- package/dist/ports.js +0 -688
- package/dist/ports.js.map +0 -1
- package/dist/prompt-doc.d.ts +0 -70
- package/dist/prompt-doc.d.ts.map +0 -1
- package/dist/prompt-doc.js +0 -154
- package/dist/prompt-doc.js.map +0 -1
- package/dist/registry.d.ts +0 -67
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -293
- package/dist/registry.js.map +0 -1
- package/dist/task-ref.d.ts +0 -55
- package/dist/task-ref.d.ts.map +0 -1
- package/dist/task-ref.js +0 -103
- package/dist/task-ref.js.map +0 -1
- package/dist/utils.d.ts +0 -13
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -177
- package/dist/utils.js.map +0 -1
- package/src/adapters/stdin-approval.ts +0 -106
- package/src/adapters/websocket-approval.ts +0 -224
- package/src/approval.ts +0 -131
- package/src/bootstrap.ts +0 -55
- package/src/completions/exit-code.ts +0 -34
- package/src/completions/file-exists.ts +0 -66
- package/src/completions/output-check.test.ts +0 -50
- package/src/completions/output-check.ts +0 -92
- package/src/config-ops.test.ts +0 -70
- package/src/config-ops.ts +0 -328
- package/src/config.ts +0 -26
- package/src/core/dataflow.test.ts +0 -166
- package/src/core/dataflow.ts +0 -161
- package/src/core/log-prune.test.ts +0 -58
- package/src/core/log-prune.ts +0 -43
- package/src/core/preflight.test.ts +0 -49
- package/src/core/preflight.ts +0 -89
- package/src/core/run-context.test.ts +0 -256
- package/src/core/run-context.ts +0 -211
- package/src/core/run-state.test.ts +0 -98
- package/src/core/run-state.ts +0 -122
- package/src/core/scheduler.test.ts +0 -83
- package/src/core/scheduler.ts +0 -42
- package/src/core/task-executor.ts +0 -743
- package/src/core/trigger-errors.ts +0 -15
- package/src/dag.test.ts +0 -56
- package/src/dag.ts +0 -245
- package/src/drivers/opencode.ts +0 -410
- package/src/engine-ports-mixed.test.ts +0 -156
- package/src/engine-ports.test.ts +0 -166
- package/src/engine-task-type.test.ts +0 -56
- package/src/engine.ts +0 -458
- package/src/hooks.ts +0 -193
- package/src/index.ts +0 -33
- package/src/logger.ts +0 -182
- package/src/middlewares/static-context.ts +0 -49
- package/src/pipeline-definition.ts +0 -5
- package/src/pipeline-runner.test.ts +0 -91
- package/src/pipeline-runner.ts +0 -194
- package/src/plugin-registry.test.ts +0 -382
- package/src/plugins.ts +0 -21
- package/src/ports.test.ts +0 -678
- package/src/ports.ts +0 -925
- package/src/prompt-doc.test.ts +0 -174
- package/src/prompt-doc.ts +0 -169
- package/src/registry.ts +0 -353
- package/src/runner.test.ts +0 -142
- package/src/runner.ts +0 -666
- package/src/runtime.ts +0 -20
- package/src/schema-ports.test.ts +0 -172
- package/src/schema.test.ts +0 -213
- package/src/schema.ts +0 -379
- package/src/tagma.test.ts +0 -155
- package/src/tagma.ts +0 -62
- package/src/task-ref.test.ts +0 -401
- package/src/task-ref.ts +0 -121
- package/src/triggers/file.ts +0 -164
- package/src/triggers/manual.ts +0 -86
- package/src/types.ts +0 -18
- package/src/utils-api.ts +0 -8
- package/src/utils.test.ts +0 -28
- package/src/utils.ts +0 -203
- package/src/validate-raw-plugin-types.test.ts +0 -60
- package/src/validate-raw-ports.test.ts +0 -136
- package/src/validate-raw.ts +0 -852
- package/src/yaml-compiler.test.ts +0 -108
- package/src/yaml-compiler.ts +0 -110
- package/src/yaml.ts +0 -11
package/src/engine-ports.test.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { bootstrapBuiltins } from './bootstrap';
|
|
6
|
-
import { runPipeline, type RunEventPayload } from './engine';
|
|
7
|
-
import { PluginRegistry } from './registry';
|
|
8
|
-
import type { PipelineConfig, TaskConfig, TaskStatus } from './types';
|
|
9
|
-
|
|
10
|
-
const PERMS = { read: true, write: false, execute: false };
|
|
11
|
-
|
|
12
|
-
function freshRegistry(): PluginRegistry {
|
|
13
|
-
const reg = new PluginRegistry();
|
|
14
|
-
bootstrapBuiltins(reg);
|
|
15
|
-
return reg;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function makeDir(): string {
|
|
19
|
-
return mkdtempSync(join(tmpdir(), 'tagma-bindings-'));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function writeEmitScript(dir: string, name: string, payload: Record<string, unknown>): string {
|
|
23
|
-
const path = join(dir, `${name}.js`);
|
|
24
|
-
writeFileSync(
|
|
25
|
-
path,
|
|
26
|
-
`process.stdout.write(${JSON.stringify(JSON.stringify(payload))});\nprocess.stdout.write('\\n');\n`,
|
|
27
|
-
);
|
|
28
|
-
return path;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function writeEchoArgsScript(dir: string, name: string): string {
|
|
32
|
-
const path = join(dir, `${name}.js`);
|
|
33
|
-
writeFileSync(path, `process.stdout.write(process.argv.slice(2).join('|'));\n`);
|
|
34
|
-
return path;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function task(overrides: Partial<TaskConfig> & { id: string }): TaskConfig {
|
|
38
|
-
return {
|
|
39
|
-
name: overrides.id,
|
|
40
|
-
permissions: PERMS,
|
|
41
|
-
driver: 'opencode',
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function pipeline(tasks: TaskConfig[]): PipelineConfig {
|
|
47
|
-
return {
|
|
48
|
-
name: 'bindings-test',
|
|
49
|
-
tracks: [
|
|
50
|
-
{
|
|
51
|
-
id: 't',
|
|
52
|
-
name: 'T',
|
|
53
|
-
driver: 'opencode',
|
|
54
|
-
permissions: PERMS,
|
|
55
|
-
on_failure: 'skip_downstream',
|
|
56
|
-
tasks,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function run(config: PipelineConfig, workDir: string) {
|
|
63
|
-
const events: RunEventPayload[] = [];
|
|
64
|
-
const result = await runPipeline(config, workDir, {
|
|
65
|
-
registry: freshRegistry(),
|
|
66
|
-
skipPluginLoading: true,
|
|
67
|
-
onEvent: (e) => events.push(e),
|
|
68
|
-
});
|
|
69
|
-
return { events, success: result.success };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function finalUpdateFor(events: RunEventPayload[], qid: string): RunEventPayload | undefined {
|
|
73
|
-
let last: RunEventPayload | undefined;
|
|
74
|
-
for (const ev of events) {
|
|
75
|
-
if (ev.type === 'task_update' && ev.taskId === qid) last = ev;
|
|
76
|
-
}
|
|
77
|
-
return last;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function finalStatusFrom(events: RunEventPayload[], qid: string): TaskStatus | undefined {
|
|
81
|
-
const last = finalUpdateFor(events, qid);
|
|
82
|
-
return last && last.type === 'task_update' ? last.status : undefined;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
describe('engine — unified inputs and outputs', () => {
|
|
86
|
-
test('typed outputs feed typed inputs and command placeholders', async () => {
|
|
87
|
-
const dir = makeDir();
|
|
88
|
-
try {
|
|
89
|
-
const emit = writeEmitScript(dir, 'emit', { id: '42', city: 'Shanghai' });
|
|
90
|
-
const echo = writeEchoArgsScript(dir, 'echo');
|
|
91
|
-
const config = pipeline([
|
|
92
|
-
task({
|
|
93
|
-
id: 'up',
|
|
94
|
-
command: `node "${emit}"`,
|
|
95
|
-
outputs: { id: { type: 'number' }, city: { type: 'string' } },
|
|
96
|
-
}),
|
|
97
|
-
task({
|
|
98
|
-
id: 'down',
|
|
99
|
-
depends_on: ['up'],
|
|
100
|
-
command: `node "${echo}" "{{inputs.city}}" "{{inputs.id}}"`,
|
|
101
|
-
inputs: {
|
|
102
|
-
city: { from: 't.up.outputs.city', type: 'string', required: true },
|
|
103
|
-
id: { from: 't.up.outputs.id', type: 'number', required: true },
|
|
104
|
-
},
|
|
105
|
-
}),
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
const { events, success } = await run(config, dir);
|
|
109
|
-
expect(success).toBe(true);
|
|
110
|
-
expect(finalUpdateFor(events, 't.up')?.outputs).toEqual({ id: 42, city: 'Shanghai' });
|
|
111
|
-
expect(finalUpdateFor(events, 't.down')?.inputs).toEqual({ city: 'Shanghai', id: 42 });
|
|
112
|
-
expect(finalUpdateFor(events, 't.down')?.stdout).toContain('Shanghai|42');
|
|
113
|
-
} finally {
|
|
114
|
-
rmSync(dir, { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('missing required unified input blocks without spawning downstream', async () => {
|
|
119
|
-
const dir = makeDir();
|
|
120
|
-
try {
|
|
121
|
-
const emit = writeEmitScript(dir, 'emit', { other: 'x' });
|
|
122
|
-
const echo = writeEchoArgsScript(dir, 'echo');
|
|
123
|
-
const config = pipeline([
|
|
124
|
-
task({ id: 'up', command: `node "${emit}"`, outputs: { city: { type: 'string' } } }),
|
|
125
|
-
task({
|
|
126
|
-
id: 'down',
|
|
127
|
-
depends_on: ['up'],
|
|
128
|
-
command: `node "${echo}" "{{inputs.city}}"`,
|
|
129
|
-
inputs: { city: { from: 't.up.outputs.city', type: 'string', required: true } },
|
|
130
|
-
}),
|
|
131
|
-
]);
|
|
132
|
-
|
|
133
|
-
const { events, success } = await run(config, dir);
|
|
134
|
-
expect(success).toBe(false);
|
|
135
|
-
expect(finalStatusFrom(events, 't.up')).toBe('success');
|
|
136
|
-
expect(finalStatusFrom(events, 't.down')).toBe('blocked');
|
|
137
|
-
} finally {
|
|
138
|
-
rmSync(dir, { recursive: true, force: true });
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('typed output coercion diagnostics leave missing downstream input', async () => {
|
|
143
|
-
const dir = makeDir();
|
|
144
|
-
try {
|
|
145
|
-
const emit = writeEmitScript(dir, 'emit', { id: 'not-a-number' });
|
|
146
|
-
const echo = writeEchoArgsScript(dir, 'echo');
|
|
147
|
-
const config = pipeline([
|
|
148
|
-
task({ id: 'up', command: `node "${emit}"`, outputs: { id: { type: 'number' } } }),
|
|
149
|
-
task({
|
|
150
|
-
id: 'down',
|
|
151
|
-
depends_on: ['up'],
|
|
152
|
-
command: `node "${echo}" "{{inputs.id}}"`,
|
|
153
|
-
inputs: { id: { from: 't.up.outputs.id', type: 'number', required: true } },
|
|
154
|
-
}),
|
|
155
|
-
]);
|
|
156
|
-
|
|
157
|
-
const { events, success } = await run(config, dir);
|
|
158
|
-
expect(success).toBe(false);
|
|
159
|
-
expect(finalStatusFrom(events, 't.up')).toBe('success');
|
|
160
|
-
expect(finalUpdateFor(events, 't.up')?.stderr).toContain('expected number');
|
|
161
|
-
expect(finalStatusFrom(events, 't.down')).toBe('blocked');
|
|
162
|
-
} finally {
|
|
163
|
-
rmSync(dir, { recursive: true, force: true });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { runPipeline, type RunEventPayload } from './engine';
|
|
6
|
-
import { PluginRegistry } from './registry';
|
|
7
|
-
import type { PipelineConfig } from './types';
|
|
8
|
-
|
|
9
|
-
function makeDir(): string {
|
|
10
|
-
return mkdtempSync(join(tmpdir(), 'tagma-task-type-'));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe('engine task type detection', () => {
|
|
14
|
-
test('empty command is still a command task and does not require a driver', async () => {
|
|
15
|
-
const dir = makeDir();
|
|
16
|
-
const previousShell = process.env.PIPELINE_SHELL;
|
|
17
|
-
process.env.PIPELINE_SHELL = 'cmd';
|
|
18
|
-
try {
|
|
19
|
-
const events: RunEventPayload[] = [];
|
|
20
|
-
const config: PipelineConfig = {
|
|
21
|
-
name: 'empty-command',
|
|
22
|
-
tracks: [
|
|
23
|
-
{
|
|
24
|
-
id: 't',
|
|
25
|
-
name: 'T',
|
|
26
|
-
tasks: [{ id: 'cmd', name: 'cmd', command: '' }],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const result = await runPipeline(config, dir, {
|
|
32
|
-
registry: new PluginRegistry(),
|
|
33
|
-
skipPluginLoading: true,
|
|
34
|
-
onEvent: (event) => events.push(event),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
expect(result.success).toBe(true);
|
|
38
|
-
expect(events.some((event) => event.type === 'run_start')).toBe(true);
|
|
39
|
-
const final = events.findLast(
|
|
40
|
-
(event) => event.type === 'task_update' && event.taskId === 't.cmd',
|
|
41
|
-
);
|
|
42
|
-
expect(final?.type).toBe('task_update');
|
|
43
|
-
if (final?.type === 'task_update') {
|
|
44
|
-
expect(final.status).toBe('success');
|
|
45
|
-
expect(final.resolvedDriver).toBeNull();
|
|
46
|
-
}
|
|
47
|
-
} finally {
|
|
48
|
-
if (previousShell === undefined) {
|
|
49
|
-
delete process.env.PIPELINE_SHELL;
|
|
50
|
-
} else {
|
|
51
|
-
process.env.PIPELINE_SHELL = previousShell;
|
|
52
|
-
}
|
|
53
|
-
rmSync(dir, { recursive: true, force: true });
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
package/src/engine.ts
DELETED
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'path';
|
|
2
|
-
import type {
|
|
3
|
-
PipelineConfig,
|
|
4
|
-
TaskConfig,
|
|
5
|
-
TaskState,
|
|
6
|
-
RunEventPayload,
|
|
7
|
-
RunTaskState,
|
|
8
|
-
} from './types';
|
|
9
|
-
import { buildDag } from './dag';
|
|
10
|
-
import type { PluginRegistry } from './registry';
|
|
11
|
-
import { parseDuration, nowISO, generateRunId } from './utils';
|
|
12
|
-
import {
|
|
13
|
-
executeHook,
|
|
14
|
-
buildPipelineStartContext,
|
|
15
|
-
buildPipelineCompleteContext,
|
|
16
|
-
buildPipelineErrorContext,
|
|
17
|
-
type PipelineInfo,
|
|
18
|
-
} from './hooks';
|
|
19
|
-
import { Logger } from './logger';
|
|
20
|
-
import { InMemoryApprovalGateway, type ApprovalGateway } from './approval';
|
|
21
|
-
import {
|
|
22
|
-
freezeStates,
|
|
23
|
-
summarizeStates,
|
|
24
|
-
toRunTaskState,
|
|
25
|
-
} from './core/run-state';
|
|
26
|
-
import { preflight } from './core/preflight';
|
|
27
|
-
import { pruneLogDirs } from './core/log-prune';
|
|
28
|
-
import { RunContext } from './core/run-context';
|
|
29
|
-
import {
|
|
30
|
-
allTasksTerminal,
|
|
31
|
-
findLaunchableTasks,
|
|
32
|
-
skipNonTerminalTasks,
|
|
33
|
-
} from './core/scheduler';
|
|
34
|
-
import { executeTask } from './core/task-executor';
|
|
35
|
-
import { bunRuntime, type TagmaRuntime } from './runtime';
|
|
36
|
-
export { TriggerBlockedError, TriggerTimeoutError } from './core/trigger-errors';
|
|
37
|
-
|
|
38
|
-
function isPromptTaskConfig(
|
|
39
|
-
task: TaskConfig,
|
|
40
|
-
): task is TaskConfig & { readonly prompt: string; readonly command?: undefined } {
|
|
41
|
-
return task.prompt !== undefined && task.command === undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ═══ Engine ═══
|
|
45
|
-
|
|
46
|
-
export interface EngineResult {
|
|
47
|
-
readonly success: boolean;
|
|
48
|
-
readonly runId: string;
|
|
49
|
-
readonly logPath: string;
|
|
50
|
-
readonly summary: {
|
|
51
|
-
total: number;
|
|
52
|
-
success: number;
|
|
53
|
-
failed: number;
|
|
54
|
-
skipped: number;
|
|
55
|
-
timeout: number;
|
|
56
|
-
blocked: number;
|
|
57
|
-
};
|
|
58
|
-
readonly states: ReadonlyMap<string, TaskState>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ═══ Pipeline Events ═══
|
|
62
|
-
//
|
|
63
|
-
// The engine emits RunEventPayload values (defined in @tagma/types) via
|
|
64
|
-
// `onEvent`. Every payload carries `runId`; the editor server stamps a
|
|
65
|
-
// per-run `seq` before broadcasting. There is one event vocabulary
|
|
66
|
-
// end-to-end — no server-side translation layer.
|
|
67
|
-
|
|
68
|
-
// Re-export so SDK consumers can import the event type without reaching
|
|
69
|
-
// into @tagma/types directly.
|
|
70
|
-
export type { RunEventPayload } from './types';
|
|
71
|
-
|
|
72
|
-
export interface RunPipelineOptions {
|
|
73
|
-
readonly approvalGateway?: ApprovalGateway;
|
|
74
|
-
/**
|
|
75
|
-
* Maximum number of per-run log directories to retain under `<workDir>/.tagma/logs/`.
|
|
76
|
-
* Oldest directories are deleted after each run. Defaults to 20. Set to 0 to disable cleanup.
|
|
77
|
-
*/
|
|
78
|
-
readonly maxLogRuns?: number;
|
|
79
|
-
/**
|
|
80
|
-
* Caller-supplied run ID. When provided the engine uses this instead of
|
|
81
|
-
* generating its own via `generateRunId()`, keeping the editor and SDK
|
|
82
|
-
* log directories aligned on the same ID.
|
|
83
|
-
*/
|
|
84
|
-
readonly runId?: string;
|
|
85
|
-
/**
|
|
86
|
-
* External AbortSignal — aborting it cancels the pipeline immediately.
|
|
87
|
-
* Equivalent to the pipeline timeout firing, but caller-controlled.
|
|
88
|
-
*/
|
|
89
|
-
readonly signal?: AbortSignal;
|
|
90
|
-
/**
|
|
91
|
-
* Called on every pipeline/task status transition.
|
|
92
|
-
* Use for real-time UI updates (e.g. updating a visual workflow graph).
|
|
93
|
-
*/
|
|
94
|
-
readonly onEvent?: (event: RunEventPayload) => void;
|
|
95
|
-
/**
|
|
96
|
-
* Skip the engine's built-in `loadPlugins(config.plugins)` call.
|
|
97
|
-
* Use this when the host has already pre-loaded plugins from a custom
|
|
98
|
-
* resolution path (e.g. a user workspace's node_modules) so the engine
|
|
99
|
-
* doesn't re-resolve them via Node's default cwd-based import.
|
|
100
|
-
*/
|
|
101
|
-
readonly skipPluginLoading?: boolean;
|
|
102
|
-
/**
|
|
103
|
-
* Plugin registry to resolve drivers/triggers/completions/middlewares from.
|
|
104
|
-
* Callers pass a per-instance or per-workspace registry so concurrent runs
|
|
105
|
-
* do not share handler state.
|
|
106
|
-
*/
|
|
107
|
-
readonly registry: PluginRegistry;
|
|
108
|
-
/**
|
|
109
|
-
* Runtime implementation for command and driver process execution.
|
|
110
|
-
* Defaults to the SDK's Bun runtime.
|
|
111
|
-
*/
|
|
112
|
-
readonly runtime?: TagmaRuntime;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Poll interval when no tasks are in-flight but non-terminal tasks remain
|
|
116
|
-
// (e.g. tasks waiting on a file or manual trigger).
|
|
117
|
-
const POLL_INTERVAL_MS = 50;
|
|
118
|
-
|
|
119
|
-
// R15: cap on each normalized-output entry stored in normalizedMap so a
|
|
120
|
-
// runaway parseResult can't accumulate hundreds of MB across tasks. 1 MB
|
|
121
|
-
// is generous for any text-context handoff between AI tasks.
|
|
122
|
-
const MAX_NORMALIZED_BYTES = 1_000_000;
|
|
123
|
-
|
|
124
|
-
export async function runPipeline(
|
|
125
|
-
config: PipelineConfig,
|
|
126
|
-
workDir: string,
|
|
127
|
-
options: RunPipelineOptions,
|
|
128
|
-
): Promise<EngineResult> {
|
|
129
|
-
const approvalGateway = options.approvalGateway ?? new InMemoryApprovalGateway();
|
|
130
|
-
const maxLogRuns = options.maxLogRuns ?? 20;
|
|
131
|
-
const registry = options.registry;
|
|
132
|
-
const runtime = options.runtime ?? bunRuntime();
|
|
133
|
-
if (!registry) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
'runPipeline requires options.registry. Use createTagma().run(...) for the public SDK API.',
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Load any plugins declared in the pipeline config before preflight so that
|
|
140
|
-
// drivers, completions, and middlewares referenced in YAML are registered.
|
|
141
|
-
// Hosts that pre-load plugins from a custom path (e.g. the editor loading
|
|
142
|
-
// from the user's workspace node_modules) pass skipPluginLoading: true so
|
|
143
|
-
// we don't re-resolve via Node's cwd-based default import.
|
|
144
|
-
if (!options.skipPluginLoading && config.plugins?.length) {
|
|
145
|
-
await registry.loadPlugins(config.plugins);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const dag = buildDag(config);
|
|
149
|
-
const runId = options.runId ?? generateRunId();
|
|
150
|
-
preflight(config, dag, registry);
|
|
151
|
-
|
|
152
|
-
const startedAt = nowISO();
|
|
153
|
-
const pipelineInfo: PipelineInfo = { name: config.name, run_id: runId, started_at: startedAt };
|
|
154
|
-
// Forward every structured log line to subscribers as task_log events.
|
|
155
|
-
// Reading options.onEvent inside the callback (vs. capturing it once) keeps
|
|
156
|
-
// the SDK behavior correct if callers pass a fresh onEvent on each run.
|
|
157
|
-
const log = new Logger(workDir, runId, (record) => {
|
|
158
|
-
options.onEvent?.({
|
|
159
|
-
type: 'task_log',
|
|
160
|
-
runId,
|
|
161
|
-
taskId: record.taskId,
|
|
162
|
-
level: record.level,
|
|
163
|
-
timestamp: record.timestamp,
|
|
164
|
-
text: record.text,
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
try {
|
|
169
|
-
log.info('[pipeline]', `start "${config.name}" run_id=${runId}`);
|
|
170
|
-
|
|
171
|
-
// File-only: dump the resolved pipeline shape + DAG topology for post-mortem.
|
|
172
|
-
log.section('Pipeline configuration');
|
|
173
|
-
log.quiet(`name: ${config.name}`);
|
|
174
|
-
log.quiet(`driver: ${config.driver ?? '(default: opencode)'}`);
|
|
175
|
-
log.quiet(`timeout: ${config.timeout ?? '(none)'}`);
|
|
176
|
-
log.quiet(`tracks: ${config.tracks.length}`);
|
|
177
|
-
log.quiet(`tasks (total): ${dag.nodes.size}`);
|
|
178
|
-
log.quiet(`plugins: ${(config.plugins ?? []).join(', ') || '(none)'}`);
|
|
179
|
-
log.quiet(
|
|
180
|
-
`hooks: ${config.hooks ? Object.keys(config.hooks).join(', ') || '(none)' : '(none)'}`,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
log.section('DAG topology');
|
|
184
|
-
for (const [id, node] of dag.nodes) {
|
|
185
|
-
const deps = node.dependsOn.length ? node.dependsOn.join(', ') : '(root)';
|
|
186
|
-
const kind = isPromptTaskConfig(node.task) ? 'ai' : 'cmd';
|
|
187
|
-
log.quiet(` • ${id} [${kind}] track=${node.track.id} deps=[${deps}]`);
|
|
188
|
-
}
|
|
189
|
-
log.quiet('');
|
|
190
|
-
|
|
191
|
-
// Per-run state container. Constructed before the pipeline_start hook
|
|
192
|
-
// so the early-return path (blocked pipeline) can call freezeStates on
|
|
193
|
-
// the populated idle-state map. The constructor has no side effects —
|
|
194
|
-
// no listeners installed, no events emitted.
|
|
195
|
-
const ctx = new RunContext({
|
|
196
|
-
runId,
|
|
197
|
-
dag,
|
|
198
|
-
config,
|
|
199
|
-
workDir,
|
|
200
|
-
pipelineInfo,
|
|
201
|
-
onEvent: options.onEvent,
|
|
202
|
-
runtime,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Pipeline start hook (gate). Runs BEFORE the engine emits run_start so
|
|
206
|
-
// a blocked pipeline produces zero wire events (the server treats the
|
|
207
|
-
// thrown error as run_error). Hosts get a rich error message; nothing
|
|
208
|
-
// is ever half-broadcast.
|
|
209
|
-
const startHook = await executeHook(
|
|
210
|
-
config.hooks,
|
|
211
|
-
'pipeline_start',
|
|
212
|
-
buildPipelineStartContext(pipelineInfo),
|
|
213
|
-
workDir,
|
|
214
|
-
);
|
|
215
|
-
if (!startHook.allowed) {
|
|
216
|
-
console.error(`Pipeline blocked by pipeline_start hook (exit code ${startHook.exitCode})`);
|
|
217
|
-
await executeHook(
|
|
218
|
-
config.hooks,
|
|
219
|
-
'pipeline_error',
|
|
220
|
-
buildPipelineErrorContext(pipelineInfo, 'pipeline_blocked', 'pipeline_blocked'),
|
|
221
|
-
workDir,
|
|
222
|
-
);
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
runId,
|
|
226
|
-
logPath: log.path,
|
|
227
|
-
summary: {
|
|
228
|
-
total: dag.nodes.size,
|
|
229
|
-
success: 0,
|
|
230
|
-
failed: 0,
|
|
231
|
-
skipped: 0,
|
|
232
|
-
timeout: 0,
|
|
233
|
-
blocked: 0,
|
|
234
|
-
},
|
|
235
|
-
states: freezeStates(ctx.states),
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Pipeline approved — transition all tasks to waiting.
|
|
240
|
-
for (const [, state] of ctx.states) {
|
|
241
|
-
state.status = 'waiting';
|
|
242
|
-
}
|
|
243
|
-
// Emit run_start with a wire-shape snapshot so SSE subscribers can
|
|
244
|
-
// initialize their task maps on the same event stream that carries
|
|
245
|
-
// updates. No separate "server pre-broadcasts run_start" ceremony —
|
|
246
|
-
// the engine owns the lifecycle boundary.
|
|
247
|
-
const runStartTasks: RunTaskState[] = [];
|
|
248
|
-
for (const [id, node] of dag.nodes) {
|
|
249
|
-
const s = ctx.states.get(id)!;
|
|
250
|
-
runStartTasks.push(toRunTaskState(id, node.track.id, node.task.name ?? id, s));
|
|
251
|
-
}
|
|
252
|
-
ctx.emit({ type: 'run_start', runId, tasks: runStartTasks });
|
|
253
|
-
|
|
254
|
-
// Pipeline timeout. `ctx.abortReason` carries the concrete cause
|
|
255
|
-
// (timeout / stop_all / external) through to run_end and the
|
|
256
|
-
// pipeline_error hook so downstream consumers can distinguish them
|
|
257
|
-
// without scraping message strings.
|
|
258
|
-
const pipelineTimeoutMs = config.timeout ? parseDuration(config.timeout) : 0;
|
|
259
|
-
let pipelineTimer: ReturnType<typeof setTimeout> | null = null;
|
|
260
|
-
|
|
261
|
-
if (pipelineTimeoutMs > 0) {
|
|
262
|
-
pipelineTimer = setTimeout(() => {
|
|
263
|
-
if (ctx.abortReason === null) ctx.abortReason = 'timeout';
|
|
264
|
-
ctx.abortController.abort();
|
|
265
|
-
}, pipelineTimeoutMs);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// When the pipeline is aborted (timeout, stop_all, external), drain
|
|
269
|
-
// all pending approvals so waiting triggers unblock immediately.
|
|
270
|
-
ctx.abortController.signal.addEventListener('abort', () => {
|
|
271
|
-
approvalGateway.abortAll('pipeline aborted');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Wire external cancel signal into the internal abort controller.
|
|
275
|
-
const externalAbortHandler = () => {
|
|
276
|
-
if (ctx.abortReason === null) ctx.abortReason = 'external';
|
|
277
|
-
ctx.abortController.abort();
|
|
278
|
-
};
|
|
279
|
-
if (options.signal) {
|
|
280
|
-
if (options.signal.aborted) {
|
|
281
|
-
externalAbortHandler();
|
|
282
|
-
} else {
|
|
283
|
-
options.signal.addEventListener('abort', externalAbortHandler, { once: true });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Bridge approval gateway events onto the wire stream so hosts (editor
|
|
288
|
-
// server, CLI adapters) see approvals on the same channel as task
|
|
289
|
-
// updates. The server no longer needs its own gateway subscription.
|
|
290
|
-
const unsubscribeApprovals = approvalGateway.subscribe((ev) => {
|
|
291
|
-
if (ev.type === 'requested') {
|
|
292
|
-
ctx.emit({
|
|
293
|
-
type: 'approval_request',
|
|
294
|
-
runId,
|
|
295
|
-
request: {
|
|
296
|
-
id: ev.request.id,
|
|
297
|
-
taskId: ev.request.taskId,
|
|
298
|
-
trackId: ev.request.trackId,
|
|
299
|
-
message: ev.request.message,
|
|
300
|
-
createdAt: ev.request.createdAt,
|
|
301
|
-
timeoutMs: ev.request.timeoutMs,
|
|
302
|
-
metadata: ev.request.metadata,
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (ev.type === 'resolved' || ev.type === 'expired' || ev.type === 'aborted') {
|
|
308
|
-
const outcome =
|
|
309
|
-
ev.type === 'resolved'
|
|
310
|
-
? ev.decision.outcome
|
|
311
|
-
: ev.type === 'expired'
|
|
312
|
-
? 'timeout'
|
|
313
|
-
: 'aborted';
|
|
314
|
-
ctx.emit({
|
|
315
|
-
type: 'approval_resolved',
|
|
316
|
-
runId,
|
|
317
|
-
requestId: ev.request.id,
|
|
318
|
-
outcome,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// ── Process a single task ──
|
|
324
|
-
// ── Event loop ──
|
|
325
|
-
// Each task is launched as soon as ALL its deps reach a terminal state.
|
|
326
|
-
// We track in-flight tasks in `running` so a task completing mid-batch
|
|
327
|
-
// immediately unblocks its dependents without waiting for sibling tasks.
|
|
328
|
-
const running = new Map<string, Promise<void>>();
|
|
329
|
-
|
|
330
|
-
try {
|
|
331
|
-
while (ctx.abortReason === null) {
|
|
332
|
-
// Launch every task whose deps are all terminal and that isn't already in-flight
|
|
333
|
-
for (const id of findLaunchableTasks(ctx, new Set(running.keys()))) {
|
|
334
|
-
const p = executeTask({
|
|
335
|
-
taskId: id,
|
|
336
|
-
ctx,
|
|
337
|
-
registry,
|
|
338
|
-
log,
|
|
339
|
-
approvalGateway,
|
|
340
|
-
}).finally(() => running.delete(id));
|
|
341
|
-
running.set(id, p);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// All tasks terminal — done
|
|
345
|
-
if (allTasksTerminal(ctx)) break;
|
|
346
|
-
|
|
347
|
-
if (running.size === 0) {
|
|
348
|
-
// Nothing in-flight but non-terminal tasks exist (e.g. trigger-wait states
|
|
349
|
-
// that processTask hasn't been called for yet). Poll briefly.
|
|
350
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
351
|
-
} else {
|
|
352
|
-
// Wait for any one task to finish, then re-scan for new launchables.
|
|
353
|
-
await Promise.race(running.values());
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (ctx.abortReason !== null) {
|
|
358
|
-
// Wait for in-flight tasks to honour the abort signal before marking states.
|
|
359
|
-
if (running.size > 0) await Promise.allSettled(running.values());
|
|
360
|
-
// By the time allSettled resolves, processTask's try/finally has already
|
|
361
|
-
// set running tasks to success/failed/timeout. The only non-terminal
|
|
362
|
-
// statuses remaining here are waiting/idle tasks that were never started.
|
|
363
|
-
skipNonTerminalTasks(ctx);
|
|
364
|
-
}
|
|
365
|
-
} finally {
|
|
366
|
-
if (pipelineTimer) clearTimeout(pipelineTimer);
|
|
367
|
-
// Clean up the external abort signal listener to prevent dead references
|
|
368
|
-
// accumulating on long-lived shared AbortControllers.
|
|
369
|
-
if (options.signal) {
|
|
370
|
-
options.signal.removeEventListener('abort', externalAbortHandler);
|
|
371
|
-
}
|
|
372
|
-
// Safety net: drain any approvals still pending at shutdown (e.g. crash path).
|
|
373
|
-
if (approvalGateway.pending().length > 0) {
|
|
374
|
-
approvalGateway.abortAll('pipeline finished');
|
|
375
|
-
}
|
|
376
|
-
// Detach gateway → onEvent bridge so a long-lived gateway (host-supplied)
|
|
377
|
-
// doesn't keep firing into a dead run.
|
|
378
|
-
unsubscribeApprovals();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ── Summary ──
|
|
382
|
-
const summary = summarizeStates(ctx.states);
|
|
383
|
-
|
|
384
|
-
const finishedAt = nowISO();
|
|
385
|
-
const durationMs = new Date(finishedAt).getTime() - new Date(startedAt).getTime();
|
|
386
|
-
|
|
387
|
-
if (ctx.abortReason !== null) {
|
|
388
|
-
const reasonText =
|
|
389
|
-
ctx.abortReason === 'timeout'
|
|
390
|
-
? 'Pipeline timeout exceeded'
|
|
391
|
-
: ctx.abortReason === 'stop_all'
|
|
392
|
-
? 'Pipeline stopped (on_failure: stop_all)'
|
|
393
|
-
: 'Pipeline aborted by host';
|
|
394
|
-
await executeHook(
|
|
395
|
-
config.hooks,
|
|
396
|
-
'pipeline_error',
|
|
397
|
-
buildPipelineErrorContext(pipelineInfo, reasonText, undefined, ctx.abortReason),
|
|
398
|
-
workDir,
|
|
399
|
-
);
|
|
400
|
-
} else {
|
|
401
|
-
await executeHook(
|
|
402
|
-
config.hooks,
|
|
403
|
-
'pipeline_complete',
|
|
404
|
-
buildPipelineCompleteContext(
|
|
405
|
-
{ ...pipelineInfo, finished_at: finishedAt, duration_ms: durationMs },
|
|
406
|
-
summary,
|
|
407
|
-
),
|
|
408
|
-
workDir,
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const allSuccess =
|
|
413
|
-
ctx.abortReason === null &&
|
|
414
|
-
summary.failed === 0 &&
|
|
415
|
-
summary.timeout === 0 &&
|
|
416
|
-
summary.blocked === 0;
|
|
417
|
-
|
|
418
|
-
log.section('Pipeline summary');
|
|
419
|
-
log.quiet(
|
|
420
|
-
`status: ${ctx.abortReason !== null ? `aborted (${ctx.abortReason})` : 'completed'}`,
|
|
421
|
-
);
|
|
422
|
-
log.quiet(`duration: ${(durationMs / 1000).toFixed(1)}s`);
|
|
423
|
-
log.quiet(
|
|
424
|
-
`counts: total=${summary.total} success=${summary.success} ` +
|
|
425
|
-
`failed=${summary.failed} skipped=${summary.skipped} ` +
|
|
426
|
-
`timeout=${summary.timeout} blocked=${summary.blocked}`,
|
|
427
|
-
);
|
|
428
|
-
log.quiet('');
|
|
429
|
-
log.quiet('per-task:');
|
|
430
|
-
for (const [id, state] of ctx.states) {
|
|
431
|
-
const dur =
|
|
432
|
-
state.result?.durationMs != null ? `${(state.result.durationMs / 1000).toFixed(1)}s` : '-';
|
|
433
|
-
const exit = state.result?.exitCode ?? '-';
|
|
434
|
-
log.quiet(` ${state.status.padEnd(8)} ${id} (exit=${exit}, ${dur})`);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
log.info('[pipeline]', `completed "${config.name}"`);
|
|
438
|
-
log.info(
|
|
439
|
-
'[pipeline]',
|
|
440
|
-
`Total: ${summary.total} | Success: ${summary.success} | Failed: ${summary.failed} | Skipped: ${summary.skipped} | Timeout: ${summary.timeout} | Blocked: ${summary.blocked}`,
|
|
441
|
-
);
|
|
442
|
-
log.info('[pipeline]', `Duration: ${(durationMs / 1000).toFixed(1)}s`);
|
|
443
|
-
log.info('[pipeline]', `Log: ${log.path}`);
|
|
444
|
-
|
|
445
|
-
ctx.emit({ type: 'run_end', runId, success: allSuccess, abortReason: ctx.abortReason });
|
|
446
|
-
return { success: allSuccess, runId, logPath: log.path, summary, states: freezeStates(ctx.states) };
|
|
447
|
-
} finally {
|
|
448
|
-
// Close the persistent log file handle before pruning.
|
|
449
|
-
log.close();
|
|
450
|
-
// Prune old per-run log directories on every exit path (normal, blocked, or thrown).
|
|
451
|
-
// Exclude the current runId so a concurrent run cannot delete its own live directory.
|
|
452
|
-
if (maxLogRuns > 0) {
|
|
453
|
-
await pruneLogDirs(resolve(workDir, '.tagma', 'logs'), maxLogRuns, runId);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|