@stilero/bankan 1.0.7 → 1.0.8

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.7",
3
+ "version": "1.0.8",
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",
@@ -7,7 +7,7 @@ import config, { 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 { parseReviewResult, reviewShouldPass } from './workflow.js';
10
+ import { isReviewResultPlaceholder, parseReviewResult, reviewShouldPass } from './workflow.js';
11
11
 
12
12
  const POLL_INTERVAL = 4000;
13
13
  const SIGNAL_CHECK_INTERVAL = 2500;
@@ -354,15 +354,17 @@ ${task.plan}
354
354
  Instructions:
355
355
  ${promptBody}
356
356
 
357
- Output ONLY in this exact format:
357
+ Output ONLY a completed review block in this format.
358
+ Do not copy placeholder text. Replace every field with concrete findings from the actual diff.
359
+ Do not emit the review block until after you have run the required git diff commands and finished the review.
358
360
 
359
361
  === REVIEW START ===
360
- VERDICT: PASS
362
+ VERDICT: PASS or FAIL
361
363
  CRITICAL_ISSUES:
362
- - none
364
+ - concrete issue, or none
363
365
  MINOR_ISSUES:
364
- - (issue description, or 'none')
365
- SUMMARY: (2-3 sentences summarising the review)
366
+ - concrete issue, or none
367
+ SUMMARY: 2-3 concrete sentences summarising the review, including changed files and strengths
366
368
  === REVIEW END ===`;
367
369
  }
368
370
 
@@ -721,6 +723,7 @@ async function onReviewComplete(agentId, taskId) {
721
723
  const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
722
724
  if (!reviewText) return;
723
725
  const reviewResult = parseReviewResult(reviewText);
726
+ if (isReviewResultPlaceholder(reviewText, reviewResult)) return;
724
727
  const shouldPass = reviewShouldPass(reviewResult);
725
728
 
726
729
  store.updateTask(taskId, { review: reviewText });
@@ -1081,7 +1084,21 @@ function pollLoop() {
1081
1084
  ? hasCodexStructuredOutput(buf, '=== REVIEW END ===')
1082
1085
  : buf.includes('=== REVIEW END ===');
1083
1086
  if (reviewReady) {
1084
- onReviewComplete(agent.id, taskId);
1087
+ const reviewer = agentManager.get(agent.id);
1088
+ const bufStr = reviewer?.getBufferString(100) || '';
1089
+ const captured = reviewer?.cli === 'codex' ? readCapturedCodexMessage(bufStr, { remove: false }) : null;
1090
+ const sourceText = captured || bufStr;
1091
+ const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
1092
+ const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
1093
+ if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
1094
+ onReviewComplete(agent.id, taskId);
1095
+ } else {
1096
+ store.updateTask(taskId, {
1097
+ status: 'blocked',
1098
+ blockedReason: 'Reviewer exited after returning placeholder output',
1099
+ assignedTo: null,
1100
+ });
1101
+ }
1085
1102
  } else {
1086
1103
  store.updateTask(taskId, {
1087
1104
  status: 'blocked',
@@ -1147,8 +1164,15 @@ bus.on('agent:unexpected-exit', ({ agentId, taskId }) => {
1147
1164
  ? hasCodexStructuredOutput(buf, '=== REVIEW END ===')
1148
1165
  : buf.includes('=== REVIEW END ===');
1149
1166
  if (reviewReady) {
1150
- onReviewComplete(agentId, taskId);
1151
- return;
1167
+ const captured = agent.cli === 'codex' ? readCapturedCodexMessage(buf, { remove: false }) : null;
1168
+ const sourceText = captured || buf;
1169
+ const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
1170
+ const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
1171
+ if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
1172
+ onReviewComplete(agentId, taskId);
1173
+ return;
1174
+ }
1175
+ authBlockedReason = 'Reviewer exited after returning placeholder output';
1152
1176
  }
1153
1177
  }
1154
1178
  console.error(`[unexpected-exit] agent=${agentId} task=${taskId} last output:\n${buf.slice(-500)}`);
@@ -27,11 +27,13 @@ export function parseReviewResult(reviewText) {
27
27
  const minorIssues = parseBulletList(
28
28
  extractSection(reviewText, 'MINOR_ISSUES:', ['SUMMARY:', '=== REVIEW END ==='])
29
29
  ).filter(item => item.toLowerCase() !== 'none');
30
+ const summary = extractSection(reviewText, 'SUMMARY:', ['=== REVIEW END ===']);
30
31
 
31
32
  return {
32
33
  verdict,
33
34
  criticalIssues,
34
35
  minorIssues,
36
+ summary,
35
37
  hasCriticalIssues: criticalIssues.length > 0,
36
38
  };
37
39
  }
@@ -40,6 +42,20 @@ export function reviewShouldPass(reviewResult) {
40
42
  return reviewResult.verdict === 'PASS' || !reviewResult.hasCriticalIssues;
41
43
  }
42
44
 
45
+ export function isReviewResultPlaceholder(reviewText, reviewResult = parseReviewResult(reviewText)) {
46
+ if (typeof reviewText !== 'string' || !reviewText.trim()) return true;
47
+
48
+ const normalized = reviewText.replace(/\s+/g, ' ').trim().toLowerCase();
49
+ if (normalized.includes("(issue description, or 'none')")) return true;
50
+ if (normalized.includes('(2-3 sentences summarising the review)')) return true;
51
+
52
+ const summary = (reviewResult.summary || '').replace(/\s+/g, ' ').trim().toLowerCase();
53
+ if (!summary) return true;
54
+ if (summary.includes('2-3 sentences summarising the review')) return true;
55
+
56
+ return false;
57
+ }
58
+
43
59
  export function getLiveTaskAgent(task, agentManager) {
44
60
  if (!task?.assignedTo) return null;
45
61
  const agent = agentManager.get(task.assignedTo);
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
3
3
 
4
4
  import {
5
5
  getLiveTaskAgent,
6
+ isReviewResultPlaceholder,
6
7
  parseReviewResult,
7
8
  reviewShouldPass,
8
9
  stageToRetryStatus,
@@ -42,6 +43,36 @@ SUMMARY: A must-fix issue remains.
42
43
  assert.equal(reviewShouldPass(result), false);
43
44
  });
44
45
 
46
+ test('placeholder review template is rejected', () => {
47
+ const reviewText = `=== REVIEW START ===
48
+ VERDICT: PASS
49
+ CRITICAL_ISSUES:
50
+ - none
51
+ MINOR_ISSUES:
52
+ - (issue description, or 'none')
53
+ SUMMARY: (2-3 sentences summarising the review)
54
+ === REVIEW END ===`;
55
+
56
+ const result = parseReviewResult(reviewText);
57
+
58
+ assert.equal(isReviewResultPlaceholder(reviewText, result), true);
59
+ });
60
+
61
+ test('concrete review output is not treated as placeholder', () => {
62
+ const reviewText = `=== REVIEW START ===
63
+ VERDICT: PASS
64
+ CRITICAL_ISSUES:
65
+ - none
66
+ MINOR_ISSUES:
67
+ - none
68
+ SUMMARY: Changed files: server/src/orchestrator.js, server/src/workflow.js. The review completion gate now rejects placeholder output and the branch otherwise looks consistent with existing task flow. Strengths: the change is narrowly scoped and adds regression coverage.
69
+ === REVIEW END ===`;
70
+
71
+ const result = parseReviewResult(reviewText);
72
+
73
+ assert.equal(isReviewResultPlaceholder(reviewText, result), false);
74
+ });
75
+
45
76
  test('retry ignores stale assigned agent process from another task', () => {
46
77
  const task = {
47
78
  id: 'T-123',