@renseiai/agentfactory 0.8.5 → 0.8.7
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 +2 -2
- package/dist/src/governor/decision-engine.d.ts +7 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +59 -1
- package/dist/src/governor/governor.d.ts +5 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +6 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/orchestrator/activity-emitter.d.ts +3 -3
- package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
- package/dist/src/orchestrator/activity-emitter.js +1 -1
- package/dist/src/orchestrator/detect-work-type.test.js +25 -16
- package/dist/src/orchestrator/index.d.ts +4 -0
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +1 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts +103 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -0
- package/dist/src/orchestrator/issue-tracker-client.js +8 -0
- package/dist/src/orchestrator/log-analyzer.d.ts +19 -4
- package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -1
- package/dist/src/orchestrator/log-analyzer.js +26 -50
- package/dist/src/orchestrator/orchestrator-utils.test.js +3 -0
- package/dist/src/orchestrator/orchestrator.d.ts +4 -2
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +193 -115
- package/dist/src/orchestrator/parse-work-result.d.ts +1 -1
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +3 -1
- package/dist/src/orchestrator/parse-work-result.test.js +9 -0
- package/dist/src/orchestrator/session-logger.d.ts +1 -1
- package/dist/src/orchestrator/session-logger.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.d.ts +1 -1
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +1 -0
- package/dist/src/orchestrator/state-types.d.ts +1 -1
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/types.d.ts +22 -2
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +50 -0
- package/dist/src/orchestrator/work-types.d.ts.map +1 -0
- package/dist/src/orchestrator/work-types.js +20 -0
- package/dist/src/templates/registry.d.ts +1 -1
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/renderer.d.ts +1 -1
- package/dist/src/templates/types.d.ts +4 -2
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +1 -0
- package/dist/src/templates/types.test.js +4 -3
- package/dist/src/tools/index.d.ts +0 -3
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +0 -2
- package/dist/src/workflow/index.d.ts +14 -0
- package/dist/src/workflow/index.d.ts.map +1 -0
- package/dist/src/workflow/index.js +10 -0
- package/dist/src/workflow/transition-engine.d.ts +44 -0
- package/dist/src/workflow/transition-engine.d.ts.map +1 -0
- package/dist/src/workflow/transition-engine.js +106 -0
- package/dist/src/workflow/transition-engine.test.d.ts +2 -0
- package/dist/src/workflow/transition-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/transition-engine.test.js +313 -0
- package/dist/src/workflow/workflow-loader.d.ts +21 -0
- package/dist/src/workflow/workflow-loader.d.ts.map +1 -0
- package/dist/src/workflow/workflow-loader.js +40 -0
- package/dist/src/workflow/workflow-loader.test.d.ts +2 -0
- package/dist/src/workflow/workflow-loader.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-loader.test.js +134 -0
- package/dist/src/workflow/workflow-registry.d.ts +56 -0
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -0
- package/dist/src/workflow/workflow-registry.js +107 -0
- package/dist/src/workflow/workflow-registry.test.d.ts +2 -0
- package/dist/src/workflow/workflow-registry.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-registry.test.js +201 -0
- package/dist/src/workflow/workflow-types.d.ts +269 -0
- package/dist/src/workflow/workflow-types.d.ts.map +1 -0
- package/dist/src/workflow/workflow-types.js +88 -0
- package/dist/src/workflow/workflow-types.test.d.ts +2 -0
- package/dist/src/workflow/workflow-types.test.d.ts.map +1 -0
- package/dist/src/workflow/workflow-types.test.js +440 -0
- package/package.json +3 -4
- package/dist/src/linear-cli.d.ts +0 -38
- package/dist/src/linear-cli.d.ts.map +0 -1
- package/dist/src/linear-cli.js +0 -674
- package/dist/src/tools/linear-runner.d.ts +0 -34
- package/dist/src/tools/linear-runner.d.ts.map +0 -1
- package/dist/src/tools/linear-runner.js +0 -700
- package/dist/src/tools/plugins/linear.d.ts +0 -9
- package/dist/src/tools/plugins/linear.d.ts.map +0 -1
- package/dist/src/tools/plugins/linear.js +0 -138
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { evaluateTransitions } from './transition-engine.js';
|
|
3
|
+
import { WorkflowRegistry } from './workflow-registry.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function makeIssue(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'issue-1',
|
|
10
|
+
identifier: 'SUP-100',
|
|
11
|
+
title: 'Test Issue',
|
|
12
|
+
status: 'Backlog',
|
|
13
|
+
labels: [],
|
|
14
|
+
createdAt: Date.now() - 2 * 60 * 60 * 1000,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function makeWorkflow(overrides) {
|
|
19
|
+
return {
|
|
20
|
+
apiVersion: 'v1.1',
|
|
21
|
+
kind: 'WorkflowDefinition',
|
|
22
|
+
metadata: { name: 'test' },
|
|
23
|
+
phases: [
|
|
24
|
+
{ name: 'development', template: 'development' },
|
|
25
|
+
{ name: 'qa', template: 'qa' },
|
|
26
|
+
{ name: 'acceptance', template: 'acceptance' },
|
|
27
|
+
{ name: 'refinement', template: 'refinement' },
|
|
28
|
+
],
|
|
29
|
+
transitions: [
|
|
30
|
+
{ from: 'Backlog', to: 'development' },
|
|
31
|
+
{ from: 'Finished', to: 'qa' },
|
|
32
|
+
{ from: 'Delivered', to: 'acceptance' },
|
|
33
|
+
{ from: 'Rejected', to: 'refinement' },
|
|
34
|
+
],
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function makeContext(overrides = {}) {
|
|
39
|
+
const registry = WorkflowRegistry.create({ workflow: makeWorkflow() });
|
|
40
|
+
return {
|
|
41
|
+
issue: makeIssue(),
|
|
42
|
+
registry,
|
|
43
|
+
isParentIssue: false,
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function registryWith(workflow) {
|
|
48
|
+
return WorkflowRegistry.create({ workflow });
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Tests
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
describe('evaluateTransitions', () => {
|
|
54
|
+
// --- Standard pipeline transitions (matching hard-coded switch) ---
|
|
55
|
+
describe('standard pipeline transitions', () => {
|
|
56
|
+
it('Backlog → trigger-development', () => {
|
|
57
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Backlog' }) });
|
|
58
|
+
const result = evaluateTransitions(ctx);
|
|
59
|
+
expect(result.action).toBe('trigger-development');
|
|
60
|
+
expect(result.reason).toContain('Backlog');
|
|
61
|
+
expect(result.reason).toContain('development');
|
|
62
|
+
});
|
|
63
|
+
it('Finished → trigger-qa', () => {
|
|
64
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Finished' }) });
|
|
65
|
+
const result = evaluateTransitions(ctx);
|
|
66
|
+
expect(result.action).toBe('trigger-qa');
|
|
67
|
+
expect(result.reason).toContain('Finished');
|
|
68
|
+
});
|
|
69
|
+
it('Delivered → trigger-acceptance', () => {
|
|
70
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Delivered' }) });
|
|
71
|
+
const result = evaluateTransitions(ctx);
|
|
72
|
+
expect(result.action).toBe('trigger-acceptance');
|
|
73
|
+
expect(result.reason).toContain('Delivered');
|
|
74
|
+
});
|
|
75
|
+
it('Rejected → trigger-refinement', () => {
|
|
76
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'Rejected' }) });
|
|
77
|
+
const result = evaluateTransitions(ctx);
|
|
78
|
+
expect(result.action).toBe('trigger-refinement');
|
|
79
|
+
expect(result.reason).toContain('Rejected');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
// --- Escalation strategy overrides ---
|
|
83
|
+
describe('escalation strategy overrides', () => {
|
|
84
|
+
it('escalate-human strategy overrides any transition', () => {
|
|
85
|
+
const ctx = makeContext({
|
|
86
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
87
|
+
workflowStrategy: 'escalate-human',
|
|
88
|
+
});
|
|
89
|
+
const result = evaluateTransitions(ctx);
|
|
90
|
+
expect(result.action).toBe('escalate-human');
|
|
91
|
+
expect(result.reason).toContain('escalate-human');
|
|
92
|
+
expect(result.reason).toContain('human intervention');
|
|
93
|
+
});
|
|
94
|
+
it('decompose strategy overrides any transition', () => {
|
|
95
|
+
const ctx = makeContext({
|
|
96
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
97
|
+
workflowStrategy: 'decompose',
|
|
98
|
+
});
|
|
99
|
+
const result = evaluateTransitions(ctx);
|
|
100
|
+
expect(result.action).toBe('decompose');
|
|
101
|
+
expect(result.reason).toContain('decompose');
|
|
102
|
+
expect(result.reason).toContain('decomposition');
|
|
103
|
+
});
|
|
104
|
+
it('normal strategy does NOT override transitions', () => {
|
|
105
|
+
const ctx = makeContext({
|
|
106
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
107
|
+
workflowStrategy: 'normal',
|
|
108
|
+
});
|
|
109
|
+
const result = evaluateTransitions(ctx);
|
|
110
|
+
expect(result.action).toBe('trigger-qa');
|
|
111
|
+
});
|
|
112
|
+
it('context-enriched strategy does NOT override transitions', () => {
|
|
113
|
+
const ctx = makeContext({
|
|
114
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
115
|
+
workflowStrategy: 'context-enriched',
|
|
116
|
+
});
|
|
117
|
+
const result = evaluateTransitions(ctx);
|
|
118
|
+
expect(result.action).toBe('trigger-refinement');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
// --- No matching transitions ---
|
|
122
|
+
describe('no matching transitions', () => {
|
|
123
|
+
it('returns none for unrecognized status', () => {
|
|
124
|
+
const ctx = makeContext({ issue: makeIssue({ status: 'UnknownStatus' }) });
|
|
125
|
+
const result = evaluateTransitions(ctx);
|
|
126
|
+
expect(result.action).toBe('none');
|
|
127
|
+
expect(result.reason).toContain('No transitions defined');
|
|
128
|
+
expect(result.reason).toContain('UnknownStatus');
|
|
129
|
+
});
|
|
130
|
+
it('returns none when no workflow loaded', () => {
|
|
131
|
+
const registry = WorkflowRegistry.create({ useBuiltinDefault: false });
|
|
132
|
+
const ctx = {
|
|
133
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
134
|
+
registry,
|
|
135
|
+
isParentIssue: false,
|
|
136
|
+
};
|
|
137
|
+
const result = evaluateTransitions(ctx);
|
|
138
|
+
expect(result.action).toBe('none');
|
|
139
|
+
expect(result.reason).toContain('No workflow definition loaded');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// --- Priority ordering ---
|
|
143
|
+
describe('priority ordering', () => {
|
|
144
|
+
it('evaluates higher priority transitions first', () => {
|
|
145
|
+
const workflow = makeWorkflow({
|
|
146
|
+
transitions: [
|
|
147
|
+
{ from: 'Backlog', to: 'qa', priority: 5 },
|
|
148
|
+
{ from: 'Backlog', to: 'development', priority: 10 },
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
const ctx = makeContext({
|
|
152
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
153
|
+
registry: registryWith(workflow),
|
|
154
|
+
});
|
|
155
|
+
const result = evaluateTransitions(ctx);
|
|
156
|
+
expect(result.action).toBe('trigger-development');
|
|
157
|
+
});
|
|
158
|
+
it('uses definition order when priorities are equal', () => {
|
|
159
|
+
const workflow = makeWorkflow({
|
|
160
|
+
transitions: [
|
|
161
|
+
{ from: 'Backlog', to: 'development' },
|
|
162
|
+
{ from: 'Backlog', to: 'qa' },
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
const ctx = makeContext({
|
|
166
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
167
|
+
registry: registryWith(workflow),
|
|
168
|
+
});
|
|
169
|
+
const result = evaluateTransitions(ctx);
|
|
170
|
+
// First match wins when priority is equal (both default to 0)
|
|
171
|
+
expect(result.action).toBe('trigger-development');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
// --- Conditional transitions (Phase 2 behavior: skip conditions) ---
|
|
175
|
+
describe('conditional transitions (Phase 2: conditions skipped)', () => {
|
|
176
|
+
it('skips transitions with conditions in Phase 2', () => {
|
|
177
|
+
const workflow = makeWorkflow({
|
|
178
|
+
transitions: [
|
|
179
|
+
{ from: 'Backlog', to: 'qa', condition: '{{ isParentIssue }}', priority: 10 },
|
|
180
|
+
{ from: 'Backlog', to: 'development' },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
const ctx = makeContext({
|
|
184
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
185
|
+
registry: registryWith(workflow),
|
|
186
|
+
});
|
|
187
|
+
const result = evaluateTransitions(ctx);
|
|
188
|
+
// Conditional transition skipped, falls through to unconditional
|
|
189
|
+
expect(result.action).toBe('trigger-development');
|
|
190
|
+
});
|
|
191
|
+
it('returns none when all transitions have conditions', () => {
|
|
192
|
+
const workflow = makeWorkflow({
|
|
193
|
+
transitions: [
|
|
194
|
+
{ from: 'Backlog', to: 'development', condition: '{{ true }}' },
|
|
195
|
+
{ from: 'Backlog', to: 'qa', condition: '{{ false }}' },
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
const ctx = makeContext({
|
|
199
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
200
|
+
registry: registryWith(workflow),
|
|
201
|
+
});
|
|
202
|
+
const result = evaluateTransitions(ctx);
|
|
203
|
+
expect(result.action).toBe('none');
|
|
204
|
+
expect(result.reason).toContain('conditions');
|
|
205
|
+
expect(result.reason).toContain('Phase 3');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
// --- Parent issue annotation ---
|
|
209
|
+
describe('parent issue annotation', () => {
|
|
210
|
+
it('includes parent note in reason for parent issues', () => {
|
|
211
|
+
const ctx = makeContext({
|
|
212
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
213
|
+
isParentIssue: true,
|
|
214
|
+
});
|
|
215
|
+
const result = evaluateTransitions(ctx);
|
|
216
|
+
expect(result.action).toBe('trigger-development');
|
|
217
|
+
expect(result.reason).toContain('coordination template');
|
|
218
|
+
});
|
|
219
|
+
it('does not include parent note for non-parent issues', () => {
|
|
220
|
+
const ctx = makeContext({
|
|
221
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
222
|
+
isParentIssue: false,
|
|
223
|
+
});
|
|
224
|
+
const result = evaluateTransitions(ctx);
|
|
225
|
+
expect(result.action).toBe('trigger-development');
|
|
226
|
+
expect(result.reason).not.toContain('coordination template');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// --- Phase-to-action mapping ---
|
|
230
|
+
describe('phase-to-action mapping', () => {
|
|
231
|
+
it('returns none for unknown phase name', () => {
|
|
232
|
+
const workflow = makeWorkflow({
|
|
233
|
+
transitions: [
|
|
234
|
+
{ from: 'Backlog', to: 'completely-unknown-phase' },
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
const ctx = makeContext({
|
|
238
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
239
|
+
registry: registryWith(workflow),
|
|
240
|
+
});
|
|
241
|
+
const result = evaluateTransitions(ctx);
|
|
242
|
+
expect(result.action).toBe('none');
|
|
243
|
+
expect(result.reason).toContain('does not map to a known GovernorAction');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// --- Built-in default workflow parity ---
|
|
247
|
+
describe('built-in default workflow parity', () => {
|
|
248
|
+
// These tests verify that the built-in workflow.yaml transitions
|
|
249
|
+
// produce the same GovernorActions as the hard-coded switch statement,
|
|
250
|
+
// for the standard unconditional transitions.
|
|
251
|
+
const builtinRegistry = WorkflowRegistry.create();
|
|
252
|
+
it('Backlog → trigger-development (matches decideBacklog)', () => {
|
|
253
|
+
const result = evaluateTransitions({
|
|
254
|
+
issue: makeIssue({ status: 'Backlog' }),
|
|
255
|
+
registry: builtinRegistry,
|
|
256
|
+
isParentIssue: false,
|
|
257
|
+
});
|
|
258
|
+
expect(result.action).toBe('trigger-development');
|
|
259
|
+
});
|
|
260
|
+
it('Finished → trigger-qa (matches decideFinished)', () => {
|
|
261
|
+
const result = evaluateTransitions({
|
|
262
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
263
|
+
registry: builtinRegistry,
|
|
264
|
+
isParentIssue: false,
|
|
265
|
+
});
|
|
266
|
+
expect(result.action).toBe('trigger-qa');
|
|
267
|
+
});
|
|
268
|
+
it('Delivered → trigger-acceptance (matches decideDelivered)', () => {
|
|
269
|
+
const result = evaluateTransitions({
|
|
270
|
+
issue: makeIssue({ status: 'Delivered' }),
|
|
271
|
+
registry: builtinRegistry,
|
|
272
|
+
isParentIssue: false,
|
|
273
|
+
});
|
|
274
|
+
expect(result.action).toBe('trigger-acceptance');
|
|
275
|
+
});
|
|
276
|
+
it('Rejected → trigger-refinement (matches decideRejected)', () => {
|
|
277
|
+
const result = evaluateTransitions({
|
|
278
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
279
|
+
registry: builtinRegistry,
|
|
280
|
+
isParentIssue: false,
|
|
281
|
+
});
|
|
282
|
+
expect(result.action).toBe('trigger-refinement');
|
|
283
|
+
});
|
|
284
|
+
it('Finished + escalate-human → escalate-human (matches decideFinished)', () => {
|
|
285
|
+
const result = evaluateTransitions({
|
|
286
|
+
issue: makeIssue({ status: 'Finished' }),
|
|
287
|
+
registry: builtinRegistry,
|
|
288
|
+
workflowStrategy: 'escalate-human',
|
|
289
|
+
isParentIssue: false,
|
|
290
|
+
});
|
|
291
|
+
expect(result.action).toBe('escalate-human');
|
|
292
|
+
});
|
|
293
|
+
it('Rejected + decompose → decompose (matches decideRejected)', () => {
|
|
294
|
+
const result = evaluateTransitions({
|
|
295
|
+
issue: makeIssue({ status: 'Rejected' }),
|
|
296
|
+
registry: builtinRegistry,
|
|
297
|
+
workflowStrategy: 'decompose',
|
|
298
|
+
isParentIssue: false,
|
|
299
|
+
});
|
|
300
|
+
expect(result.action).toBe('decompose');
|
|
301
|
+
});
|
|
302
|
+
it('Icebox has only conditional transitions — returns none in Phase 2', () => {
|
|
303
|
+
const result = evaluateTransitions({
|
|
304
|
+
issue: makeIssue({ status: 'Icebox' }),
|
|
305
|
+
registry: builtinRegistry,
|
|
306
|
+
isParentIssue: false,
|
|
307
|
+
});
|
|
308
|
+
// Icebox transitions all have conditions, which are skipped in Phase 2.
|
|
309
|
+
// This is expected — Icebox routing is handled by decideIcebox() directly.
|
|
310
|
+
expect(result.action).toBe('none');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Definition Loader
|
|
3
|
+
*
|
|
4
|
+
* Discovers, parses, and validates WorkflowDefinition documents from YAML files.
|
|
5
|
+
* Follows the same pattern as templates/loader.ts for consistency.
|
|
6
|
+
*/
|
|
7
|
+
import type { WorkflowDefinition } from './workflow-types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Load and validate a single WorkflowDefinition YAML file.
|
|
10
|
+
* Throws on invalid YAML syntax or schema validation failure.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadWorkflowDefinitionFile(filePath: string): WorkflowDefinition;
|
|
13
|
+
/**
|
|
14
|
+
* Get the path to the built-in default workflow definitions directory.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getBuiltinWorkflowDir(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Get the path to the built-in default workflow definition file.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getBuiltinWorkflowPath(): string;
|
|
21
|
+
//# sourceMappingURL=workflow-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-loader.d.ts","sourceRoot":"","sources":["../../../src/workflow/workflow-loader.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAG7D;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,CAY/E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Definition Loader
|
|
3
|
+
*
|
|
4
|
+
* Discovers, parses, and validates WorkflowDefinition documents from YAML files.
|
|
5
|
+
* Follows the same pattern as templates/loader.ts for consistency.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
|
+
import { validateWorkflowDefinition } from './workflow-types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Load and validate a single WorkflowDefinition YAML file.
|
|
13
|
+
* Throws on invalid YAML syntax or schema validation failure.
|
|
14
|
+
*/
|
|
15
|
+
export function loadWorkflowDefinitionFile(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
18
|
+
const data = parseYaml(content);
|
|
19
|
+
return validateWorkflowDefinition(data, filePath);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof Error && error.message.startsWith('Invalid workflow definition')) {
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
throw new Error(`Failed to load workflow definition ${filePath}: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the path to the built-in default workflow definitions directory.
|
|
31
|
+
*/
|
|
32
|
+
export function getBuiltinWorkflowDir() {
|
|
33
|
+
return path.join(path.dirname(new URL(import.meta.url).pathname), 'defaults');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the path to the built-in default workflow definition file.
|
|
37
|
+
*/
|
|
38
|
+
export function getBuiltinWorkflowPath() {
|
|
39
|
+
return path.join(getBuiltinWorkflowDir(), 'workflow.yaml');
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-loader.test.d.ts","sourceRoot":"","sources":["../../../src/workflow/workflow-loader.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { loadWorkflowDefinitionFile, getBuiltinWorkflowDir, getBuiltinWorkflowPath, } from './workflow-loader.js';
|
|
6
|
+
describe('getBuiltinWorkflowDir', () => {
|
|
7
|
+
it('returns an existing directory', () => {
|
|
8
|
+
const dir = getBuiltinWorkflowDir();
|
|
9
|
+
expect(fs.existsSync(dir)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
describe('getBuiltinWorkflowPath', () => {
|
|
13
|
+
it('returns a path to an existing workflow.yaml', () => {
|
|
14
|
+
const workflowPath = getBuiltinWorkflowPath();
|
|
15
|
+
expect(workflowPath).toContain('workflow.yaml');
|
|
16
|
+
expect(fs.existsSync(workflowPath)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
describe('loadWorkflowDefinitionFile', () => {
|
|
20
|
+
it('loads and validates the built-in default workflow', () => {
|
|
21
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
22
|
+
expect(workflow.apiVersion).toBe('v1.1');
|
|
23
|
+
expect(workflow.kind).toBe('WorkflowDefinition');
|
|
24
|
+
expect(workflow.metadata.name).toBe('default-workflow');
|
|
25
|
+
});
|
|
26
|
+
it('built-in workflow has expected phases', () => {
|
|
27
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
28
|
+
const phaseNames = workflow.phases.map(p => p.name);
|
|
29
|
+
expect(phaseNames).toContain('research');
|
|
30
|
+
expect(phaseNames).toContain('backlog-creation');
|
|
31
|
+
expect(phaseNames).toContain('development');
|
|
32
|
+
expect(phaseNames).toContain('qa');
|
|
33
|
+
expect(phaseNames).toContain('acceptance');
|
|
34
|
+
expect(phaseNames).toContain('refinement');
|
|
35
|
+
expect(phaseNames).toContain('coordination');
|
|
36
|
+
expect(phaseNames).toContain('qa-coordination');
|
|
37
|
+
expect(phaseNames).toContain('acceptance-coordination');
|
|
38
|
+
expect(phaseNames).toContain('refinement-coordination');
|
|
39
|
+
});
|
|
40
|
+
it('built-in workflow has expected transitions', () => {
|
|
41
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
42
|
+
// Check standard pipeline transitions exist
|
|
43
|
+
const transitionMap = new Map(workflow.transitions
|
|
44
|
+
.filter(t => !t.condition) // Only unconditional transitions
|
|
45
|
+
.map(t => [t.from, t.to]));
|
|
46
|
+
expect(transitionMap.get('Backlog')).toBe('development');
|
|
47
|
+
expect(transitionMap.get('Finished')).toBe('qa');
|
|
48
|
+
expect(transitionMap.get('Delivered')).toBe('acceptance');
|
|
49
|
+
expect(transitionMap.get('Rejected')).toBe('refinement');
|
|
50
|
+
});
|
|
51
|
+
it('built-in workflow has Icebox transitions with conditions', () => {
|
|
52
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
53
|
+
const iceboxTransitions = workflow.transitions.filter(t => t.from === 'Icebox');
|
|
54
|
+
expect(iceboxTransitions.length).toBeGreaterThanOrEqual(2);
|
|
55
|
+
const researchTransition = iceboxTransitions.find(t => t.to === 'research');
|
|
56
|
+
expect(researchTransition).toBeDefined();
|
|
57
|
+
expect(researchTransition.condition).toBeDefined();
|
|
58
|
+
expect(researchTransition.priority).toBeDefined();
|
|
59
|
+
const backlogTransition = iceboxTransitions.find(t => t.to === 'backlog-creation');
|
|
60
|
+
expect(backlogTransition).toBeDefined();
|
|
61
|
+
expect(backlogTransition.condition).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
it('built-in escalation ladder matches computeStrategy() values', () => {
|
|
64
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
65
|
+
expect(workflow.escalation).toBeDefined();
|
|
66
|
+
const ladder = workflow.escalation.ladder;
|
|
67
|
+
// Verify the escalation ladder matches the hard-coded computeStrategy()
|
|
68
|
+
// from agent-tracking.ts: cycle 1→normal, 2→context-enriched, 3→decompose, 4+→escalate-human
|
|
69
|
+
const strategyByCycle = new Map(ladder.map(r => [r.cycle, r.strategy]));
|
|
70
|
+
expect(strategyByCycle.get(1)).toBe('normal');
|
|
71
|
+
expect(strategyByCycle.get(2)).toBe('context-enriched');
|
|
72
|
+
expect(strategyByCycle.get(3)).toBe('decompose');
|
|
73
|
+
expect(strategyByCycle.get(4)).toBe('escalate-human');
|
|
74
|
+
});
|
|
75
|
+
it('built-in circuit breaker matches hard-coded constants', () => {
|
|
76
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
77
|
+
expect(workflow.escalation).toBeDefined();
|
|
78
|
+
const cb = workflow.escalation.circuitBreaker;
|
|
79
|
+
// MAX_TOTAL_SESSIONS = 8 from agent-tracking.ts
|
|
80
|
+
expect(cb.maxSessionsPerIssue).toBe(8);
|
|
81
|
+
// MAX_SESSION_ATTEMPTS = 3 from decision-engine.ts
|
|
82
|
+
expect(cb.maxSessionsPerPhase).toBe(3);
|
|
83
|
+
});
|
|
84
|
+
it('built-in refinement phase has strategy variants', () => {
|
|
85
|
+
const workflow = loadWorkflowDefinitionFile(getBuiltinWorkflowPath());
|
|
86
|
+
const refinement = workflow.phases.find(p => p.name === 'refinement');
|
|
87
|
+
expect(refinement).toBeDefined();
|
|
88
|
+
expect(refinement.variants).toBeDefined();
|
|
89
|
+
expect(refinement.variants['context-enriched']).toBe('refinement-context-enriched');
|
|
90
|
+
expect(refinement.variants['decompose']).toBe('refinement-decompose');
|
|
91
|
+
});
|
|
92
|
+
it('throws on non-existent file', () => {
|
|
93
|
+
expect(() => loadWorkflowDefinitionFile('/non/existent/file.yaml')).toThrow();
|
|
94
|
+
});
|
|
95
|
+
it('throws on invalid YAML syntax', () => {
|
|
96
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-test-'));
|
|
97
|
+
const tmpPath = path.join(tmpDir, 'bad-yaml.yaml');
|
|
98
|
+
fs.writeFileSync(tmpPath, '{ invalid yaml syntax :::\n broken: [');
|
|
99
|
+
try {
|
|
100
|
+
expect(() => loadWorkflowDefinitionFile(tmpPath)).toThrow('Failed to load workflow definition');
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
it('throws on schema validation failure with file path', () => {
|
|
107
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-test-'));
|
|
108
|
+
const tmpPath = path.join(tmpDir, 'invalid-schema.yaml');
|
|
109
|
+
fs.writeFileSync(tmpPath, 'apiVersion: v1\nkind: WorkflowTemplate\nmetadata:\n name: test\n');
|
|
110
|
+
try {
|
|
111
|
+
expect(() => loadWorkflowDefinitionFile(tmpPath)).toThrow(tmpPath);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('throws on valid YAML but invalid workflow schema', () => {
|
|
118
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'workflow-test-'));
|
|
119
|
+
const tmpPath = path.join(tmpDir, 'wrong-kind.yaml');
|
|
120
|
+
fs.writeFileSync(tmpPath, [
|
|
121
|
+
'apiVersion: v1.1',
|
|
122
|
+
'kind: WorkflowDefinition',
|
|
123
|
+
'metadata:',
|
|
124
|
+
' name: test',
|
|
125
|
+
'# missing phases and transitions',
|
|
126
|
+
].join('\n'));
|
|
127
|
+
try {
|
|
128
|
+
expect(() => loadWorkflowDefinitionFile(tmpPath)).toThrow('Invalid workflow definition');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Registry
|
|
3
|
+
*
|
|
4
|
+
* In-memory registry that manages WorkflowDefinition resolution with
|
|
5
|
+
* layered overrides, following the same pattern as TemplateRegistry.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order (later sources override earlier):
|
|
8
|
+
* 1. Built-in default (workflow/defaults/workflow.yaml)
|
|
9
|
+
* 2. Project-level override (e.g., .agentfactory/workflow.yaml)
|
|
10
|
+
* 3. Inline config override (programmatic)
|
|
11
|
+
*/
|
|
12
|
+
import type { WorkflowDefinition, EscalationConfig } from './workflow-types.js';
|
|
13
|
+
export interface WorkflowRegistryConfig {
|
|
14
|
+
/** Path to a project-level workflow definition YAML override */
|
|
15
|
+
workflowPath?: string;
|
|
16
|
+
/** Inline workflow definition override (highest priority) */
|
|
17
|
+
workflow?: WorkflowDefinition;
|
|
18
|
+
/** Whether to load the built-in default workflow (default: true) */
|
|
19
|
+
useBuiltinDefault?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare class WorkflowRegistry {
|
|
22
|
+
private workflow;
|
|
23
|
+
constructor();
|
|
24
|
+
/**
|
|
25
|
+
* Create and initialize a registry from configuration.
|
|
26
|
+
*/
|
|
27
|
+
static create(config?: WorkflowRegistryConfig): WorkflowRegistry;
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the registry by loading workflow definition from
|
|
30
|
+
* configured sources. Later sources override earlier ones.
|
|
31
|
+
*/
|
|
32
|
+
initialize(config?: WorkflowRegistryConfig): void;
|
|
33
|
+
/**
|
|
34
|
+
* Get the currently loaded workflow definition.
|
|
35
|
+
*/
|
|
36
|
+
getWorkflow(): WorkflowDefinition | null;
|
|
37
|
+
/**
|
|
38
|
+
* Get the escalation configuration, or null if none defined.
|
|
39
|
+
*/
|
|
40
|
+
getEscalation(): EscalationConfig | null;
|
|
41
|
+
/**
|
|
42
|
+
* Compute escalation strategy from the workflow's escalation ladder.
|
|
43
|
+
*
|
|
44
|
+
* Selects the highest cycle threshold that is <= the given cycleCount.
|
|
45
|
+
* Falls back to 'normal' if no match or no escalation config.
|
|
46
|
+
*/
|
|
47
|
+
getEscalationStrategy(cycleCount: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Get circuit breaker limits from the workflow definition.
|
|
50
|
+
*/
|
|
51
|
+
getCircuitBreakerLimits(): {
|
|
52
|
+
maxSessionsPerIssue: number;
|
|
53
|
+
maxSessionsPerPhase: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=workflow-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-registry.d.ts","sourceRoot":"","sources":["../../../src/workflow/workflow-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAO/E,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IAC7B,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAcD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAkC;;IAIlD;;OAEG;IACH,MAAM,CAAC,MAAM,CAAC,MAAM,GAAE,sBAA2B,GAAG,gBAAgB;IAMpE;;;OAGG;IACH,UAAU,CAAC,MAAM,GAAE,sBAA2B,GAAG,IAAI;IAsBrD;;OAEG;IACH,WAAW,IAAI,kBAAkB,GAAG,IAAI;IAIxC;;OAEG;IACH,aAAa,IAAI,gBAAgB,GAAG,IAAI;IAIxC;;;;;OAKG;IACH,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAcjD;;OAEG;IACH,uBAAuB,IAAI;QAAE,mBAAmB,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,MAAM,CAAA;KAAE;CAOxF"}
|