@stilero/bankan 1.0.13 → 1.0.17
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 +17 -1
- package/bin/bankan.js +1 -1
- package/client/dist/assets/{index-pUZAEGtO.js → index-CHxyLFN_.js} +17 -15
- package/client/dist/index.html +1 -1
- package/docs/images/workflow/taskflow_animated.gif +0 -0
- package/package.json +14 -2
- package/scripts/setup.js +1 -5
- package/server/src/agents.js +123 -4
- package/server/src/agents.test.js +462 -76
- package/server/src/config.js +11 -4
- package/server/src/config.test.js +170 -0
- package/server/src/index.js +11 -2
- package/server/src/linting.test.js +37 -0
- package/server/src/orchestrator.js +279 -99
- package/server/src/orchestrator.test.js +431 -0
- package/server/src/paths.test.js +49 -0
- package/server/src/sessionHistory.test.js +39 -0
- package/server/src/store.js +2 -3
- package/server/src/store.test.js +186 -0
- package/server/src/workflow.js +23 -7
- package/server/src/workflow.test.js +216 -71
|
@@ -3,7 +3,7 @@ import { rm } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { simpleGit } from 'simple-git';
|
|
6
|
-
import
|
|
6
|
+
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';
|
|
@@ -23,8 +23,13 @@ let signalTimer = null;
|
|
|
23
23
|
|
|
24
24
|
function stripAnsi(text) {
|
|
25
25
|
if (typeof text !== 'string') return text;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
// Replace cursor forward codes (\x1b[nC) with a space to preserve word boundaries.
|
|
27
|
+
// eslint-disable-next-line no-control-regex
|
|
28
|
+
let result = text.replace(/\x1b\[\d*C/g, ' ');
|
|
29
|
+
// Strip remaining ANSI control sequences.
|
|
30
|
+
return result.replace(
|
|
31
|
+
// eslint-disable-next-line no-control-regex
|
|
32
|
+
/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|\x1b\].*?(?:\x07|\x1b\\)|\r/g,
|
|
28
33
|
''
|
|
29
34
|
);
|
|
30
35
|
}
|
|
@@ -33,31 +38,34 @@ function escapePrompt(text) {
|
|
|
33
38
|
return text.replace(/'/g, "'\\''");
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
function buildCodexExecCommand(prompt, { captureLastMessage = false, sandbox = 'read-only' } = {}) {
|
|
41
|
+
function buildCodexExecCommand(prompt, { captureLastMessage = false, sandbox = 'read-only', model = '' } = {}) {
|
|
37
42
|
const escapedPrompt = escapePrompt(prompt);
|
|
43
|
+
const modelFlag = model ? `-m ${model} ` : '';
|
|
38
44
|
if (!captureLastMessage) {
|
|
39
|
-
return `codex exec --sandbox ${sandbox} '${escapedPrompt}'`;
|
|
45
|
+
return `codex exec ${modelFlag}--sandbox ${sandbox} '${escapedPrompt}'`;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
return `tmpfile=$(mktemp); codex exec --sandbox ${sandbox} -o "$tmpfile" '${escapedPrompt}'; status=$?; printf '\\n=== CODEX_LAST_MESSAGE_FILE:%s ===\\n' "$tmpfile"; exit $status`;
|
|
48
|
+
return `tmpfile=$(mktemp); codex exec ${modelFlag}--sandbox ${sandbox} -o "$tmpfile" '${escapedPrompt}'; status=$?; printf '\\n=== CODEX_LAST_MESSAGE_FILE:%s ===\\n' "$tmpfile"; exit $status`;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
function buildAgentCommand(cliTool, prompt, mode = 'interactive') {
|
|
51
|
+
export function buildAgentCommand(cliTool, prompt, mode = 'interactive', model = '') {
|
|
46
52
|
if (cliTool === 'codex') {
|
|
47
53
|
if (mode === 'plan' || mode === 'review') {
|
|
48
|
-
return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'read-only' });
|
|
54
|
+
return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'read-only', model });
|
|
49
55
|
}
|
|
50
56
|
if (mode === 'interactive') {
|
|
51
|
-
return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'danger-full-access' });
|
|
57
|
+
return buildCodexExecCommand(prompt, { captureLastMessage: true, sandbox: 'danger-full-access', model });
|
|
52
58
|
}
|
|
53
|
-
return buildCodexExecCommand(prompt, { captureLastMessage: false, sandbox: 'read-only' });
|
|
59
|
+
return buildCodexExecCommand(prompt, { captureLastMessage: false, sandbox: 'read-only', model });
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
const modelFlag = model ? `--model ${model} ` : '';
|
|
63
|
+
|
|
64
|
+
if (mode === 'print') {
|
|
65
|
+
return `claude ${modelFlag}--print '${escapePrompt(prompt)}'`;
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
return `claude --dangerously-skip-permissions '${escapePrompt(prompt)}'`;
|
|
68
|
+
return `claude ${modelFlag}--dangerously-skip-permissions '${escapePrompt(prompt)}'`;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
function getLastStructuredBlock(text, startMarker, endMarker) {
|
|
@@ -69,6 +77,98 @@ function getLastStructuredBlock(text, startMarker, endMarker) {
|
|
|
69
77
|
return text.slice(startIdx, endIdx + endMarker.length);
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
function getAllStructuredBlocks(text, startMarker, endMarker) {
|
|
81
|
+
if (typeof text !== 'string' || !text) return [];
|
|
82
|
+
const blocks = [];
|
|
83
|
+
let searchFrom = 0;
|
|
84
|
+
while (true) {
|
|
85
|
+
const startIdx = text.indexOf(startMarker, searchFrom);
|
|
86
|
+
if (startIdx === -1) break;
|
|
87
|
+
const endIdx = text.indexOf(endMarker, startIdx + startMarker.length);
|
|
88
|
+
if (endIdx === -1) break;
|
|
89
|
+
blocks.push(text.slice(startIdx, endIdx + endMarker.length));
|
|
90
|
+
searchFrom = endIdx + endMarker.length;
|
|
91
|
+
}
|
|
92
|
+
return blocks;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Terminal UI noise patterns left behind after ANSI stripping.
|
|
96
|
+
// Matches entire lines that are purely artifacts.
|
|
97
|
+
const TERMINAL_ARTIFACT_LINE_RE = /^(?:.*(?:⏵⏵bypass|bypasspermission|shift\+tab\s*to\s*cycle)|.*Opus\s*4\.\d.*(?:│|context)|.*Claude(?:Code|Max)|.*▐▛|.*▝▜|.*[░▓█]{3,}|[─━═]{10,}|^\s*[❯›]\s*$|.*\.data\/workspaces\/T-)/i;
|
|
98
|
+
|
|
99
|
+
// Prompt echo lines: CLI echoes the full prompt and org messages into the terminal.
|
|
100
|
+
// These lines are noise when captured as part of the plan/review text.
|
|
101
|
+
const PROMPT_ECHO_LINE_RE = /^(?:❯\s+\S|.*\bOrganization:|\s*(?:Repository|Workspace):\s|.*(?:TASK\s+ID|PRIORITY):\s|.*Plan\s+Mode\s+Instructions|.*Core\s+constraints:|.*Output\s+ONLY\s+in\s+this\s+exact\s+format|.*Do\s+not\s+edit\s+files.*change\s+system\s+state|.*Treat\s+this\s+stage\s+as\s+planning\s+only)/i;
|
|
102
|
+
|
|
103
|
+
// Inline artifacts that can appear at the end of or within content lines.
|
|
104
|
+
// These are stripped from each line individually.
|
|
105
|
+
const TRAILING_ARTIFACT_RE = /\s*[❯›]\s*[─━═]{4,}.*$/;
|
|
106
|
+
const INLINE_ARTIFACT_RE = /[─━═]{10,}/g;
|
|
107
|
+
|
|
108
|
+
export function cleanTerminalArtifacts(text) {
|
|
109
|
+
if (typeof text !== 'string') return text;
|
|
110
|
+
|
|
111
|
+
// If the text contains a second === PLAN START === or === REVIEW START ===,
|
|
112
|
+
// truncate at that point — everything after is echoed prompt/template noise.
|
|
113
|
+
// Also strip noise lines between the real plan content and the truncation point.
|
|
114
|
+
let truncated = text;
|
|
115
|
+
for (const marker of ['=== PLAN START ===', '=== REVIEW START ===']) {
|
|
116
|
+
const firstIdx = truncated.indexOf(marker);
|
|
117
|
+
if (firstIdx !== -1) {
|
|
118
|
+
const secondIdx = truncated.indexOf(marker, firstIdx + marker.length);
|
|
119
|
+
if (secondIdx !== -1) {
|
|
120
|
+
truncated = truncated.slice(0, secondIdx).trimEnd();
|
|
121
|
+
// Find where real plan content ends by locating the last structured
|
|
122
|
+
// section header and its items, then strip everything after.
|
|
123
|
+
const contentLines = truncated.split('\n');
|
|
124
|
+
const planHeaderRe = /^(?:SUMMARY:|BRANCH:|FILES_TO_MODIFY:|STEPS:|TESTS_NEEDED:|RISKS:|VERDICT:|CRITICAL_ISSUES:|MINOR_ISSUES:|CHANGED_FILES:)/;
|
|
125
|
+
let lastFieldLine = contentLines.length - 1;
|
|
126
|
+
let sectionIdx = -1;
|
|
127
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
128
|
+
if (planHeaderRe.test(contentLines[i].trim())) sectionIdx = i;
|
|
129
|
+
}
|
|
130
|
+
if (sectionIdx !== -1) {
|
|
131
|
+
lastFieldLine = sectionIdx;
|
|
132
|
+
for (let i = sectionIdx + 1; i < contentLines.length; i++) {
|
|
133
|
+
const trimmed = contentLines[i].trim();
|
|
134
|
+
if (!trimmed) break;
|
|
135
|
+
if (trimmed.startsWith('- ') || /^\d+\.\s/.test(trimmed)) {
|
|
136
|
+
lastFieldLine = i;
|
|
137
|
+
} else {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
truncated = contentLines.slice(0, lastFieldLine + 1).join('\n');
|
|
143
|
+
// Re-add the end marker if it was lost
|
|
144
|
+
const endMarker = marker.replace('START', 'END');
|
|
145
|
+
if (!truncated.includes(endMarker)) {
|
|
146
|
+
truncated += '\n' + endMarker;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lines = truncated.split('\n');
|
|
153
|
+
const cleaned = lines
|
|
154
|
+
.map(line => {
|
|
155
|
+
// Trim trailing box-drawing and prompt artifacts from content lines
|
|
156
|
+
let result = line.replace(TRAILING_ARTIFACT_RE, '');
|
|
157
|
+
result = result.replace(INLINE_ARTIFACT_RE, '');
|
|
158
|
+
// Strip trailing prompt characters (❯/›) from content lines
|
|
159
|
+
result = result.replace(/\s*[❯›]\s*$/, '');
|
|
160
|
+
return result.trimEnd();
|
|
161
|
+
})
|
|
162
|
+
.filter(line => {
|
|
163
|
+
if (!line) return true; // keep blank lines
|
|
164
|
+
if (TERMINAL_ARTIFACT_LINE_RE.test(line)) return false;
|
|
165
|
+
if (PROMPT_ECHO_LINE_RE.test(line)) return false;
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
// Collapse runs of 3+ blank lines down to a single blank line
|
|
169
|
+
return cleaned.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
72
172
|
function getCodexLastMessagePath(buffer) {
|
|
73
173
|
if (typeof buffer !== 'string' || !buffer) return null;
|
|
74
174
|
const matches = [...buffer.matchAll(/=== CODEX_LAST_MESSAGE_FILE:(.+?) ===/g)];
|
|
@@ -91,9 +191,93 @@ function readCapturedCodexMessage(buffer, { remove = true } = {}) {
|
|
|
91
191
|
}
|
|
92
192
|
}
|
|
93
193
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
194
|
+
function extractStructuredStageText(agent, {
|
|
195
|
+
startMarker,
|
|
196
|
+
endMarker,
|
|
197
|
+
kind,
|
|
198
|
+
removeCaptured = false,
|
|
199
|
+
readCapturedCodexMessage: readCaptured = readCapturedCodexMessage,
|
|
200
|
+
} = {}) {
|
|
201
|
+
if (!agent) return null;
|
|
202
|
+
const bufStr = agent.getBufferString(100);
|
|
203
|
+
if (agent.cli === 'codex') {
|
|
204
|
+
const captured = readCaptured(bufStr, { remove: removeCaptured });
|
|
205
|
+
if (captured) {
|
|
206
|
+
return getLastStructuredBlock(captured, startMarker, endMarker);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const structured = agent.getStructuredBlock?.(kind) || null;
|
|
211
|
+
if (structured) return structured;
|
|
212
|
+
|
|
213
|
+
// Fallback: scan terminal buffer directly (handles edge cases
|
|
214
|
+
// where structured capture missed the block)
|
|
215
|
+
const cleanBuf = stripAnsi(agent.getBufferString(100));
|
|
216
|
+
return getLastStructuredBlock(cleanBuf, startMarker, endMarker);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function extractPlannerPlanText(agent, options = {}) {
|
|
220
|
+
const startMarker = '=== PLAN START ===';
|
|
221
|
+
const endMarker = '=== PLAN END ===';
|
|
222
|
+
const result = extractStructuredStageText(agent, {
|
|
223
|
+
startMarker,
|
|
224
|
+
endMarker,
|
|
225
|
+
kind: 'plan',
|
|
226
|
+
...options,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// If the extracted block is a placeholder (echoed prompt template),
|
|
230
|
+
// search all captured blocks first, then fall back to the full buffer.
|
|
231
|
+
// The CLI can re-render the prompt template after the real plan, causing
|
|
232
|
+
// getLastStructuredBlock to return the template instead of the real plan.
|
|
233
|
+
if (result && isPlanPlaceholder(result)) {
|
|
234
|
+
// Check all blocks captured by the agent's streaming parser
|
|
235
|
+
const capturedBlocks = agent.getAllCapturedBlocks?.('plan') || [];
|
|
236
|
+
for (let i = capturedBlocks.length - 1; i >= 0; i--) {
|
|
237
|
+
if (!isPlanPlaceholder(capturedBlocks[i])) return capturedBlocks[i];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Final fallback: scan the full terminal buffer
|
|
241
|
+
const cleanBuf = stripAnsi(agent.getBufferString(500));
|
|
242
|
+
const blocks = getAllStructuredBlocks(cleanBuf, startMarker, endMarker);
|
|
243
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
244
|
+
if (!isPlanPlaceholder(blocks[i])) return blocks[i];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function extractReviewerReviewText(agent, options = {}) {
|
|
252
|
+
const startMarker = '=== REVIEW START ===';
|
|
253
|
+
const endMarker = '=== REVIEW END ===';
|
|
254
|
+
const result = extractStructuredStageText(agent, {
|
|
255
|
+
startMarker,
|
|
256
|
+
endMarker,
|
|
257
|
+
kind: 'review',
|
|
258
|
+
...options,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Same fallback as extractPlannerPlanText: if the captured block is a
|
|
262
|
+
// placeholder (echoed prompt template), search all captured blocks and
|
|
263
|
+
// the full buffer for the real review output.
|
|
264
|
+
const reviewResult = result ? parseReviewResult(result) : null;
|
|
265
|
+
if (result && isReviewResultPlaceholder(result, reviewResult)) {
|
|
266
|
+
const capturedBlocks = agent.getAllCapturedBlocks?.('review') || [];
|
|
267
|
+
for (let i = capturedBlocks.length - 1; i >= 0; i--) {
|
|
268
|
+
const parsed = parseReviewResult(capturedBlocks[i]);
|
|
269
|
+
if (!isReviewResultPlaceholder(capturedBlocks[i], parsed)) return capturedBlocks[i];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const cleanBuf = stripAnsi(agent.getBufferString(500));
|
|
273
|
+
const blocks = getAllStructuredBlocks(cleanBuf, startMarker, endMarker);
|
|
274
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
275
|
+
const parsed = parseReviewResult(blocks[i]);
|
|
276
|
+
if (!isReviewResultPlaceholder(blocks[i], parsed)) return blocks[i];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
97
281
|
}
|
|
98
282
|
|
|
99
283
|
function getImplementationCompletionState(agent, taskId) {
|
|
@@ -192,6 +376,18 @@ function generateBranchName(task) {
|
|
|
192
376
|
return `feature/${task.id.toLowerCase()}-${slugifyTitle(task.title)}`;
|
|
193
377
|
}
|
|
194
378
|
|
|
379
|
+
// Strip garbage from branch names caused by ANSI cursor positioning collapse
|
|
380
|
+
// (e.g. "feature/t-a811ca-reporting FILES_TO_MODIFY:" → "feature/t-a811ca-reporting").
|
|
381
|
+
// Stops at the first character that's invalid in a git branch name.
|
|
382
|
+
export function sanitizeBranchName(raw) {
|
|
383
|
+
if (typeof raw !== 'string') return raw;
|
|
384
|
+
// Trim leading/trailing whitespace, then keep only valid branch chars.
|
|
385
|
+
// Git branch names allow: alphanumeric, -, _, /, .
|
|
386
|
+
// Stop at first space or other invalid char (catches appended field headers).
|
|
387
|
+
const match = raw.trim().match(/^[a-zA-Z0-9/_.-]+/);
|
|
388
|
+
return match ? match[0].replace(/\.+$/, '') : raw.trim();
|
|
389
|
+
}
|
|
390
|
+
|
|
195
391
|
function buildSyntheticPlan(task) {
|
|
196
392
|
return `=== PLAN START ===
|
|
197
393
|
SUMMARY: Planning skipped because planner max is set to 0. Implement the requested task directly.
|
|
@@ -340,7 +536,8 @@ ${task.plan}
|
|
|
340
536
|
Instructions:
|
|
341
537
|
- You are already on branch ${task.branch} in ${repoDir}
|
|
342
538
|
${promptBody}
|
|
343
|
-
-
|
|
539
|
+
- 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:
|
|
344
541
|
=== IMPLEMENTATION COMPLETE ${task.id} ===
|
|
345
542
|
- If you encounter a blocker you cannot resolve, output:
|
|
346
543
|
=== BLOCKED: {reason} ===
|
|
@@ -536,7 +733,7 @@ async function startPlanning(task) {
|
|
|
536
733
|
planner.taskLabel = `Planning: ${task.title}`;
|
|
537
734
|
|
|
538
735
|
const prompt = buildPlannerPrompt({ ...task, workspacePath });
|
|
539
|
-
const cmd = buildAgentCommand(planner.cli, prompt, 'plan');
|
|
736
|
+
const cmd = buildAgentCommand(planner.cli, prompt, 'plan', planner.model);
|
|
540
737
|
const plannerCwd = workspacePath;
|
|
541
738
|
const ok = planner.spawn(plannerCwd, cmd);
|
|
542
739
|
if (!ok) {
|
|
@@ -555,18 +752,18 @@ async function startPlanning(task) {
|
|
|
555
752
|
function onPlanComplete(agentId, taskId) {
|
|
556
753
|
const planner = agentManager.get(agentId);
|
|
557
754
|
if (!planner) return;
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
755
|
+
const rawPlanText = extractPlannerPlanText(planner, { removeCaptured: true });
|
|
756
|
+
|
|
757
|
+
if (!rawPlanText) return;
|
|
758
|
+
if (isPlanPlaceholder(rawPlanText)) return;
|
|
561
759
|
|
|
562
|
-
|
|
563
|
-
const planText = getLastStructuredBlock(sourceText, '=== PLAN START ===', '=== PLAN END ===');
|
|
564
|
-
if (!planText) return;
|
|
565
|
-
if (isPlanPlaceholder(planText)) return;
|
|
760
|
+
const planText = cleanTerminalArtifacts(rawPlanText);
|
|
566
761
|
|
|
567
762
|
// Parse branch name
|
|
568
763
|
const branchMatch = planText.match(/BRANCH:\s*(.+)/);
|
|
569
|
-
const branch = branchMatch
|
|
764
|
+
const branch = branchMatch
|
|
765
|
+
? sanitizeBranchName(branchMatch[1])
|
|
766
|
+
: generateBranchName(store.getTask(taskId) || { id: taskId, title: 'auto' });
|
|
570
767
|
|
|
571
768
|
// Save plan
|
|
572
769
|
store.savePlan(taskId, planText);
|
|
@@ -581,7 +778,7 @@ function onPlanComplete(agentId, taskId) {
|
|
|
581
778
|
assignedTo: null,
|
|
582
779
|
});
|
|
583
780
|
|
|
584
|
-
retireAgentSession(planner, { taskId, outcome: 'completed', transcript:
|
|
781
|
+
retireAgentSession(planner, { taskId, outcome: 'completed', transcript: planText });
|
|
585
782
|
bus.emit('plan:ready', { taskId, plan: planText });
|
|
586
783
|
}
|
|
587
784
|
|
|
@@ -639,7 +836,7 @@ async function startImplementation(task) {
|
|
|
639
836
|
|
|
640
837
|
const cliTool = agent.cli;
|
|
641
838
|
const prompt = buildImplementorPrompt(task, workspacePath);
|
|
642
|
-
const cmd = buildAgentCommand(cliTool, prompt, 'interactive');
|
|
839
|
+
const cmd = buildAgentCommand(cliTool, prompt, 'interactive', agent.model);
|
|
643
840
|
|
|
644
841
|
const ok = agent.spawn(workspacePath, cmd);
|
|
645
842
|
if (!ok) {
|
|
@@ -668,7 +865,7 @@ async function onImplementationComplete(agentId) {
|
|
|
668
865
|
const git = simpleGit(task.workspacePath);
|
|
669
866
|
await git.push('origin', task.branch);
|
|
670
867
|
} catch (err) {
|
|
671
|
-
console.error(
|
|
868
|
+
console.error('Git push failed:', err.message);
|
|
672
869
|
store.updateTask(taskId, {
|
|
673
870
|
status: 'blocked',
|
|
674
871
|
blockedReason: `Branch push failed: ${err.message}`,
|
|
@@ -715,7 +912,7 @@ SUMMARY: Review skipped because reviewer max is set to 0.
|
|
|
715
912
|
reviewer.status = 'active';
|
|
716
913
|
|
|
717
914
|
const prompt = buildReviewerPrompt(task);
|
|
718
|
-
const cmd = buildAgentCommand(reviewer.cli, prompt, 'review');
|
|
915
|
+
const cmd = buildAgentCommand(reviewer.cli, prompt, 'review', reviewer.model);
|
|
719
916
|
const ok = reviewer.spawn(task.workspacePath, cmd);
|
|
720
917
|
if (!ok) {
|
|
721
918
|
store.updateTask(task.id, {
|
|
@@ -732,12 +929,9 @@ SUMMARY: Review skipped because reviewer max is set to 0.
|
|
|
732
929
|
async function onReviewComplete(agentId, taskId) {
|
|
733
930
|
const reviewer = agentManager.get(agentId);
|
|
734
931
|
if (!reviewer) return;
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
const sourceText = captured || stripAnsi(bufStr);
|
|
739
|
-
const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
|
|
740
|
-
if (!reviewText) return;
|
|
932
|
+
const rawReviewText = extractReviewerReviewText(reviewer, { removeCaptured: true });
|
|
933
|
+
if (!rawReviewText) return;
|
|
934
|
+
const reviewText = cleanTerminalArtifacts(rawReviewText);
|
|
741
935
|
const reviewResult = parseReviewResult(reviewText);
|
|
742
936
|
if (isReviewResultPlaceholder(reviewText, reviewResult)) return;
|
|
743
937
|
const shouldPass = reviewShouldPass(reviewResult);
|
|
@@ -746,7 +940,7 @@ async function onReviewComplete(agentId, taskId) {
|
|
|
746
940
|
retireAgentSession(reviewer, {
|
|
747
941
|
taskId,
|
|
748
942
|
outcome: shouldPass ? 'completed' : 'failed_review',
|
|
749
|
-
transcript:
|
|
943
|
+
transcript: reviewText,
|
|
750
944
|
});
|
|
751
945
|
|
|
752
946
|
if (shouldPass) {
|
|
@@ -821,7 +1015,7 @@ async function createPR(taskId) {
|
|
|
821
1015
|
await cleanupWorkspace(store.getTask(taskId));
|
|
822
1016
|
store.updateTask(taskId, { status: 'done', assignedTo: null });
|
|
823
1017
|
} catch (err) {
|
|
824
|
-
console.error(
|
|
1018
|
+
console.error('PR creation error:', err.message);
|
|
825
1019
|
store.updateTask(taskId, {
|
|
826
1020
|
status: 'blocked',
|
|
827
1021
|
blockedReason: summarizeProcessError('PR finalization failed', err),
|
|
@@ -928,19 +1122,23 @@ function checkSignals() {
|
|
|
928
1122
|
// Check planners
|
|
929
1123
|
for (const agent of agentManager.getAgentsByRole('plan')) {
|
|
930
1124
|
if (agent.status === 'active' && agent.currentTask) {
|
|
1125
|
+
// Skip checks during initial startup to avoid matching the completion
|
|
1126
|
+
// marker in the echoed prompt text (interactive mode echoes the prompt)
|
|
1127
|
+
const elapsed = agent.startedAt ? Date.now() - agent.startedAt : 0;
|
|
1128
|
+
if (elapsed < 20000) continue;
|
|
1129
|
+
|
|
931
1130
|
const buf = agent.getBufferString(50);
|
|
932
1131
|
const cleanBuf = stripAnsi(buf);
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (planReady) {
|
|
1132
|
+
const planText = extractPlannerPlanText(agent);
|
|
1133
|
+
const hasConcretePlan = Boolean(planText && !isPlanPlaceholder(planText));
|
|
1134
|
+
if (hasConcretePlan) {
|
|
937
1135
|
onPlanComplete(agent.id, agent.currentTask);
|
|
938
1136
|
} else if (!checkTrustPrompt(agent, buf)) {
|
|
939
1137
|
// Live plan streaming
|
|
940
1138
|
if (!cleanBuf.includes('=== PLAN END ===') && cleanBuf.includes('=== PLAN START ===')) {
|
|
941
1139
|
const partial = cleanBuf.slice(cleanBuf.indexOf('=== PLAN START ==='));
|
|
942
1140
|
if (!isPlanPlaceholder(partial)) {
|
|
943
|
-
bus.emit('plan:partial', { taskId: agent.currentTask, plan: partial });
|
|
1141
|
+
bus.emit('plan:partial', { taskId: agent.currentTask, plan: cleanTerminalArtifacts(partial) });
|
|
944
1142
|
}
|
|
945
1143
|
}
|
|
946
1144
|
if (agent.startedAt && Date.now() - agent.startedAt > PLANNER_TIMEOUT) {
|
|
@@ -953,6 +1151,11 @@ function checkSignals() {
|
|
|
953
1151
|
// Check implementors
|
|
954
1152
|
for (const agent of agentManager.getAgentsByRole('imp')) {
|
|
955
1153
|
if (agent.status === 'active' && agent.currentTask) {
|
|
1154
|
+
// Skip checks during initial startup to avoid matching the completion
|
|
1155
|
+
// marker in the echoed prompt text (interactive mode echoes the prompt)
|
|
1156
|
+
const elapsed = agent.startedAt ? Date.now() - agent.startedAt : 0;
|
|
1157
|
+
if (elapsed < 20000) continue;
|
|
1158
|
+
|
|
956
1159
|
const buf = agent.getBufferString(50);
|
|
957
1160
|
const implementationState = getImplementationCompletionState(agent, agent.currentTask);
|
|
958
1161
|
if (implementationState.complete) {
|
|
@@ -982,11 +1185,18 @@ function checkSignals() {
|
|
|
982
1185
|
// Check reviewers
|
|
983
1186
|
for (const agent of agentManager.getAgentsByRole('rev')) {
|
|
984
1187
|
if (agent.status === 'active' && agent.currentTask) {
|
|
1188
|
+
// Skip checks during initial startup to avoid matching the completion
|
|
1189
|
+
// marker in the echoed prompt text (interactive mode echoes the prompt)
|
|
1190
|
+
const elapsed = agent.startedAt ? Date.now() - agent.startedAt : 0;
|
|
1191
|
+
if (elapsed < 20000) continue;
|
|
1192
|
+
|
|
985
1193
|
const buf = agent.getBufferString(50);
|
|
986
|
-
const
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1194
|
+
const reviewText = extractReviewerReviewText(agent);
|
|
1195
|
+
const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
|
|
1196
|
+
const hasConcreteReview = Boolean(
|
|
1197
|
+
reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)
|
|
1198
|
+
);
|
|
1199
|
+
if (hasConcreteReview) {
|
|
990
1200
|
onReviewComplete(agent.id, agent.currentTask);
|
|
991
1201
|
} else if (!checkTrustPrompt(agent, buf)) {
|
|
992
1202
|
if (agent.startedAt && Date.now() - agent.startedAt > REVIEWER_TIMEOUT) {
|
|
@@ -1068,25 +1278,18 @@ function pollLoop() {
|
|
|
1068
1278
|
const taskId = agent.currentTask;
|
|
1069
1279
|
const task = store.getTask(taskId);
|
|
1070
1280
|
if (task && !['blocked', 'done', 'aborted', 'backlog', 'paused', 'workspace_setup'].includes(task.status)) {
|
|
1071
|
-
const buf = agent.getBufferString(100);
|
|
1072
1281
|
const isPlanner = agent.id.startsWith('plan-');
|
|
1073
1282
|
const isImplementor = agent.id.startsWith('imp-');
|
|
1074
1283
|
const isReviewer = agent.id.startsWith('rev-');
|
|
1075
1284
|
|
|
1076
|
-
const cleanBuf = stripAnsi(buf);
|
|
1077
1285
|
if (isPlanner) {
|
|
1078
|
-
const
|
|
1079
|
-
? hasCodexStructuredOutput(buf, '=== PLAN END ===')
|
|
1080
|
-
: cleanBuf.includes('=== PLAN END ===');
|
|
1081
|
-
const planText = planReady
|
|
1082
|
-
? getLastStructuredBlock(cleanBuf, '=== PLAN START ===', '=== PLAN END ===')
|
|
1083
|
-
: null;
|
|
1286
|
+
const planText = extractPlannerPlanText(agent);
|
|
1084
1287
|
if (planText && !isPlanPlaceholder(planText)) {
|
|
1085
1288
|
onPlanComplete(agent.id, taskId);
|
|
1086
1289
|
} else {
|
|
1087
1290
|
store.updateTask(taskId, {
|
|
1088
1291
|
status: 'blocked',
|
|
1089
|
-
blockedReason:
|
|
1292
|
+
blockedReason: planText
|
|
1090
1293
|
? 'Planner exited after returning placeholder output'
|
|
1091
1294
|
: 'Agent process exited unexpectedly',
|
|
1092
1295
|
assignedTo: null,
|
|
@@ -1110,29 +1313,16 @@ function pollLoop() {
|
|
|
1110
1313
|
});
|
|
1111
1314
|
}
|
|
1112
1315
|
} else if (isReviewer) {
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const reviewer = agentManager.get(agent.id);
|
|
1118
|
-
const bufStr = reviewer?.getBufferString(100) || '';
|
|
1119
|
-
const captured = reviewer?.cli === 'codex' ? readCapturedCodexMessage(bufStr, { remove: false }) : null;
|
|
1120
|
-
const sourceText = captured || bufStr;
|
|
1121
|
-
const reviewText = getLastStructuredBlock(sourceText, '=== REVIEW START ===', '=== REVIEW END ===');
|
|
1122
|
-
const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
|
|
1123
|
-
if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
|
|
1124
|
-
onReviewComplete(agent.id, taskId);
|
|
1125
|
-
} else {
|
|
1126
|
-
store.updateTask(taskId, {
|
|
1127
|
-
status: 'blocked',
|
|
1128
|
-
blockedReason: 'Reviewer exited after returning placeholder output',
|
|
1129
|
-
assignedTo: null,
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1316
|
+
const reviewText = extractReviewerReviewText(agent);
|
|
1317
|
+
const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
|
|
1318
|
+
if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
|
|
1319
|
+
onReviewComplete(agent.id, taskId);
|
|
1132
1320
|
} else {
|
|
1133
1321
|
store.updateTask(taskId, {
|
|
1134
1322
|
status: 'blocked',
|
|
1135
|
-
blockedReason:
|
|
1323
|
+
blockedReason: reviewText
|
|
1324
|
+
? 'Reviewer exited after returning placeholder output'
|
|
1325
|
+
: 'Agent process exited unexpectedly',
|
|
1136
1326
|
assignedTo: null,
|
|
1137
1327
|
});
|
|
1138
1328
|
}
|
|
@@ -1176,17 +1366,12 @@ bus.on('agent:unexpected-exit', ({ agentId, taskId }) => {
|
|
|
1176
1366
|
const isReviewer = agentId.startsWith('rev-');
|
|
1177
1367
|
|
|
1178
1368
|
if (isPlanner) {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
const planText = getLastStructuredBlock(sourceText, '=== PLAN START ===', '=== PLAN END ===');
|
|
1186
|
-
if (planText && !isPlanPlaceholder(planText)) {
|
|
1187
|
-
onPlanComplete(agentId, taskId);
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1369
|
+
const planText = extractPlannerPlanText(agent);
|
|
1370
|
+
if (planText && !isPlanPlaceholder(planText)) {
|
|
1371
|
+
onPlanComplete(agentId, taskId);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
if (planText) {
|
|
1190
1375
|
authBlockedReason = 'Planner exited after returning placeholder output';
|
|
1191
1376
|
}
|
|
1192
1377
|
} else if (isImplementor) {
|
|
@@ -1199,18 +1384,13 @@ bus.on('agent:unexpected-exit', ({ agentId, taskId }) => {
|
|
|
1199
1384
|
authBlockedReason = implementationState.blockedReason;
|
|
1200
1385
|
}
|
|
1201
1386
|
} else if (isReviewer) {
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
|
|
1210
|
-
if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
|
|
1211
|
-
onReviewComplete(agentId, taskId);
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1387
|
+
const reviewText = extractReviewerReviewText(agent);
|
|
1388
|
+
const reviewResult = reviewText ? parseReviewResult(reviewText) : null;
|
|
1389
|
+
if (reviewText && reviewResult && !isReviewResultPlaceholder(reviewText, reviewResult)) {
|
|
1390
|
+
onReviewComplete(agentId, taskId);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (reviewText) {
|
|
1214
1394
|
authBlockedReason = 'Reviewer exited after returning placeholder output';
|
|
1215
1395
|
}
|
|
1216
1396
|
}
|