@stilero/bankan 1.0.18 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stilero/bankan",
3
- "version": "1.0.18",
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",
@@ -545,7 +545,7 @@ RISKS:
545
545
  return prompt;
546
546
  }
547
547
 
548
- function buildImplementorPrompt(task, workspacePath) {
548
+ export function buildImplementorPrompt(task, workspacePath) {
549
549
  const repoDir = workspacePath || task.repoPath;
550
550
  const promptBody = getPromptBody('implementation');
551
551
  let prompt = `You are an expert software engineer implementing a feature on a real codebase.
@@ -568,9 +568,9 @@ Instructions:
568
568
  - You are already on branch ${task.branch} in ${repoDir}
569
569
  ${promptBody}
570
570
  - Before signaling completion, ensure ALL changes are committed to git on branch ${task.branch}
571
- - When fully complete and all changes are committed, output the completion block below with the placeholder replaced:
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
572
  === IMPLEMENTATION RESULT START ===
573
- === IMPLEMENTATION COMPLETE ${task.id} ===
573
+ === IMPLEMENTATION COMPLETE {TASK_ID} ===
574
574
  === IMPLEMENTATION RESULT END ===
575
575
  - If you encounter a blocker you cannot resolve, output:
576
576
  === IMPLEMENTATION RESULT START ===
@@ -2,12 +2,14 @@ import { describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  import {
4
4
  buildAgentCommand,
5
+ buildImplementorPrompt,
5
6
  cleanTerminalArtifacts,
6
7
  extractImplementationResult,
7
8
  extractPlannerPlanText,
8
9
  extractReviewerReviewText,
9
10
  sanitizeBranchName,
10
11
  } from './orchestrator.js';
12
+ import { isImplementationPlaceholder } from './workflow.js';
11
13
 
12
14
  describe('structured output extraction', () => {
13
15
  test('planner extraction falls back to agent structured capture when the PTY tail lost the full block', () => {
@@ -265,6 +267,49 @@ SUMMARY: Stable review capture prevents timeout.
265
267
  expect(result).not.toContain('{task.id}');
266
268
  });
267
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
+
268
313
  test('implementation extraction falls back to buffer scan when structured capture is placeholder', () => {
269
314
  const readCaptured = vi.fn(() => null);
270
315
  const templateBlock = `=== IMPLEMENTATION RESULT START ===
@@ -285,6 +330,34 @@ SUMMARY: Stable review capture prevents timeout.
285
330
  });
286
331
  });
287
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
+ });
359
+ });
360
+
288
361
  describe('sanitizeBranchName', () => {
289
362
  test('strips garbage text appended by ANSI cursor collapse', () => {
290
363
  expect(sanitizeBranchName('feature/t-a811ca-reporting FILES_TO_MODIFY:'))
@@ -61,6 +61,7 @@ export function isImplementationPlaceholder(resultText) {
61
61
  if (typeof resultText !== 'string' || !resultText.trim()) return true;
62
62
  const normalized = resultText.replace(/\s+/g, ' ').trim().toLowerCase();
63
63
  if (normalized.includes('{describe the blocker here}')) return true;
64
+ if (normalized.includes('{task_id}')) return true;
64
65
  // The prompt template contains placeholder instruction text — if the
65
66
  // captured block matches the template exactly it's an echo, not real output.
66
67
  if (normalized.includes('output the completion block below with the placeholder replaced')) return true;
@@ -195,6 +195,13 @@ some random text without markers
195
195
  === IMPLEMENTATION RESULT END ===`;
196
196
  expect(isImplementationPlaceholder(noise)).toBe(true);
197
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
+ });
198
205
  });
199
206
 
200
207
  describe('retry status resolution', () => {