@renseiai/agentfactory 0.8.6 → 0.8.8

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.
Files changed (175) hide show
  1. package/README.md +2 -2
  2. package/dist/src/config/repository-config.d.ts +14 -0
  3. package/dist/src/config/repository-config.d.ts.map +1 -1
  4. package/dist/src/config/repository-config.js +20 -0
  5. package/dist/src/governor/decision-engine.d.ts +7 -0
  6. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  7. package/dist/src/governor/decision-engine.js +59 -1
  8. package/dist/src/governor/event-types.d.ts +18 -1
  9. package/dist/src/governor/event-types.d.ts.map +1 -1
  10. package/dist/src/governor/event-types.js +4 -0
  11. package/dist/src/governor/governor.d.ts +5 -1
  12. package/dist/src/governor/governor.d.ts.map +1 -1
  13. package/dist/src/governor/governor.js +6 -1
  14. package/dist/src/index.d.ts +1 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +1 -0
  17. package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
  18. package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
  19. package/dist/src/merge-queue/adapters/github-native.js +243 -0
  20. package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
  21. package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
  22. package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
  23. package/dist/src/merge-queue/index.d.ts +18 -0
  24. package/dist/src/merge-queue/index.d.ts.map +1 -0
  25. package/dist/src/merge-queue/index.js +28 -0
  26. package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
  27. package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
  28. package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
  29. package/dist/src/merge-queue/types.d.ts +48 -0
  30. package/dist/src/merge-queue/types.d.ts.map +1 -0
  31. package/dist/src/merge-queue/types.js +8 -0
  32. package/dist/src/orchestrator/activity-emitter.d.ts +3 -3
  33. package/dist/src/orchestrator/activity-emitter.d.ts.map +1 -1
  34. package/dist/src/orchestrator/activity-emitter.js +1 -1
  35. package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
  36. package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
  37. package/dist/src/orchestrator/artifact-tracker.js +235 -0
  38. package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
  39. package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
  40. package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
  41. package/dist/src/orchestrator/context-manager.d.ts +72 -0
  42. package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
  43. package/dist/src/orchestrator/context-manager.js +120 -0
  44. package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
  45. package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
  46. package/dist/src/orchestrator/context-manager.test.js +137 -0
  47. package/dist/src/orchestrator/detect-work-type.test.js +25 -16
  48. package/dist/src/orchestrator/index.d.ts +12 -2
  49. package/dist/src/orchestrator/index.d.ts.map +1 -1
  50. package/dist/src/orchestrator/index.js +9 -1
  51. package/dist/src/orchestrator/issue-tracker-client.d.ts +103 -0
  52. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -0
  53. package/dist/src/orchestrator/issue-tracker-client.js +8 -0
  54. package/dist/src/orchestrator/log-analyzer.d.ts +19 -4
  55. package/dist/src/orchestrator/log-analyzer.d.ts.map +1 -1
  56. package/dist/src/orchestrator/log-analyzer.js +26 -50
  57. package/dist/src/orchestrator/orchestrator-utils.test.js +3 -0
  58. package/dist/src/orchestrator/orchestrator.d.ts +16 -2
  59. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  60. package/dist/src/orchestrator/orchestrator.js +449 -115
  61. package/dist/src/orchestrator/parse-work-result.d.ts +1 -1
  62. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  63. package/dist/src/orchestrator/parse-work-result.js +1 -1
  64. package/dist/src/orchestrator/session-logger.d.ts +1 -1
  65. package/dist/src/orchestrator/session-logger.d.ts.map +1 -1
  66. package/dist/src/orchestrator/state-recovery.d.ts +22 -3
  67. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  68. package/dist/src/orchestrator/state-recovery.js +55 -2
  69. package/dist/src/orchestrator/state-recovery.test.js +106 -2
  70. package/dist/src/orchestrator/state-types.d.ts +63 -1
  71. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  72. package/dist/src/orchestrator/state-types.js +5 -1
  73. package/dist/src/orchestrator/summary-builder.d.ts +47 -0
  74. package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
  75. package/dist/src/orchestrator/summary-builder.js +240 -0
  76. package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
  77. package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
  78. package/dist/src/orchestrator/summary-builder.test.js +236 -0
  79. package/dist/src/orchestrator/types.d.ts +24 -2
  80. package/dist/src/orchestrator/types.d.ts.map +1 -1
  81. package/dist/src/orchestrator/work-types.d.ts +50 -0
  82. package/dist/src/orchestrator/work-types.d.ts.map +1 -0
  83. package/dist/src/orchestrator/work-types.js +20 -0
  84. package/dist/src/templates/registry.d.ts +1 -1
  85. package/dist/src/templates/registry.test.js +2 -2
  86. package/dist/src/templates/renderer.d.ts +1 -1
  87. package/dist/src/templates/types.d.ts +6 -2
  88. package/dist/src/templates/types.d.ts.map +1 -1
  89. package/dist/src/templates/types.js +2 -0
  90. package/dist/src/templates/types.test.js +4 -3
  91. package/dist/src/tools/index.d.ts +0 -3
  92. package/dist/src/tools/index.d.ts.map +1 -1
  93. package/dist/src/tools/index.js +0 -2
  94. package/dist/src/workflow/branching-router.d.ts +38 -0
  95. package/dist/src/workflow/branching-router.d.ts.map +1 -0
  96. package/dist/src/workflow/branching-router.js +52 -0
  97. package/dist/src/workflow/branching-router.test.d.ts +2 -0
  98. package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
  99. package/dist/src/workflow/branching-router.test.js +209 -0
  100. package/dist/src/workflow/duration.d.ts +28 -0
  101. package/dist/src/workflow/duration.d.ts.map +1 -0
  102. package/dist/src/workflow/duration.js +57 -0
  103. package/dist/src/workflow/duration.test.d.ts +2 -0
  104. package/dist/src/workflow/duration.test.d.ts.map +1 -0
  105. package/dist/src/workflow/duration.test.js +74 -0
  106. package/dist/src/workflow/expression/ast.d.ts +53 -0
  107. package/dist/src/workflow/expression/ast.d.ts.map +1 -0
  108. package/dist/src/workflow/expression/ast.js +8 -0
  109. package/dist/src/workflow/expression/context.d.ts +40 -0
  110. package/dist/src/workflow/expression/context.d.ts.map +1 -0
  111. package/dist/src/workflow/expression/context.js +37 -0
  112. package/dist/src/workflow/expression/evaluator.d.ts +28 -0
  113. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
  114. package/dist/src/workflow/expression/evaluator.js +165 -0
  115. package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
  116. package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
  117. package/dist/src/workflow/expression/evaluator.test.js +792 -0
  118. package/dist/src/workflow/expression/expression.test.d.ts +2 -0
  119. package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
  120. package/dist/src/workflow/expression/expression.test.js +516 -0
  121. package/dist/src/workflow/expression/helpers.d.ts +21 -0
  122. package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
  123. package/dist/src/workflow/expression/helpers.js +56 -0
  124. package/dist/src/workflow/expression/index.d.ts +55 -0
  125. package/dist/src/workflow/expression/index.d.ts.map +1 -0
  126. package/dist/src/workflow/expression/index.js +71 -0
  127. package/dist/src/workflow/expression/lexer.d.ts +37 -0
  128. package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
  129. package/dist/src/workflow/expression/lexer.js +166 -0
  130. package/dist/src/workflow/expression/parser.d.ts +23 -0
  131. package/dist/src/workflow/expression/parser.d.ts.map +1 -0
  132. package/dist/src/workflow/expression/parser.js +181 -0
  133. package/dist/src/workflow/index.d.ts +21 -0
  134. package/dist/src/workflow/index.d.ts.map +1 -0
  135. package/dist/src/workflow/index.js +15 -0
  136. package/dist/src/workflow/retry-resolver.d.ts +51 -0
  137. package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
  138. package/dist/src/workflow/retry-resolver.js +70 -0
  139. package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
  140. package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
  141. package/dist/src/workflow/retry-resolver.test.js +149 -0
  142. package/dist/src/workflow/transition-engine.d.ts +46 -0
  143. package/dist/src/workflow/transition-engine.d.ts.map +1 -0
  144. package/dist/src/workflow/transition-engine.js +113 -0
  145. package/dist/src/workflow/transition-engine.test.d.ts +2 -0
  146. package/dist/src/workflow/transition-engine.test.d.ts.map +1 -0
  147. package/dist/src/workflow/transition-engine.test.js +425 -0
  148. package/dist/src/workflow/workflow-loader.d.ts +21 -0
  149. package/dist/src/workflow/workflow-loader.d.ts.map +1 -0
  150. package/dist/src/workflow/workflow-loader.js +40 -0
  151. package/dist/src/workflow/workflow-loader.test.d.ts +2 -0
  152. package/dist/src/workflow/workflow-loader.test.d.ts.map +1 -0
  153. package/dist/src/workflow/workflow-loader.test.js +134 -0
  154. package/dist/src/workflow/workflow-registry.d.ts +97 -0
  155. package/dist/src/workflow/workflow-registry.d.ts.map +1 -0
  156. package/dist/src/workflow/workflow-registry.js +173 -0
  157. package/dist/src/workflow/workflow-registry.test.d.ts +2 -0
  158. package/dist/src/workflow/workflow-registry.test.d.ts.map +1 -0
  159. package/dist/src/workflow/workflow-registry.test.js +201 -0
  160. package/dist/src/workflow/workflow-types.d.ts +442 -0
  161. package/dist/src/workflow/workflow-types.d.ts.map +1 -0
  162. package/dist/src/workflow/workflow-types.js +113 -0
  163. package/dist/src/workflow/workflow-types.test.d.ts +2 -0
  164. package/dist/src/workflow/workflow-types.test.d.ts.map +1 -0
  165. package/dist/src/workflow/workflow-types.test.js +440 -0
  166. package/package.json +3 -4
  167. package/dist/src/linear-cli.d.ts +0 -38
  168. package/dist/src/linear-cli.d.ts.map +0 -1
  169. package/dist/src/linear-cli.js +0 -674
  170. package/dist/src/tools/linear-runner.d.ts +0 -34
  171. package/dist/src/tools/linear-runner.d.ts.map +0 -1
  172. package/dist/src/tools/linear-runner.js +0 -700
  173. package/dist/src/tools/plugins/linear.d.ts +0 -9
  174. package/dist/src/tools/plugins/linear.d.ts.map +0 -1
  175. package/dist/src/tools/plugins/linear.js +0 -138
@@ -0,0 +1,425 @@
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
+ { name: 'research', template: 'research' },
29
+ { name: 'backlog-creation', template: 'backlog-creation' },
30
+ ],
31
+ transitions: [
32
+ { from: 'Backlog', to: 'development' },
33
+ { from: 'Finished', to: 'qa' },
34
+ { from: 'Delivered', to: 'acceptance' },
35
+ { from: 'Rejected', to: 'refinement' },
36
+ ],
37
+ ...overrides,
38
+ };
39
+ }
40
+ function makeContext(overrides = {}) {
41
+ const registry = WorkflowRegistry.create({ workflow: makeWorkflow() });
42
+ return {
43
+ issue: makeIssue(),
44
+ registry,
45
+ isParentIssue: false,
46
+ ...overrides,
47
+ };
48
+ }
49
+ function registryWith(workflow) {
50
+ return WorkflowRegistry.create({ workflow });
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Tests
54
+ // ---------------------------------------------------------------------------
55
+ describe('evaluateTransitions', () => {
56
+ // --- Standard pipeline transitions (matching hard-coded switch) ---
57
+ describe('standard pipeline transitions', () => {
58
+ it('Backlog → trigger-development', () => {
59
+ const ctx = makeContext({ issue: makeIssue({ status: 'Backlog' }) });
60
+ const result = evaluateTransitions(ctx);
61
+ expect(result.action).toBe('trigger-development');
62
+ expect(result.reason).toContain('Backlog');
63
+ expect(result.reason).toContain('development');
64
+ });
65
+ it('Finished → trigger-qa', () => {
66
+ const ctx = makeContext({ issue: makeIssue({ status: 'Finished' }) });
67
+ const result = evaluateTransitions(ctx);
68
+ expect(result.action).toBe('trigger-qa');
69
+ expect(result.reason).toContain('Finished');
70
+ });
71
+ it('Delivered → trigger-acceptance', () => {
72
+ const ctx = makeContext({ issue: makeIssue({ status: 'Delivered' }) });
73
+ const result = evaluateTransitions(ctx);
74
+ expect(result.action).toBe('trigger-acceptance');
75
+ expect(result.reason).toContain('Delivered');
76
+ });
77
+ it('Rejected → trigger-refinement', () => {
78
+ const ctx = makeContext({ issue: makeIssue({ status: 'Rejected' }) });
79
+ const result = evaluateTransitions(ctx);
80
+ expect(result.action).toBe('trigger-refinement');
81
+ expect(result.reason).toContain('Rejected');
82
+ });
83
+ });
84
+ // --- Escalation strategy overrides ---
85
+ describe('escalation strategy overrides', () => {
86
+ it('escalate-human strategy overrides any transition', () => {
87
+ const ctx = makeContext({
88
+ issue: makeIssue({ status: 'Finished' }),
89
+ workflowStrategy: 'escalate-human',
90
+ });
91
+ const result = evaluateTransitions(ctx);
92
+ expect(result.action).toBe('escalate-human');
93
+ expect(result.reason).toContain('escalate-human');
94
+ expect(result.reason).toContain('human intervention');
95
+ });
96
+ it('decompose strategy overrides any transition', () => {
97
+ const ctx = makeContext({
98
+ issue: makeIssue({ status: 'Rejected' }),
99
+ workflowStrategy: 'decompose',
100
+ });
101
+ const result = evaluateTransitions(ctx);
102
+ expect(result.action).toBe('decompose');
103
+ expect(result.reason).toContain('decompose');
104
+ expect(result.reason).toContain('decomposition');
105
+ });
106
+ it('normal strategy does NOT override transitions', () => {
107
+ const ctx = makeContext({
108
+ issue: makeIssue({ status: 'Finished' }),
109
+ workflowStrategy: 'normal',
110
+ });
111
+ const result = evaluateTransitions(ctx);
112
+ expect(result.action).toBe('trigger-qa');
113
+ });
114
+ it('context-enriched strategy does NOT override transitions', () => {
115
+ const ctx = makeContext({
116
+ issue: makeIssue({ status: 'Rejected' }),
117
+ workflowStrategy: 'context-enriched',
118
+ });
119
+ const result = evaluateTransitions(ctx);
120
+ expect(result.action).toBe('trigger-refinement');
121
+ });
122
+ });
123
+ // --- No matching transitions ---
124
+ describe('no matching transitions', () => {
125
+ it('returns none for unrecognized status', () => {
126
+ const ctx = makeContext({ issue: makeIssue({ status: 'UnknownStatus' }) });
127
+ const result = evaluateTransitions(ctx);
128
+ expect(result.action).toBe('none');
129
+ expect(result.reason).toContain('No transitions defined');
130
+ expect(result.reason).toContain('UnknownStatus');
131
+ });
132
+ it('returns none when no workflow loaded', () => {
133
+ const registry = WorkflowRegistry.create({ useBuiltinDefault: false });
134
+ const ctx = {
135
+ issue: makeIssue({ status: 'Backlog' }),
136
+ registry,
137
+ isParentIssue: false,
138
+ };
139
+ const result = evaluateTransitions(ctx);
140
+ expect(result.action).toBe('none');
141
+ expect(result.reason).toContain('No workflow definition loaded');
142
+ });
143
+ });
144
+ // --- Priority ordering ---
145
+ describe('priority ordering', () => {
146
+ it('evaluates higher priority transitions first', () => {
147
+ const workflow = makeWorkflow({
148
+ transitions: [
149
+ { from: 'Backlog', to: 'qa', priority: 5 },
150
+ { from: 'Backlog', to: 'development', priority: 10 },
151
+ ],
152
+ });
153
+ const ctx = makeContext({
154
+ issue: makeIssue({ status: 'Backlog' }),
155
+ registry: registryWith(workflow),
156
+ });
157
+ const result = evaluateTransitions(ctx);
158
+ expect(result.action).toBe('trigger-development');
159
+ });
160
+ it('uses definition order when priorities are equal', () => {
161
+ const workflow = makeWorkflow({
162
+ transitions: [
163
+ { from: 'Backlog', to: 'development' },
164
+ { from: 'Backlog', to: 'qa' },
165
+ ],
166
+ });
167
+ const ctx = makeContext({
168
+ issue: makeIssue({ status: 'Backlog' }),
169
+ registry: registryWith(workflow),
170
+ });
171
+ const result = evaluateTransitions(ctx);
172
+ // First match wins when priority is equal (both default to 0)
173
+ expect(result.action).toBe('trigger-development');
174
+ });
175
+ });
176
+ // --- Conditional transitions (conditions now evaluated) ---
177
+ describe('conditional transitions', () => {
178
+ it('evaluates condition and selects matching conditional transition', () => {
179
+ const workflow = makeWorkflow({
180
+ transitions: [
181
+ { from: 'Backlog', to: 'qa', condition: '{{ isParentIssue }}', priority: 10 },
182
+ { from: 'Backlog', to: 'development' },
183
+ ],
184
+ });
185
+ const ctx = makeContext({
186
+ issue: makeIssue({ status: 'Backlog' }),
187
+ registry: registryWith(workflow),
188
+ isParentIssue: true,
189
+ });
190
+ const result = evaluateTransitions(ctx);
191
+ // isParentIssue is true, so conditional transition matches
192
+ expect(result.action).toBe('trigger-qa');
193
+ });
194
+ it('skips conditional transition when condition is false and falls through to unconditional', () => {
195
+ const workflow = makeWorkflow({
196
+ transitions: [
197
+ { from: 'Backlog', to: 'qa', condition: '{{ isParentIssue }}', priority: 10 },
198
+ { from: 'Backlog', to: 'development' },
199
+ ],
200
+ });
201
+ const ctx = makeContext({
202
+ issue: makeIssue({ status: 'Backlog' }),
203
+ registry: registryWith(workflow),
204
+ isParentIssue: false,
205
+ });
206
+ const result = evaluateTransitions(ctx);
207
+ // isParentIssue is false, so conditional transition skipped, falls through to unconditional
208
+ expect(result.action).toBe('trigger-development');
209
+ });
210
+ it('selects first true condition when all transitions are conditional', () => {
211
+ const workflow = makeWorkflow({
212
+ transitions: [
213
+ { from: 'Backlog', to: 'development', condition: '{{ true }}' },
214
+ { from: 'Backlog', to: 'qa', condition: '{{ false }}' },
215
+ ],
216
+ });
217
+ const ctx = makeContext({
218
+ issue: makeIssue({ status: 'Backlog' }),
219
+ registry: registryWith(workflow),
220
+ });
221
+ const result = evaluateTransitions(ctx);
222
+ expect(result.action).toBe('trigger-development');
223
+ });
224
+ it('returns none when all conditional transitions evaluate to false', () => {
225
+ const workflow = makeWorkflow({
226
+ transitions: [
227
+ { from: 'Backlog', to: 'development', condition: '{{ false }}' },
228
+ { from: 'Backlog', to: 'qa', condition: '{{ false }}' },
229
+ ],
230
+ });
231
+ const ctx = makeContext({
232
+ issue: makeIssue({ status: 'Backlog' }),
233
+ registry: registryWith(workflow),
234
+ });
235
+ const result = evaluateTransitions(ctx);
236
+ expect(result.action).toBe('none');
237
+ expect(result.reason).toContain('No transition conditions satisfied');
238
+ });
239
+ it('falls through conditional to next conditional when first is false', () => {
240
+ const workflow = makeWorkflow({
241
+ transitions: [
242
+ { from: 'Backlog', to: 'development', condition: '{{ false }}', priority: 10 },
243
+ { from: 'Backlog', to: 'qa', condition: '{{ true }}', priority: 5 },
244
+ ],
245
+ });
246
+ const ctx = makeContext({
247
+ issue: makeIssue({ status: 'Backlog' }),
248
+ registry: registryWith(workflow),
249
+ });
250
+ const result = evaluateTransitions(ctx);
251
+ // First condition false, second condition true
252
+ expect(result.action).toBe('trigger-qa');
253
+ });
254
+ it('mix of conditional and unconditional transitions — conditional wins when true', () => {
255
+ const workflow = makeWorkflow({
256
+ transitions: [
257
+ { from: 'Backlog', to: 'qa', condition: '{{ true }}', priority: 10 },
258
+ { from: 'Backlog', to: 'development', priority: 5 },
259
+ ],
260
+ });
261
+ const ctx = makeContext({
262
+ issue: makeIssue({ status: 'Backlog' }),
263
+ registry: registryWith(workflow),
264
+ });
265
+ const result = evaluateTransitions(ctx);
266
+ expect(result.action).toBe('trigger-qa');
267
+ });
268
+ it('phaseState variables affect condition evaluation', () => {
269
+ const workflow = makeWorkflow({
270
+ transitions: [
271
+ { from: 'Icebox', to: 'research', condition: '{{ not researchCompleted }}', priority: 10 },
272
+ { from: 'Icebox', to: 'backlog-creation', condition: '{{ researchCompleted and not backlogCreationCompleted }}', priority: 5 },
273
+ ],
274
+ });
275
+ // No research done yet
276
+ const ctx1 = makeContext({
277
+ issue: makeIssue({ status: 'Icebox' }),
278
+ registry: registryWith(workflow),
279
+ phaseState: { researchCompleted: false, backlogCreationCompleted: false },
280
+ });
281
+ const result1 = evaluateTransitions(ctx1);
282
+ expect(result1.action).toBe('trigger-research');
283
+ // Research done, backlog not done
284
+ const ctx2 = makeContext({
285
+ issue: makeIssue({ status: 'Icebox' }),
286
+ registry: registryWith(workflow),
287
+ phaseState: { researchCompleted: true, backlogCreationCompleted: false },
288
+ });
289
+ const result2 = evaluateTransitions(ctx2);
290
+ expect(result2.action).toBe('trigger-backlog-creation');
291
+ // Both done
292
+ const ctx3 = makeContext({
293
+ issue: makeIssue({ status: 'Icebox' }),
294
+ registry: registryWith(workflow),
295
+ phaseState: { researchCompleted: true, backlogCreationCompleted: true },
296
+ });
297
+ const result3 = evaluateTransitions(ctx3);
298
+ expect(result3.action).toBe('none');
299
+ });
300
+ });
301
+ // --- Parent issue annotation ---
302
+ describe('parent issue annotation', () => {
303
+ it('includes parent note in reason for parent issues', () => {
304
+ const ctx = makeContext({
305
+ issue: makeIssue({ status: 'Backlog' }),
306
+ isParentIssue: true,
307
+ });
308
+ const result = evaluateTransitions(ctx);
309
+ expect(result.action).toBe('trigger-development');
310
+ expect(result.reason).toContain('coordination template');
311
+ });
312
+ it('does not include parent note for non-parent issues', () => {
313
+ const ctx = makeContext({
314
+ issue: makeIssue({ status: 'Backlog' }),
315
+ isParentIssue: false,
316
+ });
317
+ const result = evaluateTransitions(ctx);
318
+ expect(result.action).toBe('trigger-development');
319
+ expect(result.reason).not.toContain('coordination template');
320
+ });
321
+ });
322
+ // --- Phase-to-action mapping ---
323
+ describe('phase-to-action mapping', () => {
324
+ it('returns none for unknown phase name', () => {
325
+ const workflow = makeWorkflow({
326
+ transitions: [
327
+ { from: 'Backlog', to: 'completely-unknown-phase' },
328
+ ],
329
+ });
330
+ const ctx = makeContext({
331
+ issue: makeIssue({ status: 'Backlog' }),
332
+ registry: registryWith(workflow),
333
+ });
334
+ const result = evaluateTransitions(ctx);
335
+ expect(result.action).toBe('none');
336
+ expect(result.reason).toContain('does not map to a known GovernorAction');
337
+ });
338
+ });
339
+ // --- Built-in default workflow parity ---
340
+ describe('built-in default workflow parity', () => {
341
+ // These tests verify that the built-in workflow.yaml transitions
342
+ // produce the same GovernorActions as the hard-coded switch statement,
343
+ // for the standard unconditional transitions.
344
+ const builtinRegistry = WorkflowRegistry.create();
345
+ it('Backlog → trigger-development (matches decideBacklog)', () => {
346
+ const result = evaluateTransitions({
347
+ issue: makeIssue({ status: 'Backlog' }),
348
+ registry: builtinRegistry,
349
+ isParentIssue: false,
350
+ });
351
+ expect(result.action).toBe('trigger-development');
352
+ });
353
+ it('Finished → trigger-qa (matches decideFinished)', () => {
354
+ const result = evaluateTransitions({
355
+ issue: makeIssue({ status: 'Finished' }),
356
+ registry: builtinRegistry,
357
+ isParentIssue: false,
358
+ });
359
+ expect(result.action).toBe('trigger-qa');
360
+ });
361
+ it('Delivered → trigger-acceptance (matches decideDelivered)', () => {
362
+ const result = evaluateTransitions({
363
+ issue: makeIssue({ status: 'Delivered' }),
364
+ registry: builtinRegistry,
365
+ isParentIssue: false,
366
+ });
367
+ expect(result.action).toBe('trigger-acceptance');
368
+ });
369
+ it('Rejected → trigger-refinement (matches decideRejected)', () => {
370
+ const result = evaluateTransitions({
371
+ issue: makeIssue({ status: 'Rejected' }),
372
+ registry: builtinRegistry,
373
+ isParentIssue: false,
374
+ });
375
+ expect(result.action).toBe('trigger-refinement');
376
+ });
377
+ it('Finished + escalate-human → escalate-human (matches decideFinished)', () => {
378
+ const result = evaluateTransitions({
379
+ issue: makeIssue({ status: 'Finished' }),
380
+ registry: builtinRegistry,
381
+ workflowStrategy: 'escalate-human',
382
+ isParentIssue: false,
383
+ });
384
+ expect(result.action).toBe('escalate-human');
385
+ });
386
+ it('Rejected + decompose → decompose (matches decideRejected)', () => {
387
+ const result = evaluateTransitions({
388
+ issue: makeIssue({ status: 'Rejected' }),
389
+ registry: builtinRegistry,
390
+ workflowStrategy: 'decompose',
391
+ isParentIssue: false,
392
+ });
393
+ expect(result.action).toBe('decompose');
394
+ });
395
+ it('Icebox with phaseState routes to research when researchCompleted is false', () => {
396
+ const result = evaluateTransitions({
397
+ issue: makeIssue({ status: 'Icebox' }),
398
+ registry: builtinRegistry,
399
+ isParentIssue: false,
400
+ phaseState: { researchCompleted: false, backlogCreationCompleted: false },
401
+ });
402
+ // researchCompleted is false, so "not researchCompleted" is true → research phase
403
+ expect(result.action).toBe('trigger-research');
404
+ });
405
+ it('Icebox with phaseState routes to backlog-creation when research done', () => {
406
+ const result = evaluateTransitions({
407
+ issue: makeIssue({ status: 'Icebox' }),
408
+ registry: builtinRegistry,
409
+ isParentIssue: false,
410
+ phaseState: { researchCompleted: true, backlogCreationCompleted: false },
411
+ });
412
+ // researchCompleted is true and backlogCreationCompleted is false → backlog-creation
413
+ expect(result.action).toBe('trigger-backlog-creation');
414
+ });
415
+ it('Icebox with no phaseState routes to research (undefined vars are falsy)', () => {
416
+ const result = evaluateTransitions({
417
+ issue: makeIssue({ status: 'Icebox' }),
418
+ registry: builtinRegistry,
419
+ isParentIssue: false,
420
+ });
421
+ // No phaseState → researchCompleted is undefined → falsy → "not researchCompleted" is true
422
+ expect(result.action).toBe('trigger-research');
423
+ });
424
+ });
425
+ });
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=workflow-loader.test.d.ts.map
@@ -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
+ });