@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.6
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/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions-group.test.js +222 -0
- package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
- package/dist/__tests__/engine.conditions.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions.test.js +164 -0
- package/dist/__tests__/engine.conditions.test.js.map +1 -0
- package/dist/__tests__/engine.resume.test.d.ts +2 -0
- package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
- package/dist/__tests__/engine.resume.test.js +93 -0
- package/dist/__tests__/engine.resume.test.js.map +1 -0
- package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
- package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
- package/dist/__tests__/engine.script-stage.test.js +108 -0
- package/dist/__tests__/engine.script-stage.test.js.map +1 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -0
- package/{src/db/client.ts → dist/db/client.js} +1 -2
- package/dist/db/client.js.map +1 -0
- package/dist/engine.context-event.test.d.ts +2 -0
- package/dist/engine.context-event.test.d.ts.map +1 -0
- package/dist/engine.context-event.test.js +111 -0
- package/dist/engine.context-event.test.js.map +1 -0
- package/dist/engine.d.ts +52 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +364 -0
- package/dist/engine.js.map +1 -0
- package/dist/events.d.ts +163 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +3 -45
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/agent-loader.d.ts +4 -0
- package/dist/pipeline/agent-loader.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.js +28 -0
- package/dist/pipeline/agent-loader.js.map +1 -0
- package/dist/pipeline/agent-loader.test.d.ts +2 -0
- package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.test.js +134 -0
- package/dist/pipeline/agent-loader.test.js.map +1 -0
- package/dist/pipeline/condition-evaluator.d.ts +6 -0
- package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.js +105 -0
- package/dist/pipeline/condition-evaluator.js.map +1 -0
- package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
- package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.test.js +103 -0
- package/dist/pipeline/condition-evaluator.test.js.map +1 -0
- package/dist/pipeline/context-pack-loader.d.ts +3 -0
- package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
- package/dist/pipeline/context-pack-loader.js +51 -0
- package/dist/pipeline/context-pack-loader.js.map +1 -0
- package/dist/pipeline/context-propagation.d.ts +28 -0
- package/dist/pipeline/context-propagation.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.js +161 -0
- package/dist/pipeline/context-propagation.js.map +1 -0
- package/dist/pipeline/context-propagation.test.d.ts +2 -0
- package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.test.js +194 -0
- package/dist/pipeline/context-propagation.test.js.map +1 -0
- package/dist/pipeline/contract-loader.d.ts +4 -0
- package/dist/pipeline/contract-loader.d.ts.map +1 -0
- package/dist/pipeline/contract-loader.js +30 -0
- package/dist/pipeline/contract-loader.js.map +1 -0
- package/dist/pipeline/group-orchestrator.d.ts +19 -0
- package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
- package/dist/pipeline/group-orchestrator.js +376 -0
- package/dist/pipeline/group-orchestrator.js.map +1 -0
- package/dist/pipeline/hook-executor.d.ts +28 -0
- package/dist/pipeline/hook-executor.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.js +63 -0
- package/dist/pipeline/hook-executor.js.map +1 -0
- package/dist/pipeline/hook-executor.test.d.ts +2 -0
- package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.test.js +71 -0
- package/dist/pipeline/hook-executor.test.js.map +1 -0
- package/dist/pipeline/invariants-loader.d.ts +6 -0
- package/dist/pipeline/invariants-loader.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.js +15 -0
- package/dist/pipeline/invariants-loader.js.map +1 -0
- package/dist/pipeline/invariants-loader.test.d.ts +2 -0
- package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.test.js +39 -0
- package/dist/pipeline/invariants-loader.test.js.map +1 -0
- package/dist/pipeline/loader.d.ts +5 -0
- package/dist/pipeline/loader.d.ts.map +1 -0
- package/dist/pipeline/loader.js +132 -0
- package/dist/pipeline/loader.js.map +1 -0
- package/dist/pipeline/loader.test.d.ts +2 -0
- package/dist/pipeline/loader.test.d.ts.map +1 -0
- package/dist/pipeline/loader.test.js +120 -0
- package/dist/pipeline/loader.test.js.map +1 -0
- package/dist/pipeline/output-validator.d.ts +10 -0
- package/dist/pipeline/output-validator.d.ts.map +1 -0
- package/dist/pipeline/output-validator.js +20 -0
- package/dist/pipeline/output-validator.js.map +1 -0
- package/dist/pipeline/output-validator.test.d.ts +2 -0
- package/dist/pipeline/output-validator.test.d.ts.map +1 -0
- package/dist/pipeline/output-validator.test.js +77 -0
- package/dist/pipeline/output-validator.test.js.map +1 -0
- package/dist/pipeline/post-validator.d.ts +8 -0
- package/dist/pipeline/post-validator.d.ts.map +1 -0
- package/dist/pipeline/post-validator.js +103 -0
- package/dist/pipeline/post-validator.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +10 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +26 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skill-loader.test.d.ts +2 -0
- package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.test.js +38 -0
- package/dist/pipeline/skill-loader.test.js.map +1 -0
- package/dist/pipeline/stage-executor.d.ts +26 -0
- package/dist/pipeline/stage-executor.d.ts.map +1 -0
- package/dist/pipeline/stage-executor.js +540 -0
- package/dist/pipeline/stage-executor.js.map +1 -0
- package/dist/pipeline/stage-resolver.d.ts +3 -0
- package/dist/pipeline/stage-resolver.d.ts.map +1 -0
- package/dist/pipeline/stage-resolver.js +6 -0
- package/dist/pipeline/stage-resolver.js.map +1 -0
- package/dist/pipeline/startup-executor.d.ts +3 -0
- package/dist/pipeline/startup-executor.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.js +23 -0
- package/dist/pipeline/startup-executor.js.map +1 -0
- package/dist/pipeline/startup-executor.test.d.ts +2 -0
- package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.test.js +34 -0
- package/dist/pipeline/startup-executor.test.js.map +1 -0
- package/dist/pipeline/types.d.ts +28 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +12 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/repo-resolver.d.ts +10 -0
- package/dist/repo-resolver.d.ts.map +1 -0
- package/dist/repo-resolver.js +39 -0
- package/dist/repo-resolver.js.map +1 -0
- package/dist/spawners/direct-engine-spawner.d.ts +8 -0
- package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
- package/dist/spawners/direct-engine-spawner.js +24 -0
- package/dist/spawners/direct-engine-spawner.js.map +1 -0
- package/dist/state/run-store.d.ts +74 -0
- package/dist/state/run-store.d.ts.map +1 -0
- package/dist/state/run-store.js +269 -0
- package/dist/state/run-store.js.map +1 -0
- package/dist/state/run-store.test.d.ts +2 -0
- package/dist/state/run-store.test.d.ts.map +1 -0
- package/dist/state/run-store.test.js +130 -0
- package/dist/state/run-store.test.js.map +1 -0
- package/dist/state/state-machine.d.ts +6 -0
- package/dist/state/state-machine.d.ts.map +1 -0
- package/dist/state/state-machine.js +27 -0
- package/dist/state/state-machine.js.map +1 -0
- package/dist/state/status-derivation.d.ts +12 -0
- package/dist/state/status-derivation.d.ts.map +1 -0
- package/dist/state/status-derivation.js +30 -0
- package/dist/state/status-derivation.js.map +1 -0
- package/package.json +7 -4
- package/ARCHITECTURE.md +0 -57
- package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
- package/src/__tests__/engine.conditions-group.test.ts +0 -244
- package/src/__tests__/engine.conditions.test.ts +0 -186
- package/src/__tests__/engine.resume.test.ts +0 -108
- package/src/__tests__/engine.script-stage.test.ts +0 -125
- package/src/engine.context-event.test.ts +0 -175
- package/src/engine.ts +0 -491
- package/src/events.ts +0 -167
- package/src/pipeline/agent-loader.test.ts +0 -151
- package/src/pipeline/agent-loader.ts +0 -39
- package/src/pipeline/condition-evaluator.test.ts +0 -129
- package/src/pipeline/condition-evaluator.ts +0 -121
- package/src/pipeline/context-pack-loader.ts +0 -63
- package/src/pipeline/context-propagation.test.ts +0 -237
- package/src/pipeline/context-propagation.ts +0 -235
- package/src/pipeline/contract-loader.ts +0 -41
- package/src/pipeline/group-orchestrator.ts +0 -483
- package/src/pipeline/hook-executor.test.ts +0 -121
- package/src/pipeline/hook-executor.ts +0 -87
- package/src/pipeline/invariants-loader.test.ts +0 -51
- package/src/pipeline/invariants-loader.ts +0 -14
- package/src/pipeline/loader.test.ts +0 -128
- package/src/pipeline/loader.ts +0 -149
- package/src/pipeline/output-validator.test.ts +0 -100
- package/src/pipeline/output-validator.ts +0 -40
- package/src/pipeline/post-validator.ts +0 -124
- package/src/pipeline/skill-loader.test.ts +0 -44
- package/src/pipeline/skill-loader.ts +0 -32
- package/src/pipeline/stage-executor.ts +0 -654
- package/src/pipeline/stage-resolver.ts +0 -8
- package/src/pipeline/startup-executor.test.ts +0 -37
- package/src/pipeline/startup-executor.ts +0 -32
- package/src/pipeline/types.ts +0 -42
- package/src/repo-resolver.ts +0 -61
- package/src/spawners/direct-engine-spawner.ts +0 -26
- package/src/state/run-store.test.ts +0 -157
- package/src/state/run-store.ts +0 -362
- package/src/state/state-machine.ts +0 -35
- package/src/state/status-derivation.ts +0 -36
- package/tests/context-pack-loader.test.ts +0 -113
- package/tests/context-propagation.test.ts +0 -267
- package/tests/direct-engine-spawner.test.ts +0 -102
- package/tests/e2e/feature-v5.test.ts +0 -57
- package/tests/events.test.ts +0 -56
- package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
- package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
- package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
- package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
- package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
- package/tests/loader.test.ts +0 -385
- package/tests/post-validator.test.ts +0 -297
- package/tests/repo-resolver.test.ts +0 -102
- package/tests/run-store.test.ts +0 -143
- package/tests/state-machine.test.ts +0 -110
- package/tests/unit/context-propagation.test.ts +0 -45
- package/tests/unit/engine.test.ts +0 -770
- package/tests/unit/group-loop.test.ts +0 -389
- package/tests/unit/group-parallel.test.ts +0 -423
- package/tests/unit/state/status-derivation.test.ts +0 -88
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -14
|
@@ -1,483 +0,0 @@
|
|
|
1
|
-
// GroupOrchestrator — extracted from PipelineEngine.runGroup/runGroupParallel/runGroupSequential()
|
|
2
|
-
// Handles group-level orchestration: parallel and sequential iteration with feedback loops.
|
|
3
|
-
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import type { StageGroup, StageRun, StageStatus } from '@studio-foundation/contracts';
|
|
6
|
-
import {
|
|
7
|
-
addStageOutput,
|
|
8
|
-
addStageToolResults,
|
|
9
|
-
setGroupFeedback,
|
|
10
|
-
type PipelineContext,
|
|
11
|
-
} from './context-propagation.js';
|
|
12
|
-
import type { EngineEvents, PipelineEventEmitter } from '../events.js';
|
|
13
|
-
import type { ToolRegistry, AnonymizationMiddleware } from '@studio-foundation/runner';
|
|
14
|
-
import type { StageExecutor } from './stage-executor.js';
|
|
15
|
-
import type { GroupResult, ProjectPaths, StageResult } from './types.js';
|
|
16
|
-
|
|
17
|
-
export interface GroupOrchestratorConfig {
|
|
18
|
-
events?: EngineEvents;
|
|
19
|
-
emitter: PipelineEventEmitter;
|
|
20
|
-
stageExecutor: StageExecutor;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class GroupOrchestrator {
|
|
24
|
-
constructor(private config: GroupOrchestratorConfig) {}
|
|
25
|
-
|
|
26
|
-
async run(
|
|
27
|
-
group: StageGroup,
|
|
28
|
-
context: PipelineContext,
|
|
29
|
-
stageOffset: number,
|
|
30
|
-
totalStages: number,
|
|
31
|
-
userInput: string | Record<string, unknown>,
|
|
32
|
-
paths: ProjectPaths,
|
|
33
|
-
toolRegistry: ToolRegistry | undefined,
|
|
34
|
-
runMiddleware?: AnonymizationMiddleware | null,
|
|
35
|
-
runId?: string,
|
|
36
|
-
signal?: AbortSignal,
|
|
37
|
-
skipSet?: Set<string>,
|
|
38
|
-
originalRunId?: string,
|
|
39
|
-
): Promise<GroupResult> {
|
|
40
|
-
if (group.mode === 'parallel') {
|
|
41
|
-
return this.runParallel(group, context, stageOffset, totalStages, userInput, paths, toolRegistry, runMiddleware, runId, signal);
|
|
42
|
-
}
|
|
43
|
-
return this.runSequential(group, context, stageOffset, totalStages, userInput, paths, toolRegistry, runMiddleware, runId, signal, skipSet, originalRunId);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private async runParallel(
|
|
47
|
-
group: StageGroup,
|
|
48
|
-
context: PipelineContext,
|
|
49
|
-
stageOffset: number,
|
|
50
|
-
totalStages: number,
|
|
51
|
-
userInput: string | Record<string, unknown>,
|
|
52
|
-
paths: ProjectPaths,
|
|
53
|
-
toolRegistry: ToolRegistry | undefined,
|
|
54
|
-
runMiddleware?: AnonymizationMiddleware | null,
|
|
55
|
-
runId?: string,
|
|
56
|
-
signal?: AbortSignal,
|
|
57
|
-
): Promise<GroupResult> {
|
|
58
|
-
let totalTokensDelta = 0;
|
|
59
|
-
let totalToolCallsDelta = 0;
|
|
60
|
-
|
|
61
|
-
this.config.events?.onGroupStart?.({
|
|
62
|
-
group_name: group.group,
|
|
63
|
-
max_iterations: group.max_iterations,
|
|
64
|
-
parallel: true,
|
|
65
|
-
});
|
|
66
|
-
this.config.emitter.emit({
|
|
67
|
-
type: 'group_start',
|
|
68
|
-
groupName: group.group,
|
|
69
|
-
maxIterations: group.max_iterations,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Parallel groups run exactly one iteration
|
|
73
|
-
this.config.events?.onGroupIteration?.({
|
|
74
|
-
group_name: group.group,
|
|
75
|
-
iteration: 1,
|
|
76
|
-
max_iterations: group.max_iterations,
|
|
77
|
-
});
|
|
78
|
-
this.config.emitter.emit({
|
|
79
|
-
type: 'group_iteration',
|
|
80
|
-
groupName: group.group,
|
|
81
|
-
iteration: 1,
|
|
82
|
-
maxIterations: group.max_iterations,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (signal?.aborted) {
|
|
86
|
-
this.config.events?.onGroupComplete?.({ group_name: group.group, iterations: 1, status: 'cancelled' });
|
|
87
|
-
this.config.emitter.emit({ type: 'group_complete', groupName: group.group, iterations: 1, status: 'cancelled' });
|
|
88
|
-
return { status: 'cancelled', stageRuns: [], stagesExecuted: group.stages.length, context, totalTokensDelta, totalToolCallsDelta };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// All parallel stages read the same pre-group context snapshot.
|
|
92
|
-
// previousStageName = the last stage before this group.
|
|
93
|
-
let previousStageName: string | undefined;
|
|
94
|
-
for (const [name] of context.stageOutputs) {
|
|
95
|
-
previousStageName = name;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// fail-fast: create a shared AbortController to cancel siblings on first failure
|
|
99
|
-
const groupAbort = (group.on_failure ?? 'fail-fast') === 'fail-fast'
|
|
100
|
-
? new AbortController()
|
|
101
|
-
: null;
|
|
102
|
-
if (groupAbort && signal) {
|
|
103
|
-
signal.addEventListener('abort', () => groupAbort.abort(), { once: true });
|
|
104
|
-
}
|
|
105
|
-
const stageSignal = groupAbort?.signal ?? signal;
|
|
106
|
-
|
|
107
|
-
// Launch all stages concurrently
|
|
108
|
-
const settled = await Promise.allSettled(
|
|
109
|
-
group.stages.map(async (stage, i) => {
|
|
110
|
-
const result = await this.config.stageExecutor.execute(
|
|
111
|
-
stage,
|
|
112
|
-
context,
|
|
113
|
-
previousStageName,
|
|
114
|
-
userInput,
|
|
115
|
-
stageOffset + i,
|
|
116
|
-
totalStages,
|
|
117
|
-
paths,
|
|
118
|
-
toolRegistry,
|
|
119
|
-
runMiddleware,
|
|
120
|
-
runId,
|
|
121
|
-
stageSignal,
|
|
122
|
-
);
|
|
123
|
-
// fail-fast: abort remaining stages on first non-success
|
|
124
|
-
if (groupAbort && result.status !== 'success') {
|
|
125
|
-
groupAbort.abort();
|
|
126
|
-
}
|
|
127
|
-
return { stageName: stage.name, result };
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// Build result map keyed by stage name
|
|
132
|
-
const resultMap = new Map<string, StageResult>();
|
|
133
|
-
for (const s of settled) {
|
|
134
|
-
if (s.status === 'fulfilled') {
|
|
135
|
-
resultMap.set(s.value.stageName, s.value.result);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Collect stage runs in definition order (deterministic output ordering)
|
|
140
|
-
const allStageRuns: StageRun[] = [];
|
|
141
|
-
for (const stage of group.stages) {
|
|
142
|
-
const result = resultMap.get(stage.name);
|
|
143
|
-
if (result) allStageRuns.push(result.stageRun);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// After building resultMap, accumulate token/tool call totals
|
|
147
|
-
for (const [, result] of resultMap) {
|
|
148
|
-
totalTokensDelta += result.tokensDelta ?? 0;
|
|
149
|
-
totalToolCallsDelta += result.toolCallsDelta ?? 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Derive group status: cancelled > failed > success
|
|
153
|
-
// rejected treated as failed in parallel mode (no feedback loop)
|
|
154
|
-
let groupStatus: StageStatus = 'success';
|
|
155
|
-
for (const stage of group.stages) {
|
|
156
|
-
const result = resultMap.get(stage.name);
|
|
157
|
-
if (!result) { groupStatus = 'failed'; continue; }
|
|
158
|
-
if (result.status === 'cancelled' && groupStatus === 'success') groupStatus = 'cancelled';
|
|
159
|
-
if (result.status === 'failed' || result.status === 'rejected') groupStatus = 'failed';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// If every stage was skipped, the group is skipped (not success)
|
|
163
|
-
const allSkipped = group.stages.every(
|
|
164
|
-
(s) => resultMap.get(s.name)?.status === 'skipped',
|
|
165
|
-
);
|
|
166
|
-
if (allSkipped) groupStatus = 'skipped';
|
|
167
|
-
|
|
168
|
-
// Merge successful outputs into context in definition order (regardless of group status)
|
|
169
|
-
// This preserves observability for collect-all partial failures
|
|
170
|
-
for (const stage of group.stages) {
|
|
171
|
-
const result = resultMap.get(stage.name);
|
|
172
|
-
if (!result || result.status !== 'success') continue;
|
|
173
|
-
if (result.lastAgentOutput !== undefined) {
|
|
174
|
-
addStageOutput(context, stage.name, result.lastAgentOutput);
|
|
175
|
-
}
|
|
176
|
-
if (result.toolCalls?.length) {
|
|
177
|
-
addStageToolResults(context, stage.name, result.toolCalls);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
this.config.events?.onGroupComplete?.({ group_name: group.group, iterations: 1, status: groupStatus });
|
|
182
|
-
this.config.emitter.emit({ type: 'group_complete', groupName: group.group, iterations: 1, status: groupStatus });
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
status: groupStatus,
|
|
186
|
-
stageRuns: allStageRuns,
|
|
187
|
-
stagesExecuted: group.stages.length,
|
|
188
|
-
context,
|
|
189
|
-
totalTokensDelta,
|
|
190
|
-
totalToolCallsDelta,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private async runSequential(
|
|
195
|
-
group: StageGroup,
|
|
196
|
-
context: PipelineContext,
|
|
197
|
-
stageOffset: number,
|
|
198
|
-
totalStages: number,
|
|
199
|
-
userInput: string | Record<string, unknown>,
|
|
200
|
-
paths: ProjectPaths,
|
|
201
|
-
toolRegistry: ToolRegistry | undefined,
|
|
202
|
-
runMiddleware?: AnonymizationMiddleware | null,
|
|
203
|
-
runId?: string,
|
|
204
|
-
signal?: AbortSignal,
|
|
205
|
-
skipSet?: Set<string>,
|
|
206
|
-
originalRunId?: string,
|
|
207
|
-
): Promise<GroupResult> {
|
|
208
|
-
const allStageRuns: StageRun[] = [];
|
|
209
|
-
let iteration = 0;
|
|
210
|
-
let totalTokensDelta = 0;
|
|
211
|
-
let totalToolCallsDelta = 0;
|
|
212
|
-
|
|
213
|
-
this.config.events?.onGroupStart?.({
|
|
214
|
-
group_name: group.group,
|
|
215
|
-
max_iterations: group.max_iterations,
|
|
216
|
-
});
|
|
217
|
-
this.config.emitter.emit({
|
|
218
|
-
type: 'group_start',
|
|
219
|
-
groupName: group.group,
|
|
220
|
-
maxIterations: group.max_iterations,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
while (iteration < group.max_iterations) {
|
|
224
|
-
// Check cancellation before group iteration
|
|
225
|
-
if (signal?.aborted) {
|
|
226
|
-
this.config.events?.onGroupComplete?.({
|
|
227
|
-
group_name: group.group,
|
|
228
|
-
iterations: iteration,
|
|
229
|
-
status: 'cancelled',
|
|
230
|
-
});
|
|
231
|
-
this.config.emitter.emit({
|
|
232
|
-
type: 'group_complete',
|
|
233
|
-
groupName: group.group,
|
|
234
|
-
iterations: iteration,
|
|
235
|
-
status: 'cancelled',
|
|
236
|
-
});
|
|
237
|
-
return {
|
|
238
|
-
status: 'cancelled',
|
|
239
|
-
stageRuns: allStageRuns,
|
|
240
|
-
stagesExecuted: group.stages.length,
|
|
241
|
-
context,
|
|
242
|
-
totalTokensDelta,
|
|
243
|
-
totalToolCallsDelta,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
iteration++;
|
|
248
|
-
|
|
249
|
-
this.config.events?.onGroupIteration?.({
|
|
250
|
-
group_name: group.group,
|
|
251
|
-
iteration,
|
|
252
|
-
max_iterations: group.max_iterations,
|
|
253
|
-
});
|
|
254
|
-
this.config.emitter.emit({
|
|
255
|
-
type: 'group_iteration',
|
|
256
|
-
groupName: group.group,
|
|
257
|
-
iteration,
|
|
258
|
-
maxIterations: group.max_iterations,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
let groupSucceeded = true;
|
|
262
|
-
let anyStageExecuted = false;
|
|
263
|
-
let previousStageName: string | undefined;
|
|
264
|
-
// Find the last stage name before this group in the pipeline context
|
|
265
|
-
for (const [name] of context.stageOutputs) {
|
|
266
|
-
previousStageName = name;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
for (let i = 0; i < group.stages.length; i++) {
|
|
270
|
-
if (signal?.aborted) break;
|
|
271
|
-
|
|
272
|
-
const stage = group.stages[i];
|
|
273
|
-
const stageNumber = stageOffset + i;
|
|
274
|
-
|
|
275
|
-
// On iteration 1 only, skip stages before resumeFromStage
|
|
276
|
-
if (iteration === 1 && skipSet?.has(stage.name)) {
|
|
277
|
-
const skippedReason = originalRunId
|
|
278
|
-
? `resumed from run ${originalRunId}`
|
|
279
|
-
: 'resumed from prior run';
|
|
280
|
-
const skippedRun: StageRun = {
|
|
281
|
-
id: randomUUID(),
|
|
282
|
-
stage_name: stage.name,
|
|
283
|
-
status: 'skipped',
|
|
284
|
-
started_at: new Date().toISOString(),
|
|
285
|
-
completed_at: new Date().toISOString(),
|
|
286
|
-
tasks: [],
|
|
287
|
-
skipped_reason: skippedReason,
|
|
288
|
-
};
|
|
289
|
-
this.config.events?.onStageComplete?.({
|
|
290
|
-
stage_name: stage.name,
|
|
291
|
-
stage_index: stageNumber,
|
|
292
|
-
total_stages: totalStages,
|
|
293
|
-
status: 'skipped',
|
|
294
|
-
attempts: 0,
|
|
295
|
-
duration_ms: 0,
|
|
296
|
-
skipped_reason: skippedReason,
|
|
297
|
-
});
|
|
298
|
-
allStageRuns.push(skippedRun);
|
|
299
|
-
previousStageName = stage.name;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const result = await this.config.stageExecutor.execute(
|
|
304
|
-
stage,
|
|
305
|
-
context,
|
|
306
|
-
previousStageName,
|
|
307
|
-
userInput,
|
|
308
|
-
stageNumber,
|
|
309
|
-
totalStages,
|
|
310
|
-
paths,
|
|
311
|
-
toolRegistry,
|
|
312
|
-
runMiddleware,
|
|
313
|
-
runId,
|
|
314
|
-
signal,
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
allStageRuns.push(result.stageRun);
|
|
318
|
-
totalTokensDelta += result.tokensDelta ?? 0;
|
|
319
|
-
totalToolCallsDelta += result.toolCallsDelta ?? 0;
|
|
320
|
-
if (result.status !== 'skipped') anyStageExecuted = true;
|
|
321
|
-
|
|
322
|
-
// Cancelled → stop group
|
|
323
|
-
if (result.status === 'cancelled') {
|
|
324
|
-
this.config.events?.onGroupComplete?.({
|
|
325
|
-
group_name: group.group,
|
|
326
|
-
iterations: iteration,
|
|
327
|
-
status: 'cancelled',
|
|
328
|
-
});
|
|
329
|
-
this.config.emitter.emit({
|
|
330
|
-
type: 'group_complete',
|
|
331
|
-
groupName: group.group,
|
|
332
|
-
iterations: iteration,
|
|
333
|
-
status: 'cancelled',
|
|
334
|
-
});
|
|
335
|
-
return {
|
|
336
|
-
status: 'cancelled',
|
|
337
|
-
stageRuns: allStageRuns,
|
|
338
|
-
stagesExecuted: group.stages.length,
|
|
339
|
-
context,
|
|
340
|
-
totalTokensDelta,
|
|
341
|
-
totalToolCallsDelta,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Technical failure → stop everything
|
|
346
|
-
if (result.status === 'failed') {
|
|
347
|
-
this.config.events?.onGroupComplete?.({
|
|
348
|
-
group_name: group.group,
|
|
349
|
-
iterations: iteration,
|
|
350
|
-
status: 'failed',
|
|
351
|
-
});
|
|
352
|
-
this.config.emitter.emit({
|
|
353
|
-
type: 'group_complete',
|
|
354
|
-
groupName: group.group,
|
|
355
|
-
iterations: iteration,
|
|
356
|
-
status: 'failed',
|
|
357
|
-
});
|
|
358
|
-
return {
|
|
359
|
-
status: 'failed',
|
|
360
|
-
stageRuns: allStageRuns,
|
|
361
|
-
stagesExecuted: group.stages.length,
|
|
362
|
-
context,
|
|
363
|
-
totalTokensDelta,
|
|
364
|
-
totalToolCallsDelta,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Propagate output to context (will be cleared if we loop)
|
|
369
|
-
if (result.lastAgentOutput !== undefined) {
|
|
370
|
-
addStageOutput(context, stage.name, result.lastAgentOutput);
|
|
371
|
-
}
|
|
372
|
-
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
373
|
-
addStageToolResults(context, stage.name, result.toolCalls);
|
|
374
|
-
}
|
|
375
|
-
previousStageName = stage.name;
|
|
376
|
-
|
|
377
|
-
// Last stage rejected → feedback loop
|
|
378
|
-
const isLastStage = i === group.stages.length - 1;
|
|
379
|
-
if (result.status === 'rejected' && isLastStage) {
|
|
380
|
-
groupSucceeded = false;
|
|
381
|
-
|
|
382
|
-
if (iteration < group.max_iterations) {
|
|
383
|
-
// Clear group stage outputs and tool results for next iteration
|
|
384
|
-
for (const gs of group.stages) {
|
|
385
|
-
context.stageOutputs.delete(gs.name);
|
|
386
|
-
context.stageToolResults.delete(gs.name);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// Set feedback for next iteration
|
|
390
|
-
setGroupFeedback(context, {
|
|
391
|
-
iteration,
|
|
392
|
-
max_iterations: group.max_iterations,
|
|
393
|
-
rejection_reason: result.postValidation?.rejection_reason || 'Rejected',
|
|
394
|
-
rejection_details: result.postValidation?.rejection_details,
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
this.config.events?.onGroupFeedback?.({
|
|
398
|
-
group_name: group.group,
|
|
399
|
-
iteration,
|
|
400
|
-
rejection_reason: result.postValidation?.rejection_reason || 'Rejected',
|
|
401
|
-
rejection_details: result.postValidation?.rejection_details || [],
|
|
402
|
-
});
|
|
403
|
-
this.config.emitter.emit({
|
|
404
|
-
type: 'group_feedback',
|
|
405
|
-
groupName: group.group,
|
|
406
|
-
iteration,
|
|
407
|
-
rejectionReason: result.postValidation?.rejection_reason || 'Rejected',
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
break; // Exit inner stage loop, retry group
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Non-gate stage rejected → stop
|
|
415
|
-
if (result.status === 'rejected') {
|
|
416
|
-
this.config.events?.onGroupComplete?.({
|
|
417
|
-
group_name: group.group,
|
|
418
|
-
iterations: iteration,
|
|
419
|
-
status: 'rejected',
|
|
420
|
-
});
|
|
421
|
-
this.config.emitter.emit({
|
|
422
|
-
type: 'group_complete',
|
|
423
|
-
groupName: group.group,
|
|
424
|
-
iterations: iteration,
|
|
425
|
-
status: 'rejected',
|
|
426
|
-
});
|
|
427
|
-
return {
|
|
428
|
-
status: 'rejected',
|
|
429
|
-
stageRuns: allStageRuns,
|
|
430
|
-
stagesExecuted: group.stages.length,
|
|
431
|
-
context,
|
|
432
|
-
totalTokensDelta,
|
|
433
|
-
totalToolCallsDelta,
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (groupSucceeded) {
|
|
439
|
-
const groupStatus = anyStageExecuted ? 'success' : 'skipped';
|
|
440
|
-
this.config.events?.onGroupComplete?.({
|
|
441
|
-
group_name: group.group,
|
|
442
|
-
iterations: iteration,
|
|
443
|
-
status: groupStatus,
|
|
444
|
-
});
|
|
445
|
-
this.config.emitter.emit({
|
|
446
|
-
type: 'group_complete',
|
|
447
|
-
groupName: group.group,
|
|
448
|
-
iterations: iteration,
|
|
449
|
-
status: groupStatus,
|
|
450
|
-
});
|
|
451
|
-
return {
|
|
452
|
-
status: groupStatus,
|
|
453
|
-
stageRuns: allStageRuns,
|
|
454
|
-
stagesExecuted: group.stages.length,
|
|
455
|
-
context,
|
|
456
|
-
totalTokensDelta,
|
|
457
|
-
totalToolCallsDelta,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Max iterations exhausted
|
|
463
|
-
this.config.events?.onGroupComplete?.({
|
|
464
|
-
group_name: group.group,
|
|
465
|
-
iterations: iteration,
|
|
466
|
-
status: 'rejected',
|
|
467
|
-
});
|
|
468
|
-
this.config.emitter.emit({
|
|
469
|
-
type: 'group_complete',
|
|
470
|
-
groupName: group.group,
|
|
471
|
-
iterations: iteration,
|
|
472
|
-
status: 'rejected',
|
|
473
|
-
});
|
|
474
|
-
return {
|
|
475
|
-
status: 'rejected',
|
|
476
|
-
stageRuns: allStageRuns,
|
|
477
|
-
stagesExecuted: group.stages.length,
|
|
478
|
-
context,
|
|
479
|
-
totalTokensDelta,
|
|
480
|
-
totalToolCallsDelta,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderHookCommand, runStageHook, runToolHook } from './hook-executor.js';
|
|
3
|
-
|
|
4
|
-
describe('renderHookCommand', () => {
|
|
5
|
-
it('substitutes {{tool.argName}} with tool argument value', () => {
|
|
6
|
-
const result = renderHookCommand(
|
|
7
|
-
'npx prettier --write {{tool.path}}',
|
|
8
|
-
{ path: '/tmp/foo.ts' }
|
|
9
|
-
);
|
|
10
|
-
expect(result).toBe('npx prettier --write /tmp/foo.ts');
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns empty string for missing tool argument', () => {
|
|
14
|
-
const result = renderHookCommand('do something {{tool.missing}}', {});
|
|
15
|
-
expect(result).toBe('do something ');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('substitutes multiple occurrences of the same placeholder', () => {
|
|
19
|
-
const result = renderHookCommand('cp {{tool.src}} {{tool.dst}}', { src: 'a.ts', dst: 'b.ts' });
|
|
20
|
-
expect(result).toBe('cp a.ts b.ts');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('leaves non-tool placeholders unchanged', () => {
|
|
24
|
-
const result = renderHookCommand('echo {{other}}', { other: 'x' });
|
|
25
|
-
// {{other}} is not a {{tool.*}} pattern — left as-is
|
|
26
|
-
expect(result).toBe('echo {{other}}');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('substitutes {{output.field}} with value from outputContext', () => {
|
|
30
|
-
const result = renderHookCommand(
|
|
31
|
-
'npx eslint {{output.files_changed}}',
|
|
32
|
-
{},
|
|
33
|
-
{ files_changed: 'src/foo.ts' }
|
|
34
|
-
);
|
|
35
|
-
expect(result).toBe('npx eslint src/foo.ts');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('space-joins array values from outputContext', () => {
|
|
39
|
-
const result = renderHookCommand(
|
|
40
|
-
'npx eslint {{output.files_changed}}',
|
|
41
|
-
{},
|
|
42
|
-
{ files_changed: ['src/foo.ts', 'src/bar.ts'] }
|
|
43
|
-
);
|
|
44
|
-
expect(result).toBe('npx eslint src/foo.ts src/bar.ts');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('returns empty string for missing output field', () => {
|
|
48
|
-
const result = renderHookCommand(
|
|
49
|
-
'npx eslint {{output.missing}}',
|
|
50
|
-
{},
|
|
51
|
-
{}
|
|
52
|
-
);
|
|
53
|
-
expect(result).toBe('npx eslint ');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('handles mixed {{tool.*}} and {{output.*}} in same command', () => {
|
|
57
|
-
const result = renderHookCommand(
|
|
58
|
-
'run {{tool.script}} on {{output.files_changed}}',
|
|
59
|
-
{ script: 'check.sh' },
|
|
60
|
-
{ files_changed: 'src/foo.ts' }
|
|
61
|
-
);
|
|
62
|
-
expect(result).toBe('run check.sh on src/foo.ts');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('leaves {{tool.*}} unchanged when outputContext not provided', () => {
|
|
66
|
-
const result = renderHookCommand('echo {{tool.path}}', { path: 'x.ts' });
|
|
67
|
-
expect(result).toBe('echo x.ts');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe('runStageHook', () => {
|
|
72
|
-
it('returns success with stdout when command exits 0', async () => {
|
|
73
|
-
const result = await runStageHook(
|
|
74
|
-
{ command: 'echo hello', on_failure: 'warn' },
|
|
75
|
-
'/tmp'
|
|
76
|
-
);
|
|
77
|
-
expect(result.success).toBe(true);
|
|
78
|
-
expect(result.stdout).toBe('hello');
|
|
79
|
-
expect(result.stderr).toBe('');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('returns failure with stderr when command exits non-zero', async () => {
|
|
83
|
-
const result = await runStageHook(
|
|
84
|
-
{ command: 'sh -c "echo boom >&2; exit 1"', on_failure: 'warn' },
|
|
85
|
-
'/tmp'
|
|
86
|
-
);
|
|
87
|
-
expect(result.success).toBe(false);
|
|
88
|
-
expect(result.stderr).toContain('boom');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('resolves {{output.files_changed}} from outputContext in command', async () => {
|
|
92
|
-
const result = await runStageHook(
|
|
93
|
-
{ command: 'echo {{output.files_changed}}', on_failure: 'warn' },
|
|
94
|
-
'/tmp',
|
|
95
|
-
{ files_changed: ['src/foo.ts', 'src/bar.ts'] }
|
|
96
|
-
);
|
|
97
|
-
expect(result.success).toBe(true);
|
|
98
|
-
expect(result.stdout).toBe('src/foo.ts src/bar.ts');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('runToolHook', () => {
|
|
103
|
-
it('renders template and executes command', async () => {
|
|
104
|
-
const result = await runToolHook(
|
|
105
|
-
{ matcher: 'repo_manager-write_file', command: 'echo {{tool.path}}', on_failure: 'warn' },
|
|
106
|
-
{ path: '/tmp/test.ts' },
|
|
107
|
-
'/tmp'
|
|
108
|
-
);
|
|
109
|
-
expect(result.success).toBe(true);
|
|
110
|
-
expect(result.stdout).toBe('/tmp/test.ts');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('returns failure when rendered command exits non-zero', async () => {
|
|
114
|
-
const result = await runToolHook(
|
|
115
|
-
{ matcher: 'any-tool', command: 'exit 1', on_failure: 'warn' },
|
|
116
|
-
{},
|
|
117
|
-
'/tmp'
|
|
118
|
-
);
|
|
119
|
-
expect(result.success).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Hook executor — runs shell commands at lifecycle points within a stage
|
|
2
|
-
// Mirrors startup-executor.ts but with on_failure semantics and tool arg templates
|
|
3
|
-
|
|
4
|
-
import { exec } from 'node:child_process';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import type { StageHookDef, ToolHookDef } from '@studio-foundation/contracts';
|
|
7
|
-
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
|
-
const HOOK_TIMEOUT_MS = 30_000;
|
|
10
|
-
|
|
11
|
-
export interface HookResult {
|
|
12
|
-
success: boolean;
|
|
13
|
-
stdout: string;
|
|
14
|
-
stderr: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Renders {{tool.argName}} and {{output.field}} placeholders.
|
|
19
|
-
* Arrays in outputContext are space-joined for CLI argument passing.
|
|
20
|
-
* Unknown keys → empty string.
|
|
21
|
-
*
|
|
22
|
-
* Note: values are substituted verbatim into the command string.
|
|
23
|
-
* Hook commands are authored by pipeline owners (trusted), not end users.
|
|
24
|
-
* Do not use with untrusted input sources.
|
|
25
|
-
*/
|
|
26
|
-
export function renderHookCommand(
|
|
27
|
-
command: string,
|
|
28
|
-
toolArgs: Record<string, unknown>,
|
|
29
|
-
outputContext: Record<string, unknown> = {}
|
|
30
|
-
): string {
|
|
31
|
-
return command
|
|
32
|
-
.replace(
|
|
33
|
-
/\{\{tool\.(\w+)\}\}/g,
|
|
34
|
-
(_, key: string) => (toolArgs[key] !== undefined ? String(toolArgs[key]) : '')
|
|
35
|
-
)
|
|
36
|
-
.replace(/\{\{output\.(\w+)\}\}/g, (_, key: string) => {
|
|
37
|
-
const val = outputContext[key];
|
|
38
|
-
if (val === undefined) return '';
|
|
39
|
-
if (Array.isArray(val)) return val.join(' ');
|
|
40
|
-
return String(val);
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Run a stage-level hook command (on_stage_start, on_stage_complete).
|
|
46
|
-
* outputContext provides {{output.<field>}} substitution values.
|
|
47
|
-
* on_stage_start hooks omit outputContext (no output available before the stage runs).
|
|
48
|
-
*/
|
|
49
|
-
export async function runStageHook(
|
|
50
|
-
hook: StageHookDef,
|
|
51
|
-
cwd: string,
|
|
52
|
-
outputContext: Record<string, unknown> = {}
|
|
53
|
-
): Promise<HookResult> {
|
|
54
|
-
const command = renderHookCommand(hook.command, {}, outputContext);
|
|
55
|
-
return execHook(command, cwd);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Run a tool-level hook command (pre_tool_use, post_tool_use).
|
|
60
|
-
* The command may reference tool arguments via {{tool.argName}}.
|
|
61
|
-
*/
|
|
62
|
-
export async function runToolHook(
|
|
63
|
-
hook: ToolHookDef,
|
|
64
|
-
toolArgs: Record<string, unknown>,
|
|
65
|
-
cwd: string
|
|
66
|
-
): Promise<HookResult> {
|
|
67
|
-
const command = renderHookCommand(hook.command, toolArgs);
|
|
68
|
-
return execHook(command, cwd);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function execHook(command: string, cwd: string): Promise<HookResult> {
|
|
72
|
-
try {
|
|
73
|
-
const { stdout, stderr } = await execAsync(command, {
|
|
74
|
-
cwd,
|
|
75
|
-
timeout: HOOK_TIMEOUT_MS,
|
|
76
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
77
|
-
});
|
|
78
|
-
return { success: true, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
79
|
-
} catch (err: unknown) {
|
|
80
|
-
const e = err as { stdout?: string; stderr?: string };
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
stdout: e.stdout?.trim() ?? '',
|
|
84
|
-
stderr: e.stderr?.trim() ?? String(err),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|