@stilero/bankan 1.0.21 → 1.1.2
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/client/dist/assets/index-DBbtVfOb.js +50 -0
- package/client/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/src/capabilities.js +42 -0
- package/server/src/capabilities.test.js +50 -0
- package/server/src/index.js +67 -1
- package/server/src/index.test.js +65 -0
- package/server/src/orchestrator.js +85 -5
- package/server/src/orchestrator.pr.test.js +177 -0
- package/server/src/store.js +10 -1
- package/server/src/store.test.js +5 -0
- package/server/src/workflow.js +35 -1
- package/server/src/workflow.test.js +51 -2
- package/client/dist/assets/index-loJ8KKB-.js +0 -50
package/server/src/store.js
CHANGED
|
@@ -13,7 +13,7 @@ function statusToStage(status) {
|
|
|
13
13
|
if (['workspace_setup', 'planning', 'awaiting_approval'].includes(status)) return 'planning';
|
|
14
14
|
if (['queued', 'implementing'].includes(status)) return 'implementation';
|
|
15
15
|
if (status === 'review') return 'review';
|
|
16
|
-
if (status === 'done') return 'done';
|
|
16
|
+
if (status === 'done' || status === 'awaiting_manual_pr') return 'done';
|
|
17
17
|
if (['backlog', 'aborted'].includes(status)) return 'backlog';
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
@@ -61,6 +61,7 @@ class TaskStore {
|
|
|
61
61
|
this.tasks = this.tasks.map(task => {
|
|
62
62
|
const normalized = {
|
|
63
63
|
reviewCycleCount: 0,
|
|
64
|
+
maxReviewCycles: 3,
|
|
64
65
|
lastActiveStage: statusToStage(task.status) || 'backlog',
|
|
65
66
|
previousStatus: null,
|
|
66
67
|
totalTokens: 0,
|
|
@@ -80,6 +81,9 @@ class TaskStore {
|
|
|
80
81
|
if (typeof normalized.reviewCycleCount !== 'number' || normalized.reviewCycleCount < 0) {
|
|
81
82
|
normalized.reviewCycleCount = 0;
|
|
82
83
|
}
|
|
84
|
+
if (typeof normalized.maxReviewCycles !== 'number' || normalized.maxReviewCycles < 1) {
|
|
85
|
+
normalized.maxReviewCycles = 3;
|
|
86
|
+
}
|
|
83
87
|
if (typeof normalized.totalTokens !== 'number' || normalized.totalTokens < 0) {
|
|
84
88
|
normalized.totalTokens = 0;
|
|
85
89
|
}
|
|
@@ -130,6 +134,7 @@ class TaskStore {
|
|
|
130
134
|
blockedReason: null,
|
|
131
135
|
workspacePath: null,
|
|
132
136
|
reviewCycleCount: 0,
|
|
137
|
+
maxReviewCycles: 3,
|
|
133
138
|
lastActiveStage: 'backlog',
|
|
134
139
|
previousStatus: null,
|
|
135
140
|
totalTokens: 0,
|
|
@@ -245,6 +250,10 @@ class TaskStore {
|
|
|
245
250
|
task.reviewCycleCount = 0;
|
|
246
251
|
changed = true;
|
|
247
252
|
}
|
|
253
|
+
if (typeof task.maxReviewCycles !== 'number' || task.maxReviewCycles < 1) {
|
|
254
|
+
task.maxReviewCycles = 3;
|
|
255
|
+
changed = true;
|
|
256
|
+
}
|
|
248
257
|
if (typeof task.totalTokens !== 'number' || task.totalTokens < 0) {
|
|
249
258
|
task.totalTokens = 0;
|
|
250
259
|
changed = true;
|
package/server/src/store.test.js
CHANGED
|
@@ -26,6 +26,7 @@ describe('TaskStore persistence and recovery', () => {
|
|
|
26
26
|
expect(task.status).toBe('backlog');
|
|
27
27
|
expect(task.lastActiveStage).toBe('backlog');
|
|
28
28
|
expect(task.log.at(-1).message).toBe('Task created');
|
|
29
|
+
expect(task.maxReviewCycles).toBe(3);
|
|
29
30
|
|
|
30
31
|
store.updateTask(task.id, { status: 'planning', assignedTo: 'plan-1' });
|
|
31
32
|
store.appendLog(task.id, 'Planner started');
|
|
@@ -125,6 +126,7 @@ describe('TaskStore persistence and recovery', () => {
|
|
|
125
126
|
status: 'awaiting_human_review',
|
|
126
127
|
repoPath: '/repo',
|
|
127
128
|
reviewCycleCount: -2,
|
|
129
|
+
maxReviewCycles: -1,
|
|
128
130
|
totalTokens: -10,
|
|
129
131
|
startedAt: 42,
|
|
130
132
|
completedAt: 24,
|
|
@@ -143,6 +145,7 @@ describe('TaskStore persistence and recovery', () => {
|
|
|
143
145
|
expect(task.assignedTo).toBeNull();
|
|
144
146
|
expect(task.workspacePath).toBeNull();
|
|
145
147
|
expect(task.reviewCycleCount).toBe(0);
|
|
148
|
+
expect(task.maxReviewCycles).toBe(3);
|
|
146
149
|
expect(task.totalTokens).toBe(0);
|
|
147
150
|
expect(task.startedAt).toBeNull();
|
|
148
151
|
expect(task.completedAt).toBeNull();
|
|
@@ -170,6 +173,7 @@ describe('TaskStore persistence and recovery', () => {
|
|
|
170
173
|
blockedReason: 'Invalid planner working directory: git@github.com:stilero/bankan.git',
|
|
171
174
|
workspacePath: null,
|
|
172
175
|
reviewCycleCount: -1,
|
|
176
|
+
maxReviewCycles: -1,
|
|
173
177
|
totalTokens: -1,
|
|
174
178
|
lastActiveStage: null,
|
|
175
179
|
});
|
|
@@ -181,6 +185,7 @@ describe('TaskStore persistence and recovery', () => {
|
|
|
181
185
|
expect(recovered.previousStatus).toBeNull();
|
|
182
186
|
expect(recovered.lastActiveStage).toBe('backlog');
|
|
183
187
|
expect(recovered.reviewCycleCount).toBe(0);
|
|
188
|
+
expect(recovered.maxReviewCycles).toBe(3);
|
|
184
189
|
expect(recovered.totalTokens).toBe(0);
|
|
185
190
|
});
|
|
186
191
|
});
|
package/server/src/workflow.js
CHANGED
|
@@ -42,6 +42,40 @@ export function reviewShouldPass(reviewResult) {
|
|
|
42
42
|
return reviewResult.verdict === 'PASS' || !reviewResult.hasCriticalIssues;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export function isMaxReviewCyclesBlocker(blockedReason) {
|
|
46
|
+
if (typeof blockedReason !== 'string') return false;
|
|
47
|
+
return /^Reached maximum review cycles(?: \(\d+\))?/i.test(blockedReason.trim());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveTaskMaxReviewCycles(task, fallback = 3) {
|
|
51
|
+
const configuredValue = task?.maxReviewCycles;
|
|
52
|
+
if (typeof configuredValue === 'number' && configuredValue >= 1) {
|
|
53
|
+
return configuredValue;
|
|
54
|
+
}
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildMaxReviewBlockerApprovalUpdate(task, now = new Date().toISOString()) {
|
|
59
|
+
if (task?.status !== 'blocked' || !isMaxReviewCyclesBlocker(task?.blockedReason)) return null;
|
|
60
|
+
return {
|
|
61
|
+
status: 'done',
|
|
62
|
+
blockedReason: null,
|
|
63
|
+
assignedTo: null,
|
|
64
|
+
workspacePath: null,
|
|
65
|
+
completedAt: task.completedAt || now,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildMaxReviewBlockerExtensionUpdate(task) {
|
|
70
|
+
if (task?.status !== 'blocked' || !isMaxReviewCyclesBlocker(task?.blockedReason)) return null;
|
|
71
|
+
return {
|
|
72
|
+
status: 'queued',
|
|
73
|
+
blockedReason: null,
|
|
74
|
+
assignedTo: null,
|
|
75
|
+
maxReviewCycles: resolveTaskMaxReviewCycles(task) + 1,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
45
79
|
export function isReviewResultPlaceholder(reviewText, reviewResult = parseReviewResult(reviewText)) {
|
|
46
80
|
if (typeof reviewText !== 'string' || !reviewText.trim()) return true;
|
|
47
81
|
|
|
@@ -123,7 +157,7 @@ export function stageToRetryStatus(task, { planningDisabled = false, liveAgent =
|
|
|
123
157
|
if (stage === 'review') return 'review';
|
|
124
158
|
}
|
|
125
159
|
|
|
126
|
-
if ((task.blockedReason || '')
|
|
160
|
+
if (isMaxReviewCyclesBlocker(task.blockedReason || '')) {
|
|
127
161
|
return 'queued';
|
|
128
162
|
}
|
|
129
163
|
if (task.lastActiveStage === 'review') {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
buildMaxReviewBlockerApprovalUpdate,
|
|
5
|
+
buildMaxReviewBlockerExtensionUpdate,
|
|
5
6
|
getAgentStage,
|
|
7
|
+
getLiveTaskAgent,
|
|
6
8
|
isImplementationPlaceholder,
|
|
7
|
-
|
|
9
|
+
isMaxReviewCyclesBlocker,
|
|
8
10
|
isPlanPlaceholder,
|
|
11
|
+
isReviewResultPlaceholder,
|
|
9
12
|
parseReviewResult,
|
|
13
|
+
resolveTaskMaxReviewCycles,
|
|
10
14
|
reviewShouldPass,
|
|
11
15
|
stageToRetryStatus,
|
|
12
16
|
} from './workflow.js';
|
|
@@ -270,6 +274,51 @@ describe('retry status resolution', () => {
|
|
|
270
274
|
}, { planningDisabled: false })).toBe('queued');
|
|
271
275
|
});
|
|
272
276
|
|
|
277
|
+
test('maximum review cycle blockers are detected from the canonical message', () => {
|
|
278
|
+
expect(isMaxReviewCyclesBlocker('Reached maximum review cycles (3). Human input required.')).toBe(true);
|
|
279
|
+
expect(isMaxReviewCyclesBlocker('Reached maximum review cycles (3) Human input required.')).toBe(true);
|
|
280
|
+
expect(isMaxReviewCyclesBlocker('Invalid workspace path for review: /tmp/workspace')).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('builds approval and extension updates for max review blockers', () => {
|
|
284
|
+
const task = {
|
|
285
|
+
status: 'blocked',
|
|
286
|
+
blockedReason: 'Reached maximum review cycles (3). Human input required.',
|
|
287
|
+
maxReviewCycles: 3,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const approved = buildMaxReviewBlockerApprovalUpdate(task);
|
|
291
|
+
const extended = buildMaxReviewBlockerExtensionUpdate(task);
|
|
292
|
+
|
|
293
|
+
expect(approved).toMatchObject({
|
|
294
|
+
status: 'done',
|
|
295
|
+
blockedReason: null,
|
|
296
|
+
assignedTo: null,
|
|
297
|
+
workspacePath: null,
|
|
298
|
+
});
|
|
299
|
+
expect(typeof approved.completedAt).toBe('string');
|
|
300
|
+
expect(extended).toEqual({
|
|
301
|
+
status: 'queued',
|
|
302
|
+
blockedReason: null,
|
|
303
|
+
assignedTo: null,
|
|
304
|
+
maxReviewCycles: 4,
|
|
305
|
+
});
|
|
306
|
+
expect(buildMaxReviewBlockerApprovalUpdate({
|
|
307
|
+
status: 'blocked',
|
|
308
|
+
blockedReason: 'Different blocker',
|
|
309
|
+
})).toBeNull();
|
|
310
|
+
expect(buildMaxReviewBlockerExtensionUpdate({
|
|
311
|
+
status: 'review',
|
|
312
|
+
blockedReason: 'Reached maximum review cycles (3). Human input required.',
|
|
313
|
+
})).toBeNull();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('preserves an existing per-task review cap and falls back only when missing or invalid', () => {
|
|
317
|
+
expect(resolveTaskMaxReviewCycles({ maxReviewCycles: 7 }, 3)).toBe(7);
|
|
318
|
+
expect(resolveTaskMaxReviewCycles({ maxReviewCycles: 0 }, 3)).toBe(3);
|
|
319
|
+
expect(resolveTaskMaxReviewCycles({}, 3)).toBe(3);
|
|
320
|
+
});
|
|
321
|
+
|
|
273
322
|
test('live planner and reviewer agents preserve their active stage', () => {
|
|
274
323
|
expect(stageToRetryStatus({
|
|
275
324
|
lastActiveStage: 'implementation',
|