@tagma/sdk 0.7.4 → 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 +60 -53
- 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/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/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -4
- package/dist/schema.js.map +1 -1
- package/dist/triggers/file.d.ts.map +1 -1
- package/dist/triggers/file.js +1 -2
- 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 +11 -24
- package/dist/adapters/stdin-approval.d.ts +0 -2
- package/dist/adapters/stdin-approval.d.ts.map +0 -1
- package/dist/adapters/stdin-approval.js +0 -2
- package/dist/adapters/stdin-approval.js.map +0 -1
- package/dist/adapters/websocket-approval.d.ts +0 -2
- package/dist/adapters/websocket-approval.d.ts.map +0 -1
- package/dist/adapters/websocket-approval.js +0 -2
- 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 -610
- 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 -3
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -2
- 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 -1
- package/src/adapters/websocket-approval.ts +0 -1
- package/src/approval.ts +0 -9
- 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 -291
- 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 -752
- 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 -182
- package/src/engine-ports.test.ts +0 -210
- package/src/engine-task-type.test.ts +0 -56
- package/src/engine.ts +0 -32
- package/src/hooks.ts +0 -193
- package/src/index.ts +0 -31
- package/src/logger.ts +0 -2
- package/src/middlewares/static-context.ts +0 -49
- package/src/package-split.test.ts +0 -15
- package/src/pipeline-definition.ts +0 -5
- package/src/pipeline-runner.test.ts +0 -144
- package/src/pipeline-runner.ts +0 -194
- package/src/plugin-registry.test.ts +0 -448
- 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 -7
- package/src/runner.test.ts +0 -142
- package/src/runner.ts +0 -1
- package/src/runtime/adapters/stdin-approval.ts +0 -1
- package/src/runtime/adapters/websocket-approval.ts +0 -1
- package/src/runtime/bun-process-runner.ts +0 -1
- package/src/runtime-adapters.test.ts +0 -10
- package/src/runtime.ts +0 -12
- 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 -317
- package/src/tagma.ts +0 -67
- package/src/task-ref.test.ts +0 -401
- package/src/task-ref.ts +0 -121
- package/src/triggers/file.test.ts +0 -79
- package/src/triggers/file.ts +0 -131
- 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/tagma.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PluginRegistry,
|
|
3
|
-
runPipeline,
|
|
4
|
-
type EngineResult,
|
|
5
|
-
type RunPipelineOptions,
|
|
6
|
-
} from '@tagma/core';
|
|
7
|
-
import { bootstrapBuiltins } from './bootstrap';
|
|
8
|
-
import { validateConfig } from './schema';
|
|
9
|
-
import { bunRuntime } from '@tagma/runtime-bun';
|
|
10
|
-
import type { TagmaRuntime } from '@tagma/core';
|
|
11
|
-
import type { PipelineConfig, TagmaPlugin } from './types';
|
|
12
|
-
|
|
13
|
-
export interface CreateTagmaOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Registry used by this SDK instance. Omit to create an isolated registry.
|
|
16
|
-
*/
|
|
17
|
-
readonly registry?: PluginRegistry;
|
|
18
|
-
/**
|
|
19
|
-
* Register built-in drivers/triggers/completions/middlewares into the
|
|
20
|
-
* instance registry. Defaults to true.
|
|
21
|
-
*/
|
|
22
|
-
readonly builtins?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Package-level capability plugins to register into this SDK instance.
|
|
25
|
-
*/
|
|
26
|
-
readonly plugins?: readonly TagmaPlugin[];
|
|
27
|
-
/**
|
|
28
|
-
* Runtime implementation used for command and driver process execution.
|
|
29
|
-
* Defaults to the SDK's Bun runtime.
|
|
30
|
-
*/
|
|
31
|
-
readonly runtime?: TagmaRuntime;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface TagmaRunOptions extends Omit<RunPipelineOptions, 'registry' | 'runtime'> {
|
|
35
|
-
readonly cwd: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Tagma {
|
|
39
|
-
readonly registry: PluginRegistry;
|
|
40
|
-
run(config: PipelineConfig, options: TagmaRunOptions): Promise<EngineResult>;
|
|
41
|
-
validate(config: PipelineConfig): readonly string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createTagma(options: CreateTagmaOptions = {}): Tagma {
|
|
45
|
-
const registry = options.registry ?? new PluginRegistry();
|
|
46
|
-
const runtime = options.runtime ?? bunRuntime();
|
|
47
|
-
if (options.builtins !== false) {
|
|
48
|
-
bootstrapBuiltins(registry);
|
|
49
|
-
}
|
|
50
|
-
for (const plugin of options.plugins ?? []) {
|
|
51
|
-
registry.registerTagmaPlugin(plugin);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
registry,
|
|
56
|
-
run(config, { cwd, ...runOptions }) {
|
|
57
|
-
return runPipeline(config, cwd, {
|
|
58
|
-
...runOptions,
|
|
59
|
-
registry,
|
|
60
|
-
runtime,
|
|
61
|
-
});
|
|
62
|
-
},
|
|
63
|
-
validate(config) {
|
|
64
|
-
return validateConfig(config);
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
}
|
package/src/task-ref.test.ts
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import type { PipelineConfig, RawPipelineConfig } from './types';
|
|
3
|
-
import {
|
|
4
|
-
TASK_ID_RE,
|
|
5
|
-
isValidTaskId,
|
|
6
|
-
qualifyTaskId,
|
|
7
|
-
isQualifiedRef,
|
|
8
|
-
buildTaskIndex,
|
|
9
|
-
resolveTaskRef,
|
|
10
|
-
AMBIGUOUS,
|
|
11
|
-
} from './task-ref';
|
|
12
|
-
import { buildDag, buildRawDag } from './dag';
|
|
13
|
-
import { validateRaw } from './validate-raw';
|
|
14
|
-
|
|
15
|
-
// ═══ Low-level helpers ═══
|
|
16
|
-
|
|
17
|
-
describe('isValidTaskId', () => {
|
|
18
|
-
test('accepts letter-led ids with letters, digits, underscores, hyphens', () => {
|
|
19
|
-
for (const id of ['a', 'A', '_', 'task_1', 'Task-2', '_private', 'a_b-c_1']) {
|
|
20
|
-
expect(isValidTaskId(id)).toBe(true);
|
|
21
|
-
expect(TASK_ID_RE.test(id)).toBe(true);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('rejects empty, digit-led, dot-bearing, and whitespace forms', () => {
|
|
26
|
-
for (const id of ['', '1task', 'a.b', 'foo bar', '-leading', 'has/slash', 'dot.']) {
|
|
27
|
-
expect(isValidTaskId(id)).toBe(false);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('rejects non-string values', () => {
|
|
32
|
-
expect(isValidTaskId(null as unknown as string)).toBe(false);
|
|
33
|
-
expect(isValidTaskId(undefined as unknown as string)).toBe(false);
|
|
34
|
-
expect(isValidTaskId(42 as unknown as string)).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('qualifyTaskId + isQualifiedRef', () => {
|
|
39
|
-
test('qualifyTaskId joins with dot; isQualifiedRef detects dotted form', () => {
|
|
40
|
-
expect(qualifyTaskId('alpha', 'review')).toBe('alpha.review');
|
|
41
|
-
expect(isQualifiedRef('alpha.review')).toBe(true);
|
|
42
|
-
expect(isQualifiedRef('review')).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// ═══ Index build ═══
|
|
47
|
-
|
|
48
|
-
describe('buildTaskIndex', () => {
|
|
49
|
-
test('collects all qualified ids and unique bare ids', () => {
|
|
50
|
-
const cfg: RawPipelineConfig = {
|
|
51
|
-
name: 'T',
|
|
52
|
-
tracks: [
|
|
53
|
-
{ id: 'alpha', name: 'A', tasks: [{ id: 'plan', prompt: 'p' }] },
|
|
54
|
-
{ id: 'beta', name: 'B', tasks: [{ id: 'ship', prompt: 'p' }] },
|
|
55
|
-
],
|
|
56
|
-
};
|
|
57
|
-
const idx = buildTaskIndex(cfg);
|
|
58
|
-
expect(idx.allQualified.has('alpha.plan')).toBe(true);
|
|
59
|
-
expect(idx.allQualified.has('beta.ship')).toBe(true);
|
|
60
|
-
expect(idx.bareToQualified.get('plan')).toBe('alpha.plan');
|
|
61
|
-
expect(idx.bareToQualified.get('ship')).toBe('beta.ship');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('marks bare ids shared across tracks as ambiguous', () => {
|
|
65
|
-
const cfg: RawPipelineConfig = {
|
|
66
|
-
name: 'T',
|
|
67
|
-
tracks: [
|
|
68
|
-
{ id: 'alpha', name: 'A', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
69
|
-
{ id: 'beta', name: 'B', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
const idx = buildTaskIndex(cfg);
|
|
73
|
-
expect(idx.bareToQualified.get('review')).toBe(AMBIGUOUS);
|
|
74
|
-
expect(idx.allQualified.has('alpha.review')).toBe(true);
|
|
75
|
-
expect(idx.allQualified.has('beta.review')).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('tolerates tracks/tasks missing ids (editor in-progress state)', () => {
|
|
79
|
-
const cfg = {
|
|
80
|
-
name: 'T',
|
|
81
|
-
tracks: [
|
|
82
|
-
{ id: '', name: 'half-typed', tasks: [{ id: 'x', prompt: 'p' }] },
|
|
83
|
-
{ id: 'ok', name: 'OK', tasks: [{ id: '', prompt: 'p' }, { id: 'y', prompt: 'p' }] },
|
|
84
|
-
],
|
|
85
|
-
} as unknown as RawPipelineConfig;
|
|
86
|
-
const idx = buildTaskIndex(cfg);
|
|
87
|
-
expect(idx.allQualified.size).toBe(1);
|
|
88
|
-
expect(idx.allQualified.has('ok.y')).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// ═══ Ref resolution ═══
|
|
93
|
-
|
|
94
|
-
describe('resolveTaskRef', () => {
|
|
95
|
-
const cfg: RawPipelineConfig = {
|
|
96
|
-
name: 'T',
|
|
97
|
-
tracks: [
|
|
98
|
-
{
|
|
99
|
-
id: 'alpha',
|
|
100
|
-
name: 'A',
|
|
101
|
-
tasks: [
|
|
102
|
-
{ id: 'plan', prompt: 'p' },
|
|
103
|
-
{ id: 'review', prompt: 'p' },
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
id: 'beta',
|
|
108
|
-
name: 'B',
|
|
109
|
-
tasks: [
|
|
110
|
-
{ id: 'review', prompt: 'p' },
|
|
111
|
-
{ id: 'ship', prompt: 'p' },
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
],
|
|
115
|
-
};
|
|
116
|
-
const idx = buildTaskIndex(cfg);
|
|
117
|
-
|
|
118
|
-
test('fully qualified ref resolves when it exists', () => {
|
|
119
|
-
expect(resolveTaskRef('alpha.plan', 'beta', idx)).toEqual({
|
|
120
|
-
kind: 'resolved',
|
|
121
|
-
qid: 'alpha.plan',
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('fully qualified ref that does not exist is not_found', () => {
|
|
126
|
-
expect(resolveTaskRef('alpha.ghost', 'beta', idx)).toEqual({
|
|
127
|
-
kind: 'not_found',
|
|
128
|
-
ref: 'alpha.ghost',
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('bare ref prefers same-track shorthand', () => {
|
|
133
|
-
// "review" exists in both tracks — from beta's perspective, the same-
|
|
134
|
-
// track shadow must win over the cross-track ambiguous pool.
|
|
135
|
-
expect(resolveTaskRef('review', 'beta', idx)).toEqual({
|
|
136
|
-
kind: 'resolved',
|
|
137
|
-
qid: 'beta.review',
|
|
138
|
-
});
|
|
139
|
-
expect(resolveTaskRef('review', 'alpha', idx)).toEqual({
|
|
140
|
-
kind: 'resolved',
|
|
141
|
-
qid: 'alpha.review',
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('bare ref not in current track is ambiguous when multiple tracks have it', () => {
|
|
146
|
-
const twoForeign: RawPipelineConfig = {
|
|
147
|
-
name: 'T',
|
|
148
|
-
tracks: [
|
|
149
|
-
{ id: 'a', name: 'A', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
150
|
-
{ id: 'b', name: 'B', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
151
|
-
{ id: 'c', name: 'C', tasks: [{ id: 'other', prompt: 'p' }] },
|
|
152
|
-
],
|
|
153
|
-
};
|
|
154
|
-
const idx2 = buildTaskIndex(twoForeign);
|
|
155
|
-
expect(resolveTaskRef('review', 'c', idx2)).toEqual({ kind: 'ambiguous', ref: 'review' });
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('bare ref unique in the pool resolves cross-track', () => {
|
|
159
|
-
expect(resolveTaskRef('ship', 'alpha', idx)).toEqual({
|
|
160
|
-
kind: 'resolved',
|
|
161
|
-
qid: 'beta.ship',
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('bare ref nobody has is not_found', () => {
|
|
166
|
-
expect(resolveTaskRef('nowhere', 'alpha', idx)).toEqual({
|
|
167
|
-
kind: 'not_found',
|
|
168
|
-
ref: 'nowhere',
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// ═══ Regression: bug #2 — same bare task id in multiple tracks ═══
|
|
174
|
-
|
|
175
|
-
describe('regression: continue_from across same-named tasks (bug #2)', () => {
|
|
176
|
-
test('bare continue_from resolves via same-track shadow — qualified id handed downstream', () => {
|
|
177
|
-
// Two tracks, each with a "review" task and a follower that continues
|
|
178
|
-
// from it. The follower MUST bind to its own track's review via the
|
|
179
|
-
// same-track shorthand, not to the other track's review.
|
|
180
|
-
const resolved: PipelineConfig = {
|
|
181
|
-
name: 'Same-Bare',
|
|
182
|
-
tracks: [
|
|
183
|
-
{
|
|
184
|
-
id: 'alpha',
|
|
185
|
-
name: 'Alpha',
|
|
186
|
-
tasks: [
|
|
187
|
-
{ id: 'review', name: 'Review', prompt: 'do A' },
|
|
188
|
-
{
|
|
189
|
-
id: 'ship',
|
|
190
|
-
name: 'Ship',
|
|
191
|
-
prompt: 'ship A',
|
|
192
|
-
depends_on: ['review'],
|
|
193
|
-
continue_from: 'review',
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
id: 'beta',
|
|
199
|
-
name: 'Beta',
|
|
200
|
-
tasks: [
|
|
201
|
-
{ id: 'review', name: 'Review', prompt: 'do B' },
|
|
202
|
-
{
|
|
203
|
-
id: 'ship',
|
|
204
|
-
name: 'Ship',
|
|
205
|
-
prompt: 'ship B',
|
|
206
|
-
depends_on: ['review'],
|
|
207
|
-
continue_from: 'review',
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
};
|
|
213
|
-
const dag = buildDag(resolved);
|
|
214
|
-
const alphaShip = dag.nodes.get('alpha.ship')!;
|
|
215
|
-
const betaShip = dag.nodes.get('beta.ship')!;
|
|
216
|
-
expect(alphaShip.resolvedContinueFrom).toBe('alpha.review');
|
|
217
|
-
expect(betaShip.resolvedContinueFrom).toBe('beta.review');
|
|
218
|
-
// And the dep edges get qualified too, preventing engine map-key misses.
|
|
219
|
-
expect(alphaShip.dependsOn).toContain('alpha.review');
|
|
220
|
-
expect(betaShip.dependsOn).toContain('beta.review');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('bare continue_from pointing at a foreign ambiguous task throws', () => {
|
|
224
|
-
const resolved: PipelineConfig = {
|
|
225
|
-
name: 'Ambiguous',
|
|
226
|
-
tracks: [
|
|
227
|
-
{
|
|
228
|
-
id: 'alpha',
|
|
229
|
-
name: 'Alpha',
|
|
230
|
-
tasks: [
|
|
231
|
-
{ id: 'review', name: 'Review', prompt: 'p' },
|
|
232
|
-
// ship has no local "review" so bare "review" is ambiguous.
|
|
233
|
-
{ id: 'filler', name: 'Filler', prompt: 'p' },
|
|
234
|
-
],
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
id: 'beta',
|
|
238
|
-
name: 'Beta',
|
|
239
|
-
tasks: [{ id: 'review', name: 'Review', prompt: 'p' }],
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
id: 'gamma',
|
|
243
|
-
name: 'Gamma',
|
|
244
|
-
tasks: [
|
|
245
|
-
{
|
|
246
|
-
id: 'ship',
|
|
247
|
-
name: 'Ship',
|
|
248
|
-
prompt: 'ship',
|
|
249
|
-
continue_from: 'review',
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
};
|
|
255
|
-
expect(() => buildDag(resolved)).toThrow(/ambiguous/i);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test('qualified continue_from always wins — no same-track-shadow risk', () => {
|
|
259
|
-
const resolved: PipelineConfig = {
|
|
260
|
-
name: 'Qualified',
|
|
261
|
-
tracks: [
|
|
262
|
-
{
|
|
263
|
-
id: 'alpha',
|
|
264
|
-
name: 'Alpha',
|
|
265
|
-
tasks: [
|
|
266
|
-
{ id: 'plan', name: 'Plan', prompt: 'p' },
|
|
267
|
-
{
|
|
268
|
-
id: 'plan_v2',
|
|
269
|
-
name: 'Plan v2',
|
|
270
|
-
prompt: 'p2',
|
|
271
|
-
continue_from: 'plan',
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
id: 'beta',
|
|
277
|
-
name: 'Beta',
|
|
278
|
-
tasks: [
|
|
279
|
-
{ id: 'plan', name: 'Plan B', prompt: 'p' },
|
|
280
|
-
{
|
|
281
|
-
id: 'cross',
|
|
282
|
-
name: 'Cross',
|
|
283
|
-
prompt: 'x',
|
|
284
|
-
continue_from: 'alpha.plan',
|
|
285
|
-
},
|
|
286
|
-
],
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
};
|
|
290
|
-
const dag = buildDag(resolved);
|
|
291
|
-
expect(dag.nodes.get('alpha.plan_v2')!.resolvedContinueFrom).toBe('alpha.plan');
|
|
292
|
-
expect(dag.nodes.get('beta.cross')!.resolvedContinueFrom).toBe('alpha.plan');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// ═══ Regression: bug #10 — editor-generated ids always pass validate-raw ═══
|
|
297
|
-
|
|
298
|
-
describe('regression: TASK_ID_RE is the single source of truth (bug #10)', () => {
|
|
299
|
-
test('validateRaw rejects exactly what isValidTaskId rejects', () => {
|
|
300
|
-
// Any string the helper says is invalid must be flagged by validateRaw
|
|
301
|
-
// as an "invalid characters" error — proving they read from the same
|
|
302
|
-
// regex rather than two drifted copies.
|
|
303
|
-
const invalids = ['1bad', 'has.dot', 'with space', '-leading', 'q?mark', ''];
|
|
304
|
-
for (const badId of invalids) {
|
|
305
|
-
const cfg: RawPipelineConfig = {
|
|
306
|
-
name: 'T',
|
|
307
|
-
tracks: [
|
|
308
|
-
{
|
|
309
|
-
id: 'ok',
|
|
310
|
-
name: 'OK',
|
|
311
|
-
tasks: [{ id: badId, prompt: 'p' }],
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
};
|
|
315
|
-
const errs = validateRaw(cfg);
|
|
316
|
-
expect(errs.length).toBeGreaterThan(0);
|
|
317
|
-
expect(isValidTaskId(badId)).toBe(false);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// ═══ Regression: buildDag topo sort is deterministic (bug #13) ═══
|
|
323
|
-
|
|
324
|
-
describe('regression: buildDag topo sort is deterministic', () => {
|
|
325
|
-
const base = (trackOrder: readonly string[]): PipelineConfig => ({
|
|
326
|
-
name: 'Determinism',
|
|
327
|
-
tracks: trackOrder.map((id) => ({
|
|
328
|
-
id,
|
|
329
|
-
name: id,
|
|
330
|
-
tasks: [
|
|
331
|
-
{ id: 'a', name: 'A', prompt: 'p' },
|
|
332
|
-
{ id: 'b', name: 'B', prompt: 'p', depends_on: ['a'] },
|
|
333
|
-
],
|
|
334
|
-
})),
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test('parallel tasks with equal depth sort by qid, independent of YAML order', () => {
|
|
338
|
-
const forward = buildDag(base(['alpha', 'beta', 'gamma']));
|
|
339
|
-
const reversed = buildDag(base(['gamma', 'beta', 'alpha']));
|
|
340
|
-
expect(forward.sorted).toEqual(reversed.sorted);
|
|
341
|
-
// And the actual order is alphabetical by qid — every "a" before every "b".
|
|
342
|
-
expect(forward.sorted).toEqual([
|
|
343
|
-
'alpha.a',
|
|
344
|
-
'beta.a',
|
|
345
|
-
'gamma.a',
|
|
346
|
-
'alpha.b',
|
|
347
|
-
'beta.b',
|
|
348
|
-
'gamma.b',
|
|
349
|
-
]);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
test('diamond dependency still produces a unique sorted order', () => {
|
|
353
|
-
// root -> left, right -> join
|
|
354
|
-
const cfg: PipelineConfig = {
|
|
355
|
-
name: 'Diamond',
|
|
356
|
-
tracks: [
|
|
357
|
-
{
|
|
358
|
-
id: 't',
|
|
359
|
-
name: 't',
|
|
360
|
-
tasks: [
|
|
361
|
-
{ id: 'root', name: 'r', prompt: 'p' },
|
|
362
|
-
{ id: 'left', name: 'l', prompt: 'p', depends_on: ['root'] },
|
|
363
|
-
{ id: 'right', name: 'r', prompt: 'p', depends_on: ['root'] },
|
|
364
|
-
{ id: 'join', name: 'j', prompt: 'p', depends_on: ['left', 'right'] },
|
|
365
|
-
],
|
|
366
|
-
},
|
|
367
|
-
],
|
|
368
|
-
};
|
|
369
|
-
const first = buildDag(cfg).sorted;
|
|
370
|
-
const second = buildDag(cfg).sorted;
|
|
371
|
-
expect(first).toEqual(second);
|
|
372
|
-
// root first, join last; left/right in alphabetical order between.
|
|
373
|
-
expect(first[0]).toBe('t.root');
|
|
374
|
-
expect(first[first.length - 1]).toBe('t.join');
|
|
375
|
-
expect(first.indexOf('t.left')).toBeLessThan(first.indexOf('t.right'));
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// ═══ Regression: buildRawDag stays lenient (editor real-time view) ═══
|
|
380
|
-
|
|
381
|
-
describe('buildRawDag tolerates unresolved refs', () => {
|
|
382
|
-
test('ambiguous bare continue_from is silently skipped (no edge, no throw)', () => {
|
|
383
|
-
const cfg: RawPipelineConfig = {
|
|
384
|
-
name: 'T',
|
|
385
|
-
tracks: [
|
|
386
|
-
{ id: 'a', name: 'A', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
387
|
-
{ id: 'b', name: 'B', tasks: [{ id: 'review', prompt: 'p' }] },
|
|
388
|
-
{
|
|
389
|
-
id: 'c',
|
|
390
|
-
name: 'C',
|
|
391
|
-
tasks: [{ id: 'use', prompt: 'p', continue_from: 'review' }],
|
|
392
|
-
},
|
|
393
|
-
],
|
|
394
|
-
};
|
|
395
|
-
const raw = buildRawDag(cfg);
|
|
396
|
-
expect(raw.nodes.size).toBe(3);
|
|
397
|
-
// No edge for the ambiguous ref — the editor panel should prompt the
|
|
398
|
-
// user to qualify it instead of silently linking to the wrong track.
|
|
399
|
-
expect(raw.edges.find((e) => e.to === 'c.use')).toBeUndefined();
|
|
400
|
-
});
|
|
401
|
-
});
|
package/src/task-ref.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// ═══ Task reference resolution — single source of truth ═══
|
|
2
|
-
//
|
|
3
|
-
// Before this module existed, four sites each carried their own copy of the
|
|
4
|
-
// "what is a valid id" + "how do I resolve a bare / same-track-shorthand /
|
|
5
|
-
// fully qualified ref" logic:
|
|
6
|
-
//
|
|
7
|
-
// - dag.ts/buildDag (threw on unresolved, threw on ambiguous)
|
|
8
|
-
// - dag.ts/buildRawDag (silently skipped unresolved and ambiguous)
|
|
9
|
-
// - validate-raw.ts (reported both as errors, with different wording)
|
|
10
|
-
// - engine.ts/resolveRefInDag (returned null on ambiguous)
|
|
11
|
-
//
|
|
12
|
-
// In addition, the editor shipped its own regex for id validation in
|
|
13
|
-
// `shared/config-id.ts` and a test-local copy in `config-id-generation.test.ts`,
|
|
14
|
-
// creating multiple places where the character set could drift from the
|
|
15
|
-
// validator. Bugs observed downstream (silent context loss when two tracks
|
|
16
|
-
// happened to share a bare task name; editor-generated ids occasionally
|
|
17
|
-
// failing SDK validate-raw) all traced back to this duplication.
|
|
18
|
-
//
|
|
19
|
-
// Callers now build a TaskIndex once and use `resolveTaskRef` to classify
|
|
20
|
-
// each reference, then decide themselves whether to throw / warn / skip —
|
|
21
|
-
// instead of re-implementing the index build and the lookup logic.
|
|
22
|
-
|
|
23
|
-
import type { PipelineConfig, RawPipelineConfig } from './types';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* D8: task and track ids must match this pattern. No dots: the `.` is the
|
|
27
|
-
* qualified-id separator ("trackId.taskId"), so allowing it inside either
|
|
28
|
-
* part would make qid parsing ambiguous and break every resolver below.
|
|
29
|
-
*/
|
|
30
|
-
export const TASK_ID_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
31
|
-
|
|
32
|
-
export function isValidTaskId(id: unknown): id is string {
|
|
33
|
-
return typeof id === 'string' && TASK_ID_RE.test(id);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Canonical qualified form used throughout the engine. */
|
|
37
|
-
export function qualifyTaskId(trackId: string, taskId: string): string {
|
|
38
|
-
return `${trackId}.${taskId}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Does the reference already include a track prefix? */
|
|
42
|
-
export function isQualifiedRef(ref: string): boolean {
|
|
43
|
-
return ref.includes('.');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Sentinel stored in `TaskIndex.bareToQualified` when a bare task id is
|
|
48
|
-
* shared by more than one track, making it unresolvable without a prefix.
|
|
49
|
-
* Exposed so callers that want to inspect the index directly know what to
|
|
50
|
-
* look for — but prefer `resolveTaskRef` which returns a typed `kind`.
|
|
51
|
-
*/
|
|
52
|
-
export const AMBIGUOUS = '__ambiguous__';
|
|
53
|
-
|
|
54
|
-
export interface TaskIndex {
|
|
55
|
-
/** All fully-qualified ids ("trackId.taskId") present in the config. */
|
|
56
|
-
readonly allQualified: ReadonlySet<string>;
|
|
57
|
-
/** bare taskId → qid, or the {@link AMBIGUOUS} sentinel. */
|
|
58
|
-
readonly bareToQualified: ReadonlyMap<string, string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Build the index used by {@link resolveTaskRef}. Tolerant of partially
|
|
63
|
-
* malformed configs: tracks or tasks missing an `id` are skipped so the
|
|
64
|
-
* editor can call this during real-time validation on in-progress edits.
|
|
65
|
-
*/
|
|
66
|
-
export function buildTaskIndex(config: RawPipelineConfig | PipelineConfig): TaskIndex {
|
|
67
|
-
const allQualified = new Set<string>();
|
|
68
|
-
const bareToQualified = new Map<string, string>();
|
|
69
|
-
for (const track of config.tracks ?? []) {
|
|
70
|
-
if (!track?.id) continue;
|
|
71
|
-
if (!Array.isArray(track.tasks)) continue;
|
|
72
|
-
for (const task of track.tasks ?? []) {
|
|
73
|
-
if (!task?.id) continue;
|
|
74
|
-
const qid = qualifyTaskId(track.id, task.id);
|
|
75
|
-
allQualified.add(qid);
|
|
76
|
-
if (bareToQualified.has(task.id)) {
|
|
77
|
-
bareToQualified.set(task.id, AMBIGUOUS);
|
|
78
|
-
} else {
|
|
79
|
-
bareToQualified.set(task.id, qid);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { allQualified, bareToQualified };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type RefResolution =
|
|
87
|
-
| { readonly kind: 'resolved'; readonly qid: string }
|
|
88
|
-
| { readonly kind: 'ambiguous'; readonly ref: string }
|
|
89
|
-
| { readonly kind: 'not_found'; readonly ref: string };
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Resolve a dependency / continue_from reference to a canonical qid.
|
|
93
|
-
*
|
|
94
|
-
* 1. If the ref already contains a `.`, treat it as fully qualified —
|
|
95
|
-
* return `resolved` when the qid exists, `not_found` otherwise.
|
|
96
|
-
* 2. Otherwise, prefer the same-track shorthand (`fromTrackId.ref`).
|
|
97
|
-
* 3. Fall back to a global bare lookup. Returns `ambiguous` when more
|
|
98
|
-
* than one track has a task with that bare name.
|
|
99
|
-
*
|
|
100
|
-
* Callers decide the policy: `buildDag` throws on non-resolved, `buildRawDag`
|
|
101
|
-
* skips silently, `validateRaw` emits a structured ValidationError.
|
|
102
|
-
*/
|
|
103
|
-
export function resolveTaskRef(
|
|
104
|
-
ref: string,
|
|
105
|
-
fromTrackId: string,
|
|
106
|
-
index: TaskIndex,
|
|
107
|
-
): RefResolution {
|
|
108
|
-
if (isQualifiedRef(ref)) {
|
|
109
|
-
return index.allQualified.has(ref)
|
|
110
|
-
? { kind: 'resolved', qid: ref }
|
|
111
|
-
: { kind: 'not_found', ref };
|
|
112
|
-
}
|
|
113
|
-
const sameTrack = qualifyTaskId(fromTrackId, ref);
|
|
114
|
-
if (index.allQualified.has(sameTrack)) {
|
|
115
|
-
return { kind: 'resolved', qid: sameTrack };
|
|
116
|
-
}
|
|
117
|
-
const global = index.bareToQualified.get(ref);
|
|
118
|
-
if (global === AMBIGUOUS) return { kind: 'ambiguous', ref };
|
|
119
|
-
if (global !== undefined) return { kind: 'resolved', qid: global };
|
|
120
|
-
return { kind: 'not_found', ref };
|
|
121
|
-
}
|
|
@@ -1,79 +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, resolve } from 'node:path';
|
|
5
|
-
import { InMemoryApprovalGateway } from '../approval';
|
|
6
|
-
import { FileTrigger } from './file';
|
|
7
|
-
import type { TagmaRuntime } from '../runtime';
|
|
8
|
-
|
|
9
|
-
function makeDir(prefix: string): string {
|
|
10
|
-
return mkdtempSync(join(tmpdir(), prefix));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe('FileTrigger runtime boundary', () => {
|
|
14
|
-
test('uses ctx.runtime watch APIs instead of direct chokidar or Bun file APIs', async () => {
|
|
15
|
-
const dir = makeDir('tagma-file-trigger-runtime-');
|
|
16
|
-
const calls: string[] = [];
|
|
17
|
-
const runtime = {
|
|
18
|
-
async runCommand() {
|
|
19
|
-
throw new Error('runCommand should not be called by FileTrigger');
|
|
20
|
-
},
|
|
21
|
-
async runSpawn() {
|
|
22
|
-
throw new Error('runSpawn should not be called by FileTrigger');
|
|
23
|
-
},
|
|
24
|
-
async ensureDir(path: string) {
|
|
25
|
-
calls.push(`ensure:${path}`);
|
|
26
|
-
},
|
|
27
|
-
async fileExists(path: string) {
|
|
28
|
-
calls.push(`exists:${path}`);
|
|
29
|
-
return false;
|
|
30
|
-
},
|
|
31
|
-
async *watch(path: string, options?: { cwd?: string }) {
|
|
32
|
-
calls.push(`watch:${path}:${options?.cwd ?? ''}`);
|
|
33
|
-
yield { type: 'ready', path: '' };
|
|
34
|
-
yield { type: 'add', path: 'target.txt' };
|
|
35
|
-
},
|
|
36
|
-
now() {
|
|
37
|
-
return new Date('2026-04-26T00:00:00.000Z');
|
|
38
|
-
},
|
|
39
|
-
sleep() {
|
|
40
|
-
return Promise.resolve();
|
|
41
|
-
},
|
|
42
|
-
logStore: {
|
|
43
|
-
openRunLog() {
|
|
44
|
-
throw new Error('logStore should not be called by FileTrigger');
|
|
45
|
-
},
|
|
46
|
-
taskOutputPath() {
|
|
47
|
-
throw new Error('logStore should not be called by FileTrigger');
|
|
48
|
-
},
|
|
49
|
-
logsDir() {
|
|
50
|
-
throw new Error('logStore should not be called by FileTrigger');
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
} as unknown as TagmaRuntime;
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
await expect(
|
|
57
|
-
FileTrigger.watch(
|
|
58
|
-
{ type: 'file', path: 'target.txt', timeout: '0.05s' },
|
|
59
|
-
{
|
|
60
|
-
taskId: 't.wait',
|
|
61
|
-
trackId: 't',
|
|
62
|
-
workDir: dir,
|
|
63
|
-
signal: new AbortController().signal,
|
|
64
|
-
approvalGateway: new InMemoryApprovalGateway(),
|
|
65
|
-
runtime,
|
|
66
|
-
} as never,
|
|
67
|
-
),
|
|
68
|
-
).resolves.toEqual({ path: resolve(dir, 'target.txt') });
|
|
69
|
-
|
|
70
|
-
expect(calls).toEqual([
|
|
71
|
-
`ensure:${dir}`,
|
|
72
|
-
`watch:${dir}:${dir}`,
|
|
73
|
-
`exists:${resolve(dir, 'target.txt')}`,
|
|
74
|
-
]);
|
|
75
|
-
} finally {
|
|
76
|
-
rmSync(dir, { recursive: true, force: true });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|