@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 +13 -10
- package/package.json +1 -1
- package/server/src/agents.js +5 -0
- package/server/src/orchestrator.js +52 -17
- package/server/src/orchestrator.test.js +128 -0
- package/server/src/workflow.js +17 -0
- package/server/src/workflow.test.js +51 -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.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",
|
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) {
|
|
@@ -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
|
|
541
|
-
=== IMPLEMENTATION
|
|
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
|
-
===
|
|
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', () => {
|
package/server/src/workflow.js
CHANGED
|
@@ -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 = {
|