@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
package/client/dist/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@600;700;800&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CHxyLFN_.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-BZkAflU1.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stilero/bankan",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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,13 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"setup": "node scripts/setup.js",
|
|
47
47
|
"build": "npm run build --prefix client",
|
|
48
|
+
"lint": "eslint .",
|
|
49
|
+
"lint:fix": "eslint . --fix",
|
|
50
|
+
"test": "npm run test --prefix server && npm run test --prefix client",
|
|
51
|
+
"test:server": "npm run test --prefix server",
|
|
52
|
+
"test:client": "npm run test --prefix client",
|
|
53
|
+
"coverage": "npm run coverage --prefix server && npm run coverage --prefix client && node scripts/check-coverage.js",
|
|
54
|
+
"coverage:check": "node scripts/check-coverage.js",
|
|
48
55
|
"dev": "concurrently -n server,client -c cyan,magenta \"npm run dev --prefix server\" \"npm run dev --prefix client\"",
|
|
49
56
|
"start": "npm run dev",
|
|
50
57
|
"install:all": "npm install && npm install --prefix server && npm install --prefix client",
|
|
@@ -60,6 +67,11 @@
|
|
|
60
67
|
"ws": "^8.16.0"
|
|
61
68
|
},
|
|
62
69
|
"devDependencies": {
|
|
63
|
-
"
|
|
70
|
+
"@eslint/js": "^9.39.4",
|
|
71
|
+
"concurrently": "^8.2.2",
|
|
72
|
+
"eslint": "^9.39.4",
|
|
73
|
+
"eslint-plugin-react": "^7.37.5",
|
|
74
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
75
|
+
"globals": "^16.5.0"
|
|
64
76
|
}
|
|
65
77
|
}
|
package/scripts/setup.js
CHANGED
|
@@ -15,10 +15,6 @@ const ENV_FILE = runtimePaths.envFile;
|
|
|
15
15
|
|
|
16
16
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
17
|
|
|
18
|
-
function ask(question) {
|
|
19
|
-
return new Promise((resolve) => rl.question(question, resolve));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
18
|
function dim(text) { return `\x1b[2m${text}\x1b[0m`; }
|
|
23
19
|
function green(text) { return `\x1b[32m${text}\x1b[0m`; }
|
|
24
20
|
function yellow(text) { return `\x1b[33m${text}\x1b[0m`; }
|
|
@@ -172,7 +168,7 @@ async function main() {
|
|
|
172
168
|
try {
|
|
173
169
|
execSync(step.cmd, { cwd: ROOT, stdio: 'inherit' });
|
|
174
170
|
console.log(` ${green('✓')} ${step.label}\n`);
|
|
175
|
-
} catch
|
|
171
|
+
} catch {
|
|
176
172
|
console.log(` ${red('✗')} ${step.label} install failed. Try running manually: ${step.cmd}\n`);
|
|
177
173
|
}
|
|
178
174
|
}
|
package/server/src/agents.js
CHANGED
|
@@ -36,6 +36,38 @@ const TOKEN_PATTERNS = [
|
|
|
36
36
|
/(\d[\d, ]*)\s+(?:input\s+)?tokens\b/i,
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
+
const STRUCTURED_BLOCK_MARKERS = {
|
|
40
|
+
plan: {
|
|
41
|
+
start: '=== PLAN START ===',
|
|
42
|
+
end: '=== PLAN END ===',
|
|
43
|
+
},
|
|
44
|
+
review: {
|
|
45
|
+
start: '=== REVIEW START ===',
|
|
46
|
+
end: '=== REVIEW END ===',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function stripAnsi(text) {
|
|
51
|
+
if (typeof text !== 'string') return text;
|
|
52
|
+
// Replace cursor forward codes (\x1b[nC) with a space to preserve word boundaries.
|
|
53
|
+
// eslint-disable-next-line no-control-regex
|
|
54
|
+
let result = text.replace(/\x1b\[\d*C/g, ' ');
|
|
55
|
+
return result.replace(
|
|
56
|
+
// eslint-disable-next-line no-control-regex
|
|
57
|
+
/[\x1b\x9b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|\x1b\].*?(?:\x07|\x1b\\)|\r/g,
|
|
58
|
+
''
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getLastStructuredBlock(text, startMarker, endMarker) {
|
|
63
|
+
if (typeof text !== 'string' || !text) return null;
|
|
64
|
+
const endIdx = text.lastIndexOf(endMarker);
|
|
65
|
+
if (endIdx === -1) return null;
|
|
66
|
+
const startIdx = text.lastIndexOf(startMarker, endIdx);
|
|
67
|
+
if (startIdx === -1) return null;
|
|
68
|
+
return text.slice(startIdx, endIdx + endMarker.length);
|
|
69
|
+
}
|
|
70
|
+
|
|
39
71
|
class Agent {
|
|
40
72
|
constructor(def) {
|
|
41
73
|
this.id = def.id;
|
|
@@ -44,6 +76,7 @@ class Agent {
|
|
|
44
76
|
this.icon = def.icon;
|
|
45
77
|
this.color = def.color;
|
|
46
78
|
this.cli = def.cli || 'claude';
|
|
79
|
+
this.model = def.model || '';
|
|
47
80
|
this.draining = false;
|
|
48
81
|
this.status = 'idle';
|
|
49
82
|
this.currentTask = null;
|
|
@@ -63,6 +96,23 @@ class Agent {
|
|
|
63
96
|
openedAt: null,
|
|
64
97
|
outputPath: null,
|
|
65
98
|
};
|
|
99
|
+
this._lastTokenSync = null;
|
|
100
|
+
this.structuredOutput = this._createStructuredOutputState();
|
|
101
|
+
this.terminalSize = {
|
|
102
|
+
cols: 220,
|
|
103
|
+
rows: 50,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_createStructuredOutputState() {
|
|
108
|
+
return {
|
|
109
|
+
plan: { pending: '', completed: null, allCompleted: [] },
|
|
110
|
+
review: { pending: '', completed: null, allCompleted: [] },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_resetStructuredOutput() {
|
|
115
|
+
this.structuredOutput = this._createStructuredOutputState();
|
|
66
116
|
}
|
|
67
117
|
|
|
68
118
|
spawn(cwd, command) {
|
|
@@ -90,6 +140,7 @@ class Agent {
|
|
|
90
140
|
this.tokens = 0;
|
|
91
141
|
this.taskTokenBase = this.currentTask ? (store.getTask(this.currentTask)?.totalTokens || 0) : 0;
|
|
92
142
|
this.lastOutputAt = Date.now();
|
|
143
|
+
this._lastTokenSync = null;
|
|
93
144
|
this.bridge = {
|
|
94
145
|
active: false,
|
|
95
146
|
mode: null,
|
|
@@ -97,13 +148,14 @@ class Agent {
|
|
|
97
148
|
openedAt: null,
|
|
98
149
|
outputPath: null,
|
|
99
150
|
};
|
|
151
|
+
this._resetStructuredOutput();
|
|
100
152
|
|
|
101
153
|
const env = { ...process.env, TERM: 'xterm-256color' };
|
|
102
154
|
delete env.CLAUDECODE;
|
|
103
155
|
this.process = pty.spawn('bash', ['-l', '-c', command], {
|
|
104
156
|
name: 'xterm-256color',
|
|
105
|
-
cols:
|
|
106
|
-
rows:
|
|
157
|
+
cols: this.terminalSize.cols,
|
|
158
|
+
rows: this.terminalSize.rows,
|
|
107
159
|
cwd,
|
|
108
160
|
env,
|
|
109
161
|
});
|
|
@@ -115,6 +167,7 @@ class Agent {
|
|
|
115
167
|
}
|
|
116
168
|
this.lastOutputAt = Date.now();
|
|
117
169
|
this._parseTokens(data);
|
|
170
|
+
this._captureStructuredOutput(data);
|
|
118
171
|
this._syncTaskTokens();
|
|
119
172
|
if (this.bridge.active && this.bridge.outputPath) {
|
|
120
173
|
try { appendFileSync(this.bridge.outputPath, data); } catch { /* ignore */ }
|
|
@@ -161,8 +214,44 @@ class Agent {
|
|
|
161
214
|
}
|
|
162
215
|
}
|
|
163
216
|
|
|
217
|
+
_captureStructuredOutput(data) {
|
|
218
|
+
for (const [kind, markers] of Object.entries(STRUCTURED_BLOCK_MARKERS)) {
|
|
219
|
+
const state = this.structuredOutput[kind];
|
|
220
|
+
// Accumulate raw data so ANSI sequences split across chunks
|
|
221
|
+
// are stripped correctly when we process the combined text.
|
|
222
|
+
const rawCombined = `${state.pending}${data}`;
|
|
223
|
+
const combined = stripAnsi(rawCombined);
|
|
224
|
+
const completed = getLastStructuredBlock(combined, markers.start, markers.end);
|
|
225
|
+
if (completed) {
|
|
226
|
+
state.completed = completed;
|
|
227
|
+
state.allCompleted.push(completed);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lastStartIdx = combined.lastIndexOf(markers.start);
|
|
231
|
+
const lastEndIdx = combined.lastIndexOf(markers.end);
|
|
232
|
+
if (lastStartIdx !== -1 && lastStartIdx > lastEndIdx) {
|
|
233
|
+
// Inside an open block — keep raw data for re-stripping next time
|
|
234
|
+
state.pending = rawCombined;
|
|
235
|
+
} else {
|
|
236
|
+
const tailLength = Math.max(markers.start.length, markers.end.length) * 4;
|
|
237
|
+
state.pending = rawCombined.slice(-tailLength);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getStructuredBlock(kind) {
|
|
243
|
+
return this.structuredOutput[kind]?.completed || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getAllCapturedBlocks(kind) {
|
|
247
|
+
return this.structuredOutput[kind]?.allCompleted || [];
|
|
248
|
+
}
|
|
249
|
+
|
|
164
250
|
_syncTaskTokens() {
|
|
165
251
|
if (!this.currentTask || this.tokens <= 0) return;
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
if (this._lastTokenSync && now - this._lastTokenSync < 2000) return;
|
|
254
|
+
this._lastTokenSync = now;
|
|
166
255
|
store.updateTaskTokens(this.currentTask, this.taskTokenBase + this.tokens);
|
|
167
256
|
bus.emit('agent:updated', this.getStatus());
|
|
168
257
|
}
|
|
@@ -192,6 +281,29 @@ class Agent {
|
|
|
192
281
|
return false;
|
|
193
282
|
}
|
|
194
283
|
|
|
284
|
+
resize(cols, rows) {
|
|
285
|
+
const nextCols = Math.max(20, Math.floor(Number(cols) || 0));
|
|
286
|
+
const nextRows = Math.max(5, Math.floor(Number(rows) || 0));
|
|
287
|
+
|
|
288
|
+
if (!Number.isFinite(nextCols) || !Number.isFinite(nextRows)) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
this.terminalSize = {
|
|
293
|
+
cols: nextCols,
|
|
294
|
+
rows: nextRows,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (!this.process) return true;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
this.process.resize(nextCols, nextRows);
|
|
301
|
+
return true;
|
|
302
|
+
} catch {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
195
307
|
kill() {
|
|
196
308
|
if (this.process) {
|
|
197
309
|
try { this.process.kill(); } catch { /* ignore */ }
|
|
@@ -208,6 +320,7 @@ class Agent {
|
|
|
208
320
|
openedAt: null,
|
|
209
321
|
outputPath: null,
|
|
210
322
|
};
|
|
323
|
+
this._resetStructuredOutput();
|
|
211
324
|
bus.emit('agent:updated', this.getStatus());
|
|
212
325
|
}
|
|
213
326
|
|
|
@@ -232,6 +345,7 @@ class Agent {
|
|
|
232
345
|
bridgeMode: this.bridge.mode,
|
|
233
346
|
bridgeOwner: this.bridge.owner,
|
|
234
347
|
bridgeOpenedAt: this.bridge.openedAt,
|
|
348
|
+
terminalSize: this.terminalSize,
|
|
235
349
|
aggregatedTokens: this.currentTask
|
|
236
350
|
? Math.max(store.getTask(this.currentTask)?.totalTokens || 0, this.taskTokenBase + this.tokens)
|
|
237
351
|
: 0,
|
|
@@ -250,6 +364,7 @@ class AgentManager {
|
|
|
250
364
|
this.agents = new Map();
|
|
251
365
|
this._maxSettings = {}; // { planners: 4, implementors: 8, reviewers: 4 }
|
|
252
366
|
this._cliSettings = {}; // { planners: 'claude', implementors: 'claude', reviewers: 'claude' }
|
|
367
|
+
this._modelSettings = {}; // { planners: '', implementors: 'opus', reviewers: 'haiku' }
|
|
253
368
|
this._sessionCounters = { plan: 0, imp: 0, rev: 0 };
|
|
254
369
|
|
|
255
370
|
// Orchestrator is always present
|
|
@@ -269,10 +384,11 @@ class AgentManager {
|
|
|
269
384
|
}
|
|
270
385
|
|
|
271
386
|
reconfigure(settings) {
|
|
272
|
-
for (const [settingsKey, {
|
|
387
|
+
for (const [settingsKey, { prefix }] of Object.entries(ROLE_MAP)) {
|
|
273
388
|
const cfg = settings.agents[settingsKey];
|
|
274
389
|
this._maxSettings[settingsKey] = cfg.max;
|
|
275
390
|
this._cliSettings[settingsKey] = cfg.cli;
|
|
391
|
+
this._modelSettings[settingsKey] = cfg.model || '';
|
|
276
392
|
|
|
277
393
|
// Scale down if current count exceeds new max
|
|
278
394
|
const currentAgents = this.getAgentsByRole(prefix);
|
|
@@ -288,10 +404,11 @@ class AgentManager {
|
|
|
288
404
|
}
|
|
289
405
|
}
|
|
290
406
|
|
|
291
|
-
// Update CLI on all existing non-draining agents for this role
|
|
407
|
+
// Update CLI and model on all existing non-draining agents for this role
|
|
292
408
|
for (const agent of this.getAgentsByRole(prefix)) {
|
|
293
409
|
if (!agent.draining) {
|
|
294
410
|
agent.cli = cfg.cli;
|
|
411
|
+
agent.model = cfg.model || '';
|
|
295
412
|
}
|
|
296
413
|
}
|
|
297
414
|
}
|
|
@@ -302,6 +419,7 @@ class AgentManager {
|
|
|
302
419
|
const { meta, prefix } = ROLE_MAP[settingsKey];
|
|
303
420
|
const max = this._maxSettings[settingsKey] ?? 1;
|
|
304
421
|
const cli = this._cliSettings[settingsKey] || 'claude';
|
|
422
|
+
const model = this._modelSettings[settingsKey] || '';
|
|
305
423
|
const current = this.getAgentsByRole(prefix);
|
|
306
424
|
|
|
307
425
|
if (current.length >= max) return null;
|
|
@@ -317,6 +435,7 @@ class AgentManager {
|
|
|
317
435
|
icon: meta.icon,
|
|
318
436
|
color,
|
|
319
437
|
cli,
|
|
438
|
+
model,
|
|
320
439
|
});
|
|
321
440
|
this.agents.set(agent.id, agent);
|
|
322
441
|
bus.emit('agent:updated', agent.getStatus());
|