@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/hooks.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { HooksConfig, HookCommand, AbortReason } from './types';
|
|
2
|
-
import { shellArgs } from './utils';
|
|
3
|
-
|
|
4
|
-
type HookEvent =
|
|
5
|
-
| 'pipeline_start'
|
|
6
|
-
| 'task_start'
|
|
7
|
-
| 'task_success'
|
|
8
|
-
| 'task_failure'
|
|
9
|
-
| 'pipeline_complete'
|
|
10
|
-
| 'pipeline_error';
|
|
11
|
-
|
|
12
|
-
const GATE_HOOKS: ReadonlySet<HookEvent> = new Set(['pipeline_start', 'task_start']);
|
|
13
|
-
|
|
14
|
-
export interface HookResult {
|
|
15
|
-
readonly allowed: boolean; // for gate hooks: true = proceed, false = block
|
|
16
|
-
readonly exitCode: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function normalizeCommands(cmd: HookCommand | undefined): readonly string[] {
|
|
20
|
-
if (!cmd) return [];
|
|
21
|
-
if (typeof cmd === 'string') return [cmd];
|
|
22
|
-
return cmd;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const DEFAULT_HOOK_TIMEOUT_MS = 30_000;
|
|
26
|
-
|
|
27
|
-
async function runSingleHook(
|
|
28
|
-
command: string,
|
|
29
|
-
context: unknown,
|
|
30
|
-
cwd?: string,
|
|
31
|
-
signal?: AbortSignal,
|
|
32
|
-
timeoutMs: number = DEFAULT_HOOK_TIMEOUT_MS,
|
|
33
|
-
): Promise<number> {
|
|
34
|
-
const jsonInput = JSON.stringify(context, null, 2);
|
|
35
|
-
|
|
36
|
-
const controller = new AbortController();
|
|
37
|
-
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
38
|
-
|
|
39
|
-
// Wire pipeline abort signal into hook process
|
|
40
|
-
const onAbort = () => controller.abort();
|
|
41
|
-
if (signal) {
|
|
42
|
-
if (signal.aborted) {
|
|
43
|
-
controller.abort();
|
|
44
|
-
} else {
|
|
45
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const proc = Bun.spawn(shellArgs(command) as string[], {
|
|
51
|
-
stdin: 'pipe',
|
|
52
|
-
stdout: 'pipe',
|
|
53
|
-
stderr: 'pipe',
|
|
54
|
-
signal: controller.signal,
|
|
55
|
-
...(cwd ? { cwd } : {}),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
if (proc.stdin) {
|
|
59
|
-
try {
|
|
60
|
-
proc.stdin.write(jsonInput);
|
|
61
|
-
proc.stdin.end();
|
|
62
|
-
} catch {
|
|
63
|
-
// Process may exit before reading stdin (e.g. `exit 1`), ignore EPIPE
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Consume stdout and stderr concurrently with waiting for exit.
|
|
68
|
-
// Sequential reads after proc.exited risk a pipe-buffer deadlock when
|
|
69
|
-
// hook output exceeds the ~64 KB kernel buffer.
|
|
70
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
71
|
-
proc.exited,
|
|
72
|
-
new Response(proc.stdout).text(),
|
|
73
|
-
new Response(proc.stderr).text(),
|
|
74
|
-
]);
|
|
75
|
-
|
|
76
|
-
if (stdout.trim()) {
|
|
77
|
-
console.warn(`[hook: ${command}] stdout: ${stdout.trim()}`);
|
|
78
|
-
}
|
|
79
|
-
if (stderr.trim()) {
|
|
80
|
-
console.error(`[hook: ${command}] stderr: ${stderr.trim()}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return exitCode;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
console.error(
|
|
86
|
-
`[hook: ${command}] spawn error: ${err instanceof Error ? err.message : String(err)}`,
|
|
87
|
-
);
|
|
88
|
-
return -1;
|
|
89
|
-
} finally {
|
|
90
|
-
if (timer) clearTimeout(timer);
|
|
91
|
-
if (signal) signal.removeEventListener('abort', onAbort);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function executeHook(
|
|
96
|
-
hooks: HooksConfig | undefined,
|
|
97
|
-
event: HookEvent,
|
|
98
|
-
context: unknown,
|
|
99
|
-
workDir?: string,
|
|
100
|
-
signal?: AbortSignal,
|
|
101
|
-
): Promise<HookResult> {
|
|
102
|
-
if (!hooks) return { allowed: true, exitCode: 0 };
|
|
103
|
-
|
|
104
|
-
const commands = normalizeCommands(hooks[event]);
|
|
105
|
-
if (commands.length === 0) return { allowed: true, exitCode: 0 };
|
|
106
|
-
|
|
107
|
-
const isGate = GATE_HOOKS.has(event);
|
|
108
|
-
|
|
109
|
-
for (const cmd of commands) {
|
|
110
|
-
const exitCode = await runSingleHook(cmd, context, workDir, signal);
|
|
111
|
-
|
|
112
|
-
if (isGate && exitCode === 1) {
|
|
113
|
-
// Only exit code 1 has gate semantics (block execution)
|
|
114
|
-
return { allowed: false, exitCode };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (exitCode !== 0) {
|
|
118
|
-
// Non-zero but not 1: hook itself had an error, log but don't block
|
|
119
|
-
console.warn(`[hook: ${event}] "${cmd}" exited with code ${exitCode}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { allowed: true, exitCode: 0 };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ═══ Context Builders ═══
|
|
127
|
-
|
|
128
|
-
export interface PipelineInfo {
|
|
129
|
-
readonly name: string;
|
|
130
|
-
readonly run_id: string;
|
|
131
|
-
readonly started_at: string;
|
|
132
|
-
readonly finished_at?: string;
|
|
133
|
-
readonly duration_ms?: number;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface TrackInfo {
|
|
137
|
-
readonly id: string;
|
|
138
|
-
readonly name: string;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface TaskInfo {
|
|
142
|
-
readonly id: string;
|
|
143
|
-
readonly name: string;
|
|
144
|
-
readonly type: 'ai' | 'command';
|
|
145
|
-
readonly status: string;
|
|
146
|
-
readonly exit_code: number | null;
|
|
147
|
-
readonly duration_ms: number | null;
|
|
148
|
-
readonly stderr_path: string | null;
|
|
149
|
-
readonly session_id: string | null;
|
|
150
|
-
readonly started_at: string | null;
|
|
151
|
-
readonly finished_at: string | null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function buildPipelineStartContext(pipeline: PipelineInfo) {
|
|
155
|
-
return { event: 'pipeline_start', pipeline };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function buildTaskContext(
|
|
159
|
-
event: 'task_start' | 'task_success' | 'task_failure',
|
|
160
|
-
pipeline: PipelineInfo,
|
|
161
|
-
track: TrackInfo,
|
|
162
|
-
task: TaskInfo,
|
|
163
|
-
) {
|
|
164
|
-
return { event, pipeline, track, task };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function buildPipelineCompleteContext(
|
|
168
|
-
pipeline: PipelineInfo & { finished_at: string; duration_ms: number },
|
|
169
|
-
summary: {
|
|
170
|
-
total: number;
|
|
171
|
-
success: number;
|
|
172
|
-
failed: number;
|
|
173
|
-
skipped: number;
|
|
174
|
-
timeout: number;
|
|
175
|
-
blocked: number;
|
|
176
|
-
},
|
|
177
|
-
) {
|
|
178
|
-
return { event: 'pipeline_complete', pipeline, summary };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function buildPipelineErrorContext(
|
|
182
|
-
pipeline: PipelineInfo,
|
|
183
|
-
error: string,
|
|
184
|
-
eventType?: string,
|
|
185
|
-
abortReason?: AbortReason,
|
|
186
|
-
) {
|
|
187
|
-
return {
|
|
188
|
-
event: eventType ?? 'pipeline_error',
|
|
189
|
-
pipeline,
|
|
190
|
-
error,
|
|
191
|
-
...(abortReason !== undefined ? { abort_reason: abortReason } : {}),
|
|
192
|
-
};
|
|
193
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export { createTagma } from './tagma';
|
|
2
|
-
export type { CreateTagmaOptions, Tagma, TagmaRunOptions } from './tagma';
|
|
3
|
-
export { bunRuntime } from './runtime';
|
|
4
|
-
export type { TagmaRuntime, RunOptions as RuntimeRunOptions } from './runtime';
|
|
5
|
-
export { definePipeline } from './pipeline-definition';
|
|
6
|
-
export { PluginRegistry } from './registry';
|
|
7
|
-
export { TriggerBlockedError, TriggerTimeoutError } from './engine';
|
|
8
|
-
export type { EngineResult, RunEventPayload } from './engine';
|
|
9
|
-
export { RUN_PROTOCOL_VERSION, TASK_LOG_CAP } from './types';
|
|
10
|
-
export type {
|
|
11
|
-
PipelineConfig,
|
|
12
|
-
RawPipelineConfig,
|
|
13
|
-
RawTrackConfig,
|
|
14
|
-
RawTaskConfig,
|
|
15
|
-
TrackConfig,
|
|
16
|
-
TaskConfig,
|
|
17
|
-
RunSnapshotPayload,
|
|
18
|
-
WireRunEvent,
|
|
19
|
-
RunTaskState,
|
|
20
|
-
TaskLogLine,
|
|
21
|
-
ApprovalRequestInfo,
|
|
22
|
-
TaskStatus,
|
|
23
|
-
ApprovalRequest,
|
|
24
|
-
PluginCategory,
|
|
25
|
-
PluginCapabilities,
|
|
26
|
-
PluginSetupContext,
|
|
27
|
-
TagmaPlugin,
|
|
28
|
-
DriverPlugin,
|
|
29
|
-
TriggerPlugin,
|
|
30
|
-
CompletionPlugin,
|
|
31
|
-
MiddlewarePlugin,
|
|
32
|
-
RunEventPayload as PipelineRunEventPayload,
|
|
33
|
-
} from '@tagma/types';
|
package/src/logger.ts
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { resolve, dirname } from 'node:path';
|
|
2
|
-
import { mkdirSync, writeFileSync, openSync, writeSync, closeSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Structured record emitted for every log line. Consumers (e.g. the editor
|
|
6
|
-
* server) use this to stream process-level detail into UIs alongside the
|
|
7
|
-
* on-disk pipeline.log. `taskId` is extracted from a `[task:<id>]` prefix
|
|
8
|
-
* when the call site passes one, or overridden explicitly via the optional
|
|
9
|
-
* `taskId` argument on `section`/`quiet` (which carry no prefix).
|
|
10
|
-
*/
|
|
11
|
-
export type LogLevel = 'info' | 'warn' | 'error' | 'debug' | 'section' | 'quiet';
|
|
12
|
-
|
|
13
|
-
export interface LogRecord {
|
|
14
|
-
readonly level: LogLevel;
|
|
15
|
-
readonly taskId: string | null;
|
|
16
|
-
readonly timestamp: string;
|
|
17
|
-
readonly text: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type LogListener = (record: LogRecord) => void;
|
|
21
|
-
|
|
22
|
-
const TASK_PREFIX_RE = /\[task:([^\]]+)\]/;
|
|
23
|
-
|
|
24
|
-
function taskIdFromPrefix(prefix: string): string | null {
|
|
25
|
-
const m = TASK_PREFIX_RE.exec(prefix);
|
|
26
|
-
return m ? m[1] : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Dual-channel logger.
|
|
31
|
-
*
|
|
32
|
-
* - `info/warn/error` → console AND file (brief, user-visible events)
|
|
33
|
-
* - `debug` → file ONLY (verbose diagnostics)
|
|
34
|
-
* - `section` → file ONLY (visual separators)
|
|
35
|
-
* - `quiet` → file ONLY (bulk payload like full stdout dumps)
|
|
36
|
-
*
|
|
37
|
-
* Log file path: <workDir>/.tagma/logs/<runId>/pipeline.log (one file per pipeline run,
|
|
38
|
-
* truncated on construction). Every line is also forwarded to the optional
|
|
39
|
-
* `onLine` callback as a structured `LogRecord`, so callers that want to
|
|
40
|
-
* stream the run process over IPC/SSE don't need to tail the file.
|
|
41
|
-
*/
|
|
42
|
-
export class Logger {
|
|
43
|
-
private readonly filePath: string;
|
|
44
|
-
private readonly runDir: string;
|
|
45
|
-
private readonly onLine: LogListener | null;
|
|
46
|
-
/** Persistent file descriptor for append writes (avoids open/close per line). */
|
|
47
|
-
private fd: number | null;
|
|
48
|
-
|
|
49
|
-
constructor(workDir: string, runId: string, onLine?: LogListener) {
|
|
50
|
-
this.runDir = resolve(workDir, '.tagma', 'logs', runId);
|
|
51
|
-
this.filePath = resolve(this.runDir, 'pipeline.log');
|
|
52
|
-
this.onLine = onLine ?? null;
|
|
53
|
-
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
54
|
-
const header =
|
|
55
|
-
`# Pipeline run ${runId} @ ${new Date().toISOString()}\n` +
|
|
56
|
-
`# Host: ${process.platform} ${process.arch} Bun: ${process.versions.bun ?? 'n/a'}\n` +
|
|
57
|
-
`# Work dir: ${workDir}\n\n`;
|
|
58
|
-
writeFileSync(this.filePath, header);
|
|
59
|
-
// Open once for all subsequent appends (O_APPEND is implied by 'a' flag)
|
|
60
|
-
this.fd = openSync(this.filePath, 'a');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
info(prefix: string, message: string): void {
|
|
64
|
-
const ts = timestamp();
|
|
65
|
-
const line = `${ts} ${prefix} ${message}`;
|
|
66
|
-
// eslint-disable-next-line no-console
|
|
67
|
-
console.log(line);
|
|
68
|
-
this.emit('info', ts, line, taskIdFromPrefix(prefix));
|
|
69
|
-
this.append(line);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
warn(prefix: string, message: string): void {
|
|
73
|
-
const ts = timestamp();
|
|
74
|
-
const line = `${ts} ${prefix} WARN: ${message}`;
|
|
75
|
-
console.warn(line);
|
|
76
|
-
this.emit('warn', ts, line, taskIdFromPrefix(prefix));
|
|
77
|
-
this.append(line);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
error(prefix: string, message: string): void {
|
|
81
|
-
const ts = timestamp();
|
|
82
|
-
const line = `${ts} ${prefix} ERROR: ${message}`;
|
|
83
|
-
console.error(line);
|
|
84
|
-
this.emit('error', ts, line, taskIdFromPrefix(prefix));
|
|
85
|
-
this.append(line);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** File-only diagnostic log line. */
|
|
89
|
-
debug(prefix: string, message: string): void {
|
|
90
|
-
const ts = timestamp();
|
|
91
|
-
const line = `${ts} ${prefix} DEBUG: ${message}`;
|
|
92
|
-
this.emit('debug', ts, line, taskIdFromPrefix(prefix));
|
|
93
|
-
this.append(line);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** File-only visual separator with title. */
|
|
97
|
-
section(title: string, taskId?: string | null): void {
|
|
98
|
-
const ts = timestamp();
|
|
99
|
-
const text = `\n━━━ ${title} ━━━`;
|
|
100
|
-
this.emit('section', ts, text, taskId ?? null);
|
|
101
|
-
this.append(text);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** File-only bulk payload (e.g. full stdout / stderr dumps). */
|
|
105
|
-
quiet(message: string, taskId?: string | null): void {
|
|
106
|
-
const ts = timestamp();
|
|
107
|
-
this.emit('quiet', ts, message, taskId ?? null);
|
|
108
|
-
this.append(message);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private append(line: string): void {
|
|
112
|
-
if (this.fd === null) return;
|
|
113
|
-
try {
|
|
114
|
-
const data = line.endsWith('\n') ? line : line + '\n';
|
|
115
|
-
writeSync(this.fd, data);
|
|
116
|
-
} catch {
|
|
117
|
-
// Swallow log write failures; engine correctness shouldn't depend on logging.
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Close the persistent file handle. Called by the engine at run completion. */
|
|
122
|
-
close(): void {
|
|
123
|
-
if (this.fd !== null) {
|
|
124
|
-
try {
|
|
125
|
-
closeSync(this.fd);
|
|
126
|
-
} catch {
|
|
127
|
-
/* already closed */
|
|
128
|
-
}
|
|
129
|
-
this.fd = null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private emit(level: LogLevel, ts: string, text: string, taskId: string | null): void {
|
|
134
|
-
if (!this.onLine) return;
|
|
135
|
-
try {
|
|
136
|
-
this.onLine({ level, taskId, timestamp: ts, text });
|
|
137
|
-
} catch {
|
|
138
|
-
// Never let a listener error derail the pipeline.
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
get path(): string {
|
|
143
|
-
return this.filePath;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Directory that holds all artifacts for this run (pipeline.log, *.stderr, etc.). */
|
|
147
|
-
get dir(): string {
|
|
148
|
-
return this.runDir;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function timestamp(): string {
|
|
153
|
-
const d = new Date();
|
|
154
|
-
const hh = String(d.getHours()).padStart(2, '0');
|
|
155
|
-
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
156
|
-
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
157
|
-
const ms = String(d.getMilliseconds()).padStart(3, '0');
|
|
158
|
-
return `${hh}:${mm}:${ss}.${ms}`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Return the last `n` non-empty lines of `text`, joined with newlines. */
|
|
162
|
-
export function tailLines(text: string, n: number): string {
|
|
163
|
-
if (!text) return '';
|
|
164
|
-
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
165
|
-
return lines.slice(-n).join('\n');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Truncate a blob to at most `maxBytes` UTF-8 bytes for log embedding,
|
|
170
|
-
* appending a marker when truncation occurred.
|
|
171
|
-
* Uses TextEncoder so CJK and emoji (multi-byte) characters are counted correctly.
|
|
172
|
-
*/
|
|
173
|
-
export function clip(text: string, maxBytes = 16 * 1024): string {
|
|
174
|
-
if (!text) return '';
|
|
175
|
-
const encoder = new TextEncoder();
|
|
176
|
-
const bytes = encoder.encode(text);
|
|
177
|
-
if (bytes.length <= maxBytes) return text;
|
|
178
|
-
const omittedBytes = bytes.length - maxBytes;
|
|
179
|
-
// TextDecoder handles partial code-point boundaries safely (replacement char insertion)
|
|
180
|
-
const truncated = new TextDecoder().decode(bytes.slice(0, maxBytes));
|
|
181
|
-
return truncated + `\n…[truncated ${omittedBytes} bytes]`;
|
|
182
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { basename } from 'path';
|
|
2
|
-
import type { MiddlewarePlugin, MiddlewareContext, PromptDocument } from '../types';
|
|
3
|
-
import { validatePath } from '../utils';
|
|
4
|
-
import { appendContext } from '../prompt-doc';
|
|
5
|
-
|
|
6
|
-
export const StaticContextMiddleware: MiddlewarePlugin = {
|
|
7
|
-
name: 'static_context',
|
|
8
|
-
schema: {
|
|
9
|
-
description: 'Prepend a reference file to the prompt as static context.',
|
|
10
|
-
fields: {
|
|
11
|
-
file: {
|
|
12
|
-
type: 'path',
|
|
13
|
-
required: true,
|
|
14
|
-
description: 'Path to the reference file (relative to workDir or absolute).',
|
|
15
|
-
placeholder: 'docs/spec.md',
|
|
16
|
-
},
|
|
17
|
-
label: {
|
|
18
|
-
type: 'string',
|
|
19
|
-
description: 'Header shown before the content. Defaults to "Reference: <basename>".',
|
|
20
|
-
placeholder: 'Reference: spec.md',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
async enhanceDoc(
|
|
26
|
-
doc: PromptDocument,
|
|
27
|
-
config: Record<string, unknown>,
|
|
28
|
-
ctx: MiddlewareContext,
|
|
29
|
-
): Promise<PromptDocument> {
|
|
30
|
-
const filePath = config.file as string;
|
|
31
|
-
if (!filePath) throw new Error('static_context middleware: "file" is required');
|
|
32
|
-
|
|
33
|
-
const safePath = validatePath(filePath, ctx.workDir);
|
|
34
|
-
const file = Bun.file(safePath);
|
|
35
|
-
|
|
36
|
-
if (!(await file.exists())) {
|
|
37
|
-
console.warn(`static_context: file ${filePath} not found, skipping`);
|
|
38
|
-
return doc;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const content = await file.text();
|
|
42
|
-
const label = (config.label as string) ?? `Reference: ${basename(filePath)}`;
|
|
43
|
-
|
|
44
|
-
// Append a labeled context block; the engine's serializer joins blocks
|
|
45
|
-
// with blank lines and places the task last. No [Task] header here —
|
|
46
|
-
// that framing is the driver's concern (e.g. opencode's agent_profile).
|
|
47
|
-
return appendContext(doc, { label, content });
|
|
48
|
-
},
|
|
49
|
-
};
|
|
@@ -1,91 +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 { PipelineRunner } from './pipeline-runner';
|
|
7
|
-
import { PluginRegistry } from './registry';
|
|
8
|
-
import type { PipelineConfig } from './types';
|
|
9
|
-
|
|
10
|
-
function makeDir(): string {
|
|
11
|
-
return mkdtempSync(join(tmpdir(), 'tagma-pipeline-runner-'));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function bindingsPipeline(dir: string): PipelineConfig {
|
|
15
|
-
const emit = join(dir, 'emit.js');
|
|
16
|
-
writeFileSync(
|
|
17
|
-
emit,
|
|
18
|
-
'process.stdout.write(JSON.stringify({ city: "Shanghai" }) + "\\n");\n',
|
|
19
|
-
);
|
|
20
|
-
const echo = join(dir, 'echo.js');
|
|
21
|
-
writeFileSync(echo, 'process.stdout.write(process.argv[2] + "\\n");\n');
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
name: 'runner-snapshot',
|
|
25
|
-
tracks: [
|
|
26
|
-
{
|
|
27
|
-
id: 't',
|
|
28
|
-
name: 'T',
|
|
29
|
-
tasks: [
|
|
30
|
-
{
|
|
31
|
-
id: 'up',
|
|
32
|
-
name: 'up',
|
|
33
|
-
command: `node "${emit}"`,
|
|
34
|
-
outputs: { city: { type: 'string' } },
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
id: 'down',
|
|
38
|
-
name: 'down',
|
|
39
|
-
depends_on: ['up'],
|
|
40
|
-
command: `node "${echo}" "{{inputs.city}}"`,
|
|
41
|
-
inputs: { city: { from: 't.up.outputs.city', type: 'string', required: true } },
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function run(config: PipelineConfig, dir: string): Promise<PipelineRunner> {
|
|
50
|
-
const registry = new PluginRegistry();
|
|
51
|
-
bootstrapBuiltins(registry);
|
|
52
|
-
const runner = new PipelineRunner(config, dir, {
|
|
53
|
-
registry,
|
|
54
|
-
skipPluginLoading: true,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const result = await runner.start();
|
|
58
|
-
expect(result.success).toBe(true);
|
|
59
|
-
return runner;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
describe('PipelineRunner task snapshot', () => {
|
|
63
|
-
test('getTasks reflects task_update inputs and outputs', async () => {
|
|
64
|
-
const dir = makeDir();
|
|
65
|
-
try {
|
|
66
|
-
const runner = await run(bindingsPipeline(dir), dir);
|
|
67
|
-
|
|
68
|
-
const tasks = runner.getTasks();
|
|
69
|
-
const up = tasks.get('t.up');
|
|
70
|
-
const down = tasks.get('t.down');
|
|
71
|
-
expect(up?.outputs).toEqual({ city: 'Shanghai' });
|
|
72
|
-
expect(down?.inputs).toEqual({ city: 'Shanghai' });
|
|
73
|
-
} finally {
|
|
74
|
-
rmSync(dir, { recursive: true, force: true });
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('getTasks folds streamed task logs into the task snapshot', async () => {
|
|
79
|
-
const dir = makeDir();
|
|
80
|
-
try {
|
|
81
|
-
const runner = await run(bindingsPipeline(dir), dir);
|
|
82
|
-
|
|
83
|
-
const tasks = runner.getTasks();
|
|
84
|
-
const up = tasks.get('t.up');
|
|
85
|
-
expect(up?.logs.length).toBeGreaterThan(0);
|
|
86
|
-
expect(up?.totalLogCount).toBeGreaterThan(0);
|
|
87
|
-
} finally {
|
|
88
|
-
rmSync(dir, { recursive: true, force: true });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|