@stilero/bankan 1.0.17 → 1.0.19

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.19",
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) {
@@ -514,7 +545,7 @@ RISKS:
514
545
  return prompt;
515
546
  }
516
547
 
517
- function buildImplementorPrompt(task, workspacePath) {
548
+ export function buildImplementorPrompt(task, workspacePath) {
518
549
  const repoDir = workspacePath || task.repoPath;
519
550
  const promptBody = getPromptBody('implementation');
520
551
  let prompt = `You are an expert software engineer implementing a feature on a real codebase.
@@ -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:
541
- === IMPLEMENTATION COMPLETE ${task.id} ===
571
+ - When fully complete and all changes are committed, output the completion block below replace {TASK_ID} with the actual TASK ID shown above:
572
+ === IMPLEMENTATION RESULT START ===
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
 
@@ -2,11 +2,14 @@ import { describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  import {
4
4
  buildAgentCommand,
5
+ buildImplementorPrompt,
5
6
  cleanTerminalArtifacts,
7
+ extractImplementationResult,
6
8
  extractPlannerPlanText,
7
9
  extractReviewerReviewText,
8
10
  sanitizeBranchName,
9
11
  } from './orchestrator.js';
12
+ import { isImplementationPlaceholder } from './workflow.js';
10
13
 
11
14
  describe('structured output extraction', () => {
12
15
  test('planner extraction falls back to agent structured capture when the PTY tail lost the full block', () => {
@@ -228,6 +231,131 @@ SUMMARY: Stable review capture prevents timeout.
228
231
  'Stable review capture prevents timeout.'
229
232
  );
230
233
  });
234
+
235
+ test('implementation extraction returns real completion block via structured capture', () => {
236
+ const readCaptured = vi.fn(() => null);
237
+ const realBlock = `=== IMPLEMENTATION RESULT START ===
238
+ === IMPLEMENTATION COMPLETE T-ABC123 ===
239
+ === IMPLEMENTATION RESULT END ===`;
240
+ const agent = {
241
+ cli: 'claude',
242
+ getBufferString: vi.fn(() => 'noise'),
243
+ getStructuredBlock: vi.fn(() => realBlock),
244
+ };
245
+
246
+ const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
247
+ expect(result).toContain('IMPLEMENTATION COMPLETE T-ABC123');
248
+ });
249
+
250
+ test('implementation extraction rejects echoed prompt template and finds real block in history', () => {
251
+ const readCaptured = vi.fn(() => null);
252
+ const templateBlock = `=== IMPLEMENTATION RESULT START ===
253
+ === IMPLEMENTATION COMPLETE {task.id} ===
254
+ === IMPLEMENTATION RESULT END ===`;
255
+ const realBlock = `=== IMPLEMENTATION RESULT START ===
256
+ === IMPLEMENTATION COMPLETE T-ABC123 ===
257
+ === IMPLEMENTATION RESULT END ===`;
258
+ const agent = {
259
+ cli: 'claude',
260
+ getBufferString: vi.fn(() => 'noise'),
261
+ getStructuredBlock: vi.fn(() => templateBlock),
262
+ getAllCapturedBlocks: vi.fn(() => [templateBlock, realBlock, templateBlock]),
263
+ };
264
+
265
+ const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
266
+ expect(result).toContain('IMPLEMENTATION COMPLETE T-ABC123');
267
+ expect(result).not.toContain('{task.id}');
268
+ });
269
+
270
+ test('implementation extraction rejects echoed prompt with {TASK_ID} placeholder and finds real block', () => {
271
+ // After the fix, the prompt template uses {TASK_ID} instead of the
272
+ // interpolated task ID, so the streaming parser captures a block with
273
+ // {TASK_ID} which isImplementationPlaceholder correctly rejects.
274
+ const readCaptured = vi.fn(() => null);
275
+ const echoedBlock = `=== IMPLEMENTATION RESULT START ===
276
+ === IMPLEMENTATION COMPLETE {TASK_ID} ===
277
+ === IMPLEMENTATION RESULT END ===`;
278
+ const realBlock = `=== IMPLEMENTATION RESULT START ===
279
+ === IMPLEMENTATION COMPLETE T-ABC123 ===
280
+ === IMPLEMENTATION RESULT END ===`;
281
+ const agent = {
282
+ cli: 'claude',
283
+ getBufferString: vi.fn(() => 'noise'),
284
+ getStructuredBlock: vi.fn(() => echoedBlock),
285
+ getAllCapturedBlocks: vi.fn(() => [echoedBlock, realBlock]),
286
+ };
287
+
288
+ const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
289
+ expect(result).toContain('IMPLEMENTATION COMPLETE T-ABC123');
290
+ expect(result).not.toContain('{TASK_ID}');
291
+ });
292
+
293
+ test('implementation extraction returns null when only echoed {TASK_ID} block exists', () => {
294
+ // When the agent has only echoed the prompt and hasn't produced real
295
+ // output yet, extraction should return the placeholder (which the
296
+ // signal checker will then reject via isImplementationPlaceholder).
297
+ const readCaptured = vi.fn(() => null);
298
+ const echoedBlock = `=== IMPLEMENTATION RESULT START ===
299
+ === IMPLEMENTATION COMPLETE {TASK_ID} ===
300
+ === IMPLEMENTATION RESULT END ===`;
301
+ const agent = {
302
+ cli: 'claude',
303
+ getBufferString: vi.fn(() => echoedBlock),
304
+ getStructuredBlock: vi.fn(() => echoedBlock),
305
+ getAllCapturedBlocks: vi.fn(() => [echoedBlock]),
306
+ };
307
+
308
+ const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
309
+ // Should return the placeholder block (caller checks isImplementationPlaceholder)
310
+ expect(result).toContain('{TASK_ID}');
311
+ });
312
+
313
+ test('implementation extraction falls back to buffer scan when structured capture is placeholder', () => {
314
+ const readCaptured = vi.fn(() => null);
315
+ const templateBlock = `=== IMPLEMENTATION RESULT START ===
316
+ === BLOCKED: {describe the blocker here} ===
317
+ === IMPLEMENTATION RESULT END ===`;
318
+ const realBlock = `=== IMPLEMENTATION RESULT START ===
319
+ === BLOCKED: npm install failed with EACCES ===
320
+ === IMPLEMENTATION RESULT END ===`;
321
+ const agent = {
322
+ cli: 'claude',
323
+ getBufferString: vi.fn(() => `noise\n${realBlock}\nmore noise\n${templateBlock}`),
324
+ getStructuredBlock: vi.fn(() => templateBlock),
325
+ getAllCapturedBlocks: vi.fn(() => [templateBlock]),
326
+ };
327
+
328
+ const result = extractImplementationResult(agent, { readCapturedCodexMessage: readCaptured });
329
+ expect(result).toContain('npm install failed with EACCES');
330
+ });
331
+ });
332
+
333
+ describe('implementation prompt echo safety', () => {
334
+ test('completion block in implementor prompt is detected as placeholder by isImplementationPlaceholder', () => {
335
+ // This is the core bug: the prompt template interpolates ${task.id} into
336
+ // the example completion block. When the CLI echoes the prompt, the
337
+ // streaming parser captures a block with the real task ID, and
338
+ // isImplementationPlaceholder fails to detect it as a template echo.
339
+ const task = {
340
+ id: 'T-4F66CF',
341
+ title: 'Reporting',
342
+ branch: 'feature/t-4f66cf-reporting',
343
+ plan: 'Add reporting feature',
344
+ };
345
+ const prompt = buildImplementorPrompt(task, '/tmp/workspace');
346
+
347
+ // Extract the completion block from the prompt the same way the
348
+ // streaming parser would when the CLI echoes the prompt.
349
+ const startMarker = '=== IMPLEMENTATION RESULT START ===';
350
+ const endMarker = '=== IMPLEMENTATION RESULT END ===';
351
+ const startIdx = prompt.indexOf(startMarker);
352
+ const endIdx = prompt.indexOf(endMarker, startIdx);
353
+ const echoedBlock = prompt.slice(startIdx, endIdx + endMarker.length);
354
+
355
+ // The echoed completion block from the prompt MUST be detected as a
356
+ // placeholder — otherwise the signal checker treats it as real completion.
357
+ expect(isImplementationPlaceholder(echoedBlock)).toBe(true);
358
+ });
231
359
  });
232
360
 
233
361
  describe('sanitizeBranchName', () => {
@@ -57,6 +57,23 @@ 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
+ if (normalized.includes('{task_id}')) return true;
65
+ // The prompt template contains placeholder instruction text — if the
66
+ // captured block matches the template exactly it's an echo, not real output.
67
+ if (normalized.includes('output the completion block below with the placeholder replaced')) return true;
68
+ // Check for actual completion or blocked markers with real content.
69
+ // The prompt template contains `{task.id}` and `{describe the blocker here}` —
70
+ // real output has a concrete task ID (e.g. T-ABC123) or real blocker text.
71
+ const hasRealCompletion = /=== implementation complete t-/i.test(normalized);
72
+ const hasRealBlocked = /=== blocked:/.test(normalized) && !normalized.includes('{describe the blocker here}');
73
+ if (!hasRealCompletion && !hasRealBlocked) return true;
74
+ return false;
75
+ }
76
+
60
77
  export function isPlanPlaceholder(planText) {
61
78
  if (typeof planText !== 'string' || !planText.trim()) return true;
62
79
  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,56 @@ 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
+ test('echoed prompt with {TASK_ID} placeholder is detected as placeholder', () => {
200
+ const echoedWithPlaceholder = `=== IMPLEMENTATION RESULT START ===
201
+ === IMPLEMENTATION COMPLETE {TASK_ID} ===
202
+ === IMPLEMENTATION RESULT END ===`;
203
+ expect(isImplementationPlaceholder(echoedWithPlaceholder)).toBe(true);
204
+ });
205
+ });
206
+
156
207
  describe('retry status resolution', () => {
157
208
  test('retry ignores stale assigned agent process from another task', () => {
158
209
  const task = {