@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 CHANGED
@@ -145,19 +145,16 @@ All in one dashboard.
145
145
 
146
146
  ## Why Ban Kan Exists
147
147
 
148
- Most AI coding workflows eventually break down in the same way:
148
+ When developers levels up running multiple AI coding agents they often end up juggling multiple terminals:
149
149
 
150
- - one giant prompt tries to do planning, coding, and review
151
- - context grows and token usage explodes
152
- - agents overwrite each other’s work
153
- - there is no clear review stage
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
- Ban Kan fixes this with a model developers already understand:
155
+ Keeping track of everything quickly becomes overwhelming.
157
156
 
158
- **a Kanban board with specialized AI agents.**
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.17",
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",
@@ -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 completionMarker = `=== IMPLEMENTATION COMPLETE ${taskId} ===`;
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
- const blockedMatch = cleanBuf.match(/=== BLOCKED: (.+?) ===/);
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 this exact string on its own line:
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
- === BLOCKED: {reason} ===
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', () => {
@@ -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 = {