@stilero/bankan 1.0.17 → 1.0.18
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 +13 -10
- package/package.json +1 -1
- package/server/src/agents.js +5 -0
- package/server/src/orchestrator.js +50 -15
- package/server/src/orchestrator.test.js +55 -0
- package/server/src/workflow.js +16 -0
- package/server/src/workflow.test.js +44 -0
package/README.md
CHANGED
|
@@ -145,19 +145,16 @@ All in one dashboard.
|
|
|
145
145
|
|
|
146
146
|
## Why Ban Kan Exists
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
When developers levels up running multiple AI coding agents they often end up juggling multiple terminals:
|
|
149
149
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
- parallel development becomes chaos
|
|
150
|
+
- Agent 1 planning a feature
|
|
151
|
+
- Agent 2 implementing code
|
|
152
|
+
- Agent 3 reviewing changes
|
|
153
|
+
- Agent 4 generating tests
|
|
155
154
|
|
|
156
|
-
|
|
155
|
+
Keeping track of everything quickly becomes overwhelming.
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
Each stage has a clear responsibility, and tasks move forward only when the previous step succeeds.
|
|
157
|
+
In practice most developers struggle to manage more than 3–4 agents at once.
|
|
161
158
|
|
|
162
159
|
<table>
|
|
163
160
|
<tr>
|
|
@@ -178,6 +175,12 @@ Each stage has a clear responsibility, and tasks move forward only when the prev
|
|
|
178
175
|
</tr>
|
|
179
176
|
</table>
|
|
180
177
|
|
|
178
|
+
Ban Kan provides a control center that lets you coordinate 10+ agents simultaneously with full visibility of tasks, stages and activity.
|
|
179
|
+
|
|
180
|
+
**a Kanban board with specialized AI agents.**
|
|
181
|
+
|
|
182
|
+
Each stage has a clear responsibility, and tasks move forward only when the previous step succeeds.
|
|
183
|
+
|
|
181
184
|
---
|
|
182
185
|
|
|
183
186
|
## Built for Agile Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stilero/bankan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run AI coding agents like a Kanban board. Plan, implement, review and ship code using parallel AI agents across your local repositories.",
|
|
6
6
|
"license": "MIT",
|
package/server/src/agents.js
CHANGED
|
@@ -45,6 +45,10 @@ const STRUCTURED_BLOCK_MARKERS = {
|
|
|
45
45
|
start: '=== REVIEW START ===',
|
|
46
46
|
end: '=== REVIEW END ===',
|
|
47
47
|
},
|
|
48
|
+
implementation: {
|
|
49
|
+
start: '=== IMPLEMENTATION RESULT START ===',
|
|
50
|
+
end: '=== IMPLEMENTATION RESULT END ===',
|
|
51
|
+
},
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
function stripAnsi(text) {
|
|
@@ -108,6 +112,7 @@ class Agent {
|
|
|
108
112
|
return {
|
|
109
113
|
plan: { pending: '', completed: null, allCompleted: [] },
|
|
110
114
|
review: { pending: '', completed: null, allCompleted: [] },
|
|
115
|
+
implementation: { pending: '', completed: null, allCompleted: [] },
|
|
111
116
|
};
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -7,7 +7,7 @@ import { loadSettings, getWorkspacesDir } from './config.js';
|
|
|
7
7
|
import store from './store.js';
|
|
8
8
|
import agentManager from './agents.js';
|
|
9
9
|
import bus from './events.js';
|
|
10
|
-
import { isReviewResultPlaceholder, isPlanPlaceholder, parseReviewResult, reviewShouldPass } from './workflow.js';
|
|
10
|
+
import { isReviewResultPlaceholder, isPlanPlaceholder, isImplementationPlaceholder, parseReviewResult, reviewShouldPass } from './workflow.js';
|
|
11
11
|
import { createSessionEntry } from './sessionHistory.js';
|
|
12
12
|
|
|
13
13
|
const POLL_INTERVAL = 4000;
|
|
@@ -112,7 +112,7 @@ export function cleanTerminalArtifacts(text) {
|
|
|
112
112
|
// truncate at that point — everything after is echoed prompt/template noise.
|
|
113
113
|
// Also strip noise lines between the real plan content and the truncation point.
|
|
114
114
|
let truncated = text;
|
|
115
|
-
for (const marker of ['=== PLAN START ===', '=== REVIEW START ===']) {
|
|
115
|
+
for (const marker of ['=== PLAN START ===', '=== REVIEW START ===', '=== IMPLEMENTATION RESULT START ===']) {
|
|
116
116
|
const firstIdx = truncated.indexOf(marker);
|
|
117
117
|
if (firstIdx !== -1) {
|
|
118
118
|
const secondIdx = truncated.indexOf(marker, firstIdx + marker.length);
|
|
@@ -280,30 +280,61 @@ export function extractReviewerReviewText(agent, options = {}) {
|
|
|
280
280
|
return result;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
export function extractImplementationResult(agent, options = {}) {
|
|
284
|
+
const startMarker = '=== IMPLEMENTATION RESULT START ===';
|
|
285
|
+
const endMarker = '=== IMPLEMENTATION RESULT END ===';
|
|
286
|
+
const result = extractStructuredStageText(agent, {
|
|
287
|
+
startMarker,
|
|
288
|
+
endMarker,
|
|
289
|
+
kind: 'implementation',
|
|
290
|
+
...options,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (result && isImplementationPlaceholder(result)) {
|
|
294
|
+
const capturedBlocks = agent.getAllCapturedBlocks?.('implementation') || [];
|
|
295
|
+
for (let i = capturedBlocks.length - 1; i >= 0; i--) {
|
|
296
|
+
if (!isImplementationPlaceholder(capturedBlocks[i])) return capturedBlocks[i];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const cleanBuf = stripAnsi(agent.getBufferString(500));
|
|
300
|
+
const blocks = getAllStructuredBlocks(cleanBuf, startMarker, endMarker);
|
|
301
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
302
|
+
if (!isImplementationPlaceholder(blocks[i])) return blocks[i];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
|
|
283
309
|
function getImplementationCompletionState(agent, taskId) {
|
|
284
|
-
const
|
|
285
|
-
const buf = agent.getBufferString(100);
|
|
310
|
+
const resultText = extractImplementationResult(agent);
|
|
286
311
|
|
|
312
|
+
if (resultText && !isImplementationPlaceholder(resultText)) {
|
|
313
|
+
const completionMarker = `=== IMPLEMENTATION COMPLETE ${taskId} ===`;
|
|
314
|
+
if (resultText.includes(completionMarker)) {
|
|
315
|
+
return { complete: true, blockedReason: null };
|
|
316
|
+
}
|
|
317
|
+
const blockedMatch = resultText.match(/=== BLOCKED: (.+?) ===/);
|
|
318
|
+
if (blockedMatch) {
|
|
319
|
+
return { complete: false, blockedReason: blockedMatch[1] };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Fallback for codex: check captured message directly
|
|
287
324
|
if (agent.cli === 'codex') {
|
|
325
|
+
const buf = agent.getBufferString(100);
|
|
288
326
|
const captured = readCapturedCodexMessage(buf, { remove: false });
|
|
289
327
|
if (captured) {
|
|
328
|
+
const completionMarker = `=== IMPLEMENTATION COMPLETE ${taskId} ===`;
|
|
290
329
|
if (captured.includes(completionMarker)) {
|
|
291
330
|
return { complete: true, blockedReason: null };
|
|
292
331
|
}
|
|
293
332
|
const blockedMatch = captured.match(/=== BLOCKED: (.+?) ===/);
|
|
294
333
|
return { complete: false, blockedReason: blockedMatch ? blockedMatch[1] : null };
|
|
295
334
|
}
|
|
296
|
-
|
|
297
|
-
return { complete: false, blockedReason: null };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const cleanBuf = stripAnsi(buf);
|
|
301
|
-
if (cleanBuf.includes(completionMarker)) {
|
|
302
|
-
return { complete: true, blockedReason: null };
|
|
303
335
|
}
|
|
304
336
|
|
|
305
|
-
|
|
306
|
-
return { complete: false, blockedReason: blockedMatch ? blockedMatch[1] : null };
|
|
337
|
+
return { complete: false, blockedReason: null };
|
|
307
338
|
}
|
|
308
339
|
|
|
309
340
|
function summarizeProcessError(prefix, err) {
|
|
@@ -537,10 +568,14 @@ Instructions:
|
|
|
537
568
|
- You are already on branch ${task.branch} in ${repoDir}
|
|
538
569
|
${promptBody}
|
|
539
570
|
- Before signaling completion, ensure ALL changes are committed to git on branch ${task.branch}
|
|
540
|
-
- When fully complete and all changes are committed, output
|
|
571
|
+
- When fully complete and all changes are committed, output the completion block below with the placeholder replaced:
|
|
572
|
+
=== IMPLEMENTATION RESULT START ===
|
|
541
573
|
=== IMPLEMENTATION COMPLETE ${task.id} ===
|
|
574
|
+
=== IMPLEMENTATION RESULT END ===
|
|
542
575
|
- If you encounter a blocker you cannot resolve, output:
|
|
543
|
-
===
|
|
576
|
+
=== IMPLEMENTATION RESULT START ===
|
|
577
|
+
=== BLOCKED: {describe the blocker here} ===
|
|
578
|
+
=== IMPLEMENTATION RESULT END ===
|
|
544
579
|
|
|
545
580
|
Begin implementation now.`;
|
|
546
581
|
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test, vi } from 'vitest';
|
|
|
3
3
|
import {
|
|
4
4
|
buildAgentCommand,
|
|
5
5
|
cleanTerminalArtifacts,
|
|
6
|
+
extractImplementationResult,
|
|
6
7
|
extractPlannerPlanText,
|
|
7
8
|
extractReviewerReviewText,
|
|
8
9
|
sanitizeBranchName,
|
|
@@ -228,6 +229,60 @@ SUMMARY: Stable review capture prevents timeout.
|
|
|
228
229
|
'Stable review capture prevents timeout.'
|
|
229
230
|
);
|
|
230
231
|
});
|
|
232
|
+
|
|
233
|
+
test('implementation extraction returns real completion block via structured capture', () => {
|
|
234
|
+
const readCaptured = vi.fn(() => null);
|
|
235
|
+
const realBlock = `=== IMPLEMENTATION RESULT START ===
|
|
236
|
+
=== IMPLEMENTATION COMPLETE T-ABC123 ===
|
|
237
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
238
|
+
const agent = {
|
|
239
|
+
cli: 'claude',
|
|
240
|
+
getBufferString: vi.fn(() => 'noise'),
|
|
241
|
+
getStructuredBlock: vi.fn(() => realBlock),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
|
|
245
|
+
expect(result).toContain('IMPLEMENTATION COMPLETE T-ABC123');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('implementation extraction rejects echoed prompt template and finds real block in history', () => {
|
|
249
|
+
const readCaptured = vi.fn(() => null);
|
|
250
|
+
const templateBlock = `=== IMPLEMENTATION RESULT START ===
|
|
251
|
+
=== IMPLEMENTATION COMPLETE {task.id} ===
|
|
252
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
253
|
+
const realBlock = `=== IMPLEMENTATION RESULT START ===
|
|
254
|
+
=== IMPLEMENTATION COMPLETE T-ABC123 ===
|
|
255
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
256
|
+
const agent = {
|
|
257
|
+
cli: 'claude',
|
|
258
|
+
getBufferString: vi.fn(() => 'noise'),
|
|
259
|
+
getStructuredBlock: vi.fn(() => templateBlock),
|
|
260
|
+
getAllCapturedBlocks: vi.fn(() => [templateBlock, realBlock, templateBlock]),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
|
|
264
|
+
expect(result).toContain('IMPLEMENTATION COMPLETE T-ABC123');
|
|
265
|
+
expect(result).not.toContain('{task.id}');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('implementation extraction falls back to buffer scan when structured capture is placeholder', () => {
|
|
269
|
+
const readCaptured = vi.fn(() => null);
|
|
270
|
+
const templateBlock = `=== IMPLEMENTATION RESULT START ===
|
|
271
|
+
=== BLOCKED: {describe the blocker here} ===
|
|
272
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
273
|
+
const realBlock = `=== IMPLEMENTATION RESULT START ===
|
|
274
|
+
=== BLOCKED: npm install failed with EACCES ===
|
|
275
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
276
|
+
const agent = {
|
|
277
|
+
cli: 'claude',
|
|
278
|
+
getBufferString: vi.fn(() => `noise\n${realBlock}\nmore noise\n${templateBlock}`),
|
|
279
|
+
getStructuredBlock: vi.fn(() => templateBlock),
|
|
280
|
+
getAllCapturedBlocks: vi.fn(() => [templateBlock]),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
|
|
284
|
+
expect(result).toContain('npm install failed with EACCES');
|
|
285
|
+
});
|
|
231
286
|
});
|
|
232
287
|
|
|
233
288
|
describe('sanitizeBranchName', () => {
|
package/server/src/workflow.js
CHANGED
|
@@ -57,6 +57,22 @@ export function isReviewResultPlaceholder(reviewText, reviewResult = parseReview
|
|
|
57
57
|
return false;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function isImplementationPlaceholder(resultText) {
|
|
61
|
+
if (typeof resultText !== 'string' || !resultText.trim()) return true;
|
|
62
|
+
const normalized = resultText.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
63
|
+
if (normalized.includes('{describe the blocker here}')) return true;
|
|
64
|
+
// The prompt template contains placeholder instruction text — if the
|
|
65
|
+
// captured block matches the template exactly it's an echo, not real output.
|
|
66
|
+
if (normalized.includes('output the completion block below with the placeholder replaced')) return true;
|
|
67
|
+
// Check for actual completion or blocked markers with real content.
|
|
68
|
+
// The prompt template contains `{task.id}` and `{describe the blocker here}` —
|
|
69
|
+
// real output has a concrete task ID (e.g. T-ABC123) or real blocker text.
|
|
70
|
+
const hasRealCompletion = /=== implementation complete t-/i.test(normalized);
|
|
71
|
+
const hasRealBlocked = /=== blocked:/.test(normalized) && !normalized.includes('{describe the blocker here}');
|
|
72
|
+
if (!hasRealCompletion && !hasRealBlocked) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
export function isPlanPlaceholder(planText) {
|
|
61
77
|
if (typeof planText !== 'string' || !planText.trim()) return true;
|
|
62
78
|
const normalized = planText.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'vitest';
|
|
|
3
3
|
import {
|
|
4
4
|
getLiveTaskAgent,
|
|
5
5
|
getAgentStage,
|
|
6
|
+
isImplementationPlaceholder,
|
|
6
7
|
isReviewResultPlaceholder,
|
|
7
8
|
isPlanPlaceholder,
|
|
8
9
|
parseReviewResult,
|
|
@@ -153,6 +154,49 @@ RISKS:
|
|
|
153
154
|
});
|
|
154
155
|
});
|
|
155
156
|
|
|
157
|
+
describe('implementation placeholder detection', () => {
|
|
158
|
+
test('echoed prompt template is detected as placeholder', () => {
|
|
159
|
+
const template = `=== IMPLEMENTATION RESULT START ===
|
|
160
|
+
=== IMPLEMENTATION COMPLETE {task.id} ===
|
|
161
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
162
|
+
expect(isImplementationPlaceholder(template)).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('blocked placeholder from prompt template is detected', () => {
|
|
166
|
+
const template = `=== IMPLEMENTATION RESULT START ===
|
|
167
|
+
=== BLOCKED: {describe the blocker here} ===
|
|
168
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
169
|
+
expect(isImplementationPlaceholder(template)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('real completion block is not a placeholder', () => {
|
|
173
|
+
const real = `=== IMPLEMENTATION RESULT START ===
|
|
174
|
+
=== IMPLEMENTATION COMPLETE T-ABC123 ===
|
|
175
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
176
|
+
expect(isImplementationPlaceholder(real)).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('real blocked result is not a placeholder', () => {
|
|
180
|
+
const real = `=== IMPLEMENTATION RESULT START ===
|
|
181
|
+
=== BLOCKED: npm install failed with EACCES ===
|
|
182
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
183
|
+
expect(isImplementationPlaceholder(real)).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('empty or blank text is a placeholder', () => {
|
|
187
|
+
expect(isImplementationPlaceholder('')).toBe(true);
|
|
188
|
+
expect(isImplementationPlaceholder(' ')).toBe(true);
|
|
189
|
+
expect(isImplementationPlaceholder(null)).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('block without completion or blocked marker is a placeholder', () => {
|
|
193
|
+
const noise = `=== IMPLEMENTATION RESULT START ===
|
|
194
|
+
some random text without markers
|
|
195
|
+
=== IMPLEMENTATION RESULT END ===`;
|
|
196
|
+
expect(isImplementationPlaceholder(noise)).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
156
200
|
describe('retry status resolution', () => {
|
|
157
201
|
test('retry ignores stale assigned agent process from another task', () => {
|
|
158
202
|
const task = {
|