@stilero/bankan 1.1.3 → 1.2.0
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/client/dist/assets/index-B1C_mPSX.js +50 -0
- package/client/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/src/config.js +9 -0
- package/server/src/config.test.js +50 -0
- package/server/src/index.js +2 -2
- package/server/src/orchestrator.delete.test.js +95 -0
- package/server/src/orchestrator.js +18 -13
- package/server/src/store.js +9 -5
- package/client/dist/assets/index-DBbtVfOb.js +0 -50
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-B1C_mPSX.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-BZkAflU1.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stilero/bankan",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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/config.js
CHANGED
|
@@ -136,6 +136,7 @@ export function getDefaults() {
|
|
|
136
136
|
implementors: { max: 8, cli: getLegacyImplementorCli(), model: '' },
|
|
137
137
|
reviewers: { max: 4, cli: 'claude', model: '' },
|
|
138
138
|
},
|
|
139
|
+
maxReviewCycles: 3,
|
|
139
140
|
prompts: { ...DEFAULT_PROMPTS },
|
|
140
141
|
};
|
|
141
142
|
}
|
|
@@ -170,6 +171,10 @@ function normalizeSettingsShape(data) {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
if (typeof data.maxReviewCycles !== 'number' || data.maxReviewCycles < 1) {
|
|
175
|
+
data.maxReviewCycles = defaults.maxReviewCycles;
|
|
176
|
+
}
|
|
177
|
+
|
|
173
178
|
data.prompts = {
|
|
174
179
|
...defaults.prompts,
|
|
175
180
|
...(data.prompts || {}),
|
|
@@ -239,6 +244,10 @@ export function validateSettings(settings) {
|
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
if (typeof settings.maxReviewCycles !== 'number' || settings.maxReviewCycles < 1 || settings.maxReviewCycles > 20) {
|
|
248
|
+
errors.push('maxReviewCycles must be a number between 1 and 20');
|
|
249
|
+
}
|
|
250
|
+
|
|
242
251
|
if (!settings.prompts || typeof settings.prompts !== 'object') {
|
|
243
252
|
errors.push('prompts configuration is required');
|
|
244
253
|
} else {
|
|
@@ -76,6 +76,53 @@ describe('config settings lifecycle', () => {
|
|
|
76
76
|
expect(errors).toContain('prompts.review must be a string');
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
test('getDefaults includes maxReviewCycles and normalizeSettingsShape corrects invalid values', async () => {
|
|
80
|
+
harness = createRuntimeHarness();
|
|
81
|
+
const configModule = await harness.importModule('./src/config.js');
|
|
82
|
+
|
|
83
|
+
const defaults = configModule.getDefaults();
|
|
84
|
+
expect(defaults.maxReviewCycles).toBe(3);
|
|
85
|
+
|
|
86
|
+
configModule.saveSettings({
|
|
87
|
+
...defaults,
|
|
88
|
+
maxReviewCycles: -5,
|
|
89
|
+
});
|
|
90
|
+
const loaded = configModule.loadSettings();
|
|
91
|
+
expect(loaded.maxReviewCycles).toBe(3);
|
|
92
|
+
|
|
93
|
+
configModule.saveSettings({
|
|
94
|
+
...defaults,
|
|
95
|
+
maxReviewCycles: 'bad',
|
|
96
|
+
});
|
|
97
|
+
expect(configModule.loadSettings().maxReviewCycles).toBe(3);
|
|
98
|
+
|
|
99
|
+
configModule.saveSettings({
|
|
100
|
+
...defaults,
|
|
101
|
+
maxReviewCycles: 10,
|
|
102
|
+
});
|
|
103
|
+
expect(configModule.loadSettings().maxReviewCycles).toBe(10);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('validateSettings rejects out-of-range maxReviewCycles', async () => {
|
|
107
|
+
harness = createRuntimeHarness();
|
|
108
|
+
const { validateSettings, getDefaults } = await harness.importModule('./src/config.js');
|
|
109
|
+
const base = {
|
|
110
|
+
...getDefaults(),
|
|
111
|
+
repos: ['/repo'],
|
|
112
|
+
defaultRepoPath: '/repo',
|
|
113
|
+
workspaceRoot: '/tmp/ws',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(validateSettings({ ...base, maxReviewCycles: 0 }))
|
|
117
|
+
.toContain('maxReviewCycles must be a number between 1 and 20');
|
|
118
|
+
expect(validateSettings({ ...base, maxReviewCycles: 21 }))
|
|
119
|
+
.toContain('maxReviewCycles must be a number between 1 and 20');
|
|
120
|
+
expect(validateSettings({ ...base, maxReviewCycles: 'bad' }))
|
|
121
|
+
.toContain('maxReviewCycles must be a number between 1 and 20');
|
|
122
|
+
expect(validateSettings({ ...base, maxReviewCycles: 5 }))
|
|
123
|
+
.not.toContain('maxReviewCycles must be a number between 1 and 20');
|
|
124
|
+
});
|
|
125
|
+
|
|
79
126
|
test('reads env defaults for repos, port, and legacy implementor cli', async () => {
|
|
80
127
|
harness = createRuntimeHarness();
|
|
81
128
|
const { writeFileSync } = await import('node:fs');
|
|
@@ -135,6 +182,7 @@ describe('config settings lifecycle', () => {
|
|
|
135
182
|
repos: ['/repo'],
|
|
136
183
|
defaultRepoPath: '/repo',
|
|
137
184
|
workspaceRoot: '/tmp/ws',
|
|
185
|
+
maxReviewCycles: 3,
|
|
138
186
|
agents: {
|
|
139
187
|
planners: { max: 1, cli: 'claude', model: 'claude-haiku-4-5' },
|
|
140
188
|
implementors: { max: 1, cli: 'claude', model: 'claude-opus-4-6' },
|
|
@@ -158,6 +206,7 @@ describe('config settings lifecycle', () => {
|
|
|
158
206
|
repos: ['/repo'],
|
|
159
207
|
defaultRepoPath: '/repo',
|
|
160
208
|
workspaceRoot: '/tmp/ws',
|
|
209
|
+
maxReviewCycles: 3,
|
|
161
210
|
agents: {
|
|
162
211
|
planners: { max: 1, cli: 'codex', model: 'claude-haiku-4-5' },
|
|
163
212
|
implementors: { max: 1, cli: 'claude', model: 'gpt-5.4' },
|
|
@@ -179,6 +228,7 @@ describe('config settings lifecycle', () => {
|
|
|
179
228
|
repos: ['/repo'],
|
|
180
229
|
defaultRepoPath: '/repo',
|
|
181
230
|
workspaceRoot: '/tmp/ws',
|
|
231
|
+
maxReviewCycles: 3,
|
|
182
232
|
agents: {
|
|
183
233
|
planners: { max: 1, cli: 'codex', model: 'gpt-5.3-codex' },
|
|
184
234
|
implementors: { max: 1, cli: 'codex', model: 'gpt-5.4' },
|
package/server/src/index.js
CHANGED
|
@@ -175,7 +175,7 @@ app.patch('/api/tasks/:id/extend-max-review-blocker', (req, res) => {
|
|
|
175
175
|
app.delete('/api/tasks/:id', async (req, res) => {
|
|
176
176
|
const task = store.getTask(req.params.id);
|
|
177
177
|
if (!task) return res.status(404).json({ error: 'Task not found' });
|
|
178
|
-
if (task.status
|
|
178
|
+
if (!['done', 'aborted'].includes(task.status)) return res.status(400).json({ error: 'Only completed or aborted tasks can be deleted' });
|
|
179
179
|
await orchestrator.deleteTask(task.id);
|
|
180
180
|
broadcast('TASK_DELETED', { taskId: task.id });
|
|
181
181
|
res.json({ ok: true });
|
|
@@ -585,7 +585,7 @@ wss.on('connection', (ws) => {
|
|
|
585
585
|
case 'DELETE_TASK': {
|
|
586
586
|
const { taskId } = msg.payload || {};
|
|
587
587
|
const task = store.getTask(taskId);
|
|
588
|
-
if (task?.status === 'done') {
|
|
588
|
+
if (task?.status === 'done' || task?.status === 'aborted') {
|
|
589
589
|
orchestrator.deleteTask(taskId);
|
|
590
590
|
broadcast('TASK_DELETED', { taskId });
|
|
591
591
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const getTask = vi.fn();
|
|
4
|
+
const deleteTaskStore = vi.fn();
|
|
5
|
+
const removePlan = vi.fn();
|
|
6
|
+
const appendLog = vi.fn();
|
|
7
|
+
|
|
8
|
+
vi.mock('./store.js', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
getTask,
|
|
11
|
+
deleteTask: deleteTaskStore,
|
|
12
|
+
removePlan,
|
|
13
|
+
appendLog,
|
|
14
|
+
updateTask: vi.fn(),
|
|
15
|
+
restartRecovery: vi.fn(),
|
|
16
|
+
getAllTasks: vi.fn(() => []),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('./events.js', () => ({
|
|
21
|
+
default: {
|
|
22
|
+
emit: vi.fn(),
|
|
23
|
+
on: vi.fn(),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('./agents.js', () => ({
|
|
28
|
+
default: {
|
|
29
|
+
get: vi.fn(),
|
|
30
|
+
getAvailablePlanner: vi.fn(),
|
|
31
|
+
getAvailableImplementor: vi.fn(),
|
|
32
|
+
getAvailableReviewer: vi.fn(),
|
|
33
|
+
getAllStatus: vi.fn(() => []),
|
|
34
|
+
agents: new Map(),
|
|
35
|
+
reconfigure: vi.fn(),
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('./config.js', () => ({
|
|
40
|
+
loadSettings: vi.fn(() => ({
|
|
41
|
+
agents: {
|
|
42
|
+
planners: { max: 1 },
|
|
43
|
+
implementors: { max: 1 },
|
|
44
|
+
reviewers: { max: 1 },
|
|
45
|
+
},
|
|
46
|
+
})),
|
|
47
|
+
getWorkspacesDir: vi.fn(() => '/tmp/workspaces'),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
vi.mock('simple-git', () => ({
|
|
51
|
+
simpleGit: vi.fn(() => ({})),
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const orchestrator = (await import('./orchestrator.js')).default;
|
|
55
|
+
const deleteTask = orchestrator.deleteTask;
|
|
56
|
+
|
|
57
|
+
describe('deleteTask', () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
getTask.mockReset();
|
|
60
|
+
deleteTaskStore.mockReset();
|
|
61
|
+
removePlan.mockReset();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('succeeds for a task with status done', async () => {
|
|
65
|
+
getTask.mockReturnValue({ id: 'T-1', status: 'done', workspacePath: null });
|
|
66
|
+
const result = await deleteTask('T-1');
|
|
67
|
+
expect(result).toBe(true);
|
|
68
|
+
expect(removePlan).toHaveBeenCalledWith('T-1');
|
|
69
|
+
expect(deleteTaskStore).toHaveBeenCalledWith('T-1');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('succeeds for a task with status aborted', async () => {
|
|
73
|
+
getTask.mockReturnValue({ id: 'T-2', status: 'aborted', workspacePath: null });
|
|
74
|
+
const result = await deleteTask('T-2');
|
|
75
|
+
expect(result).toBe(true);
|
|
76
|
+
expect(removePlan).toHaveBeenCalledWith('T-2');
|
|
77
|
+
expect(deleteTaskStore).toHaveBeenCalledWith('T-2');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('rejects tasks in non-terminal statuses', async () => {
|
|
81
|
+
for (const status of ['backlog', 'planning', 'implementing', 'review', 'blocked']) {
|
|
82
|
+
getTask.mockReturnValue({ id: 'T-3', status, workspacePath: null });
|
|
83
|
+
const result = await deleteTask('T-3');
|
|
84
|
+
expect(result).toBe(false);
|
|
85
|
+
expect(deleteTaskStore).not.toHaveBeenCalled();
|
|
86
|
+
deleteTaskStore.mockReset();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('returns false when task does not exist', async () => {
|
|
91
|
+
getTask.mockReturnValue(undefined);
|
|
92
|
+
const result = await deleteTask('T-999');
|
|
93
|
+
expect(result).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -24,7 +24,9 @@ const PLANNER_TIMEOUT = 5 * 60 * 1000;
|
|
|
24
24
|
const IMPLEMENTOR_TIMEOUT = 60 * 60 * 1000;
|
|
25
25
|
const REVIEWER_TIMEOUT = 30 * 60 * 1000;
|
|
26
26
|
const STUCK_TIMEOUT = 10 * 60 * 1000;
|
|
27
|
-
|
|
27
|
+
function getMaxReviewCycles() {
|
|
28
|
+
return loadSettings().maxReviewCycles || 3;
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
let pollTimer = null;
|
|
30
32
|
let signalTimer = null;
|
|
@@ -42,8 +44,10 @@ function stripAnsi(text) {
|
|
|
42
44
|
);
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
const isWindows = process.platform === 'win32';
|
|
48
|
+
|
|
45
49
|
function escapePrompt(text) {
|
|
46
|
-
if (
|
|
50
|
+
if (isWindows) {
|
|
47
51
|
// PowerShell: escape single quotes by doubling them
|
|
48
52
|
return text.replace(/'/g, "''");
|
|
49
53
|
}
|
|
@@ -51,9 +55,6 @@ function escapePrompt(text) {
|
|
|
51
55
|
return text.replace(/'/g, "'\\''");
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
// TODO: buildCodexExecCommand emits bash syntax (mktemp, $?, printf).
|
|
55
|
-
// On Windows the agent shell is PowerShell, so codex with captureLastMessage
|
|
56
|
-
// will not work until this function gets a win32 branch.
|
|
57
58
|
function buildCodexExecCommand(prompt, { captureLastMessage = false, sandbox = 'read-only', model = '' } = {}) {
|
|
58
59
|
const escapedPrompt = escapePrompt(prompt);
|
|
59
60
|
const modelFlag = model ? `-m ${model} ` : '';
|
|
@@ -64,13 +65,17 @@ function buildCodexExecCommand(prompt, { captureLastMessage = false, sandbox = '
|
|
|
64
65
|
return `tmpfile=$(mktemp); codex exec ${modelFlag}--sandbox ${sandbox} -o "$tmpfile" '${escapedPrompt}'; status=$?; printf '\\n=== CODEX_LAST_MESSAGE_FILE:%s ===\\n' "$tmpfile"; exit $status`;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// On Windows the agent shell is PowerShell, so the bash-syntax
|
|
69
|
+
// captureLastMessage path cannot work — the structured-capture and
|
|
70
|
+
// terminal-buffer fallbacks in extractStructuredStageText still apply.
|
|
67
71
|
export function buildAgentCommand(cliTool, prompt, mode = 'interactive', model = '') {
|
|
68
72
|
if (cliTool === 'codex') {
|
|
73
|
+
const capture = !isWindows;
|
|
69
74
|
if (mode === 'plan' || mode === 'review') {
|
|
70
|
-
return buildCodexExecCommand(prompt, { captureLastMessage:
|
|
75
|
+
return buildCodexExecCommand(prompt, { captureLastMessage: capture, sandbox: 'read-only', model });
|
|
71
76
|
}
|
|
72
77
|
if (mode === 'interactive') {
|
|
73
|
-
return buildCodexExecCommand(prompt, { captureLastMessage:
|
|
78
|
+
return buildCodexExecCommand(prompt, { captureLastMessage: capture, sandbox: 'danger-full-access', model });
|
|
74
79
|
}
|
|
75
80
|
return buildCodexExecCommand(prompt, { captureLastMessage: false, sandbox: 'read-only', model });
|
|
76
81
|
}
|
|
@@ -786,7 +791,7 @@ async function startPlanning(task) {
|
|
|
786
791
|
review: null,
|
|
787
792
|
reviewFeedback: null,
|
|
788
793
|
reviewCycleCount: 0,
|
|
789
|
-
maxReviewCycles: resolveTaskMaxReviewCycles(task,
|
|
794
|
+
maxReviewCycles: resolveTaskMaxReviewCycles(task, getMaxReviewCycles()),
|
|
790
795
|
blockedReason: null,
|
|
791
796
|
assignedTo: null,
|
|
792
797
|
});
|
|
@@ -867,7 +872,7 @@ function onPlanComplete(agentId, taskId) {
|
|
|
867
872
|
review: null,
|
|
868
873
|
reviewFeedback: null,
|
|
869
874
|
reviewCycleCount: 0,
|
|
870
|
-
maxReviewCycles: resolveTaskMaxReviewCycles(task,
|
|
875
|
+
maxReviewCycles: resolveTaskMaxReviewCycles(task, getMaxReviewCycles()),
|
|
871
876
|
blockedReason: null,
|
|
872
877
|
assignedTo: null,
|
|
873
878
|
});
|
|
@@ -1048,7 +1053,7 @@ async function onReviewComplete(agentId, taskId) {
|
|
|
1048
1053
|
|
|
1049
1054
|
const task = store.getTask(taskId);
|
|
1050
1055
|
const nextReviewCycleCount = (task?.reviewCycleCount || 0) + 1;
|
|
1051
|
-
const maxReviewCycles = Math.max(1, task?.maxReviewCycles ||
|
|
1056
|
+
const maxReviewCycles = Math.max(1, task?.maxReviewCycles || getMaxReviewCycles());
|
|
1052
1057
|
|
|
1053
1058
|
if (nextReviewCycleCount >= maxReviewCycles) {
|
|
1054
1059
|
store.updateTask(taskId, {
|
|
@@ -1168,7 +1173,7 @@ async function abortTask(taskId) {
|
|
|
1168
1173
|
reviewFeedback: null,
|
|
1169
1174
|
previousStatus: null,
|
|
1170
1175
|
reviewCycleCount: 0,
|
|
1171
|
-
maxReviewCycles:
|
|
1176
|
+
maxReviewCycles: getMaxReviewCycles(),
|
|
1172
1177
|
});
|
|
1173
1178
|
|
|
1174
1179
|
bus.emit('task:aborted', { taskId });
|
|
@@ -1200,7 +1205,7 @@ async function resetTask(taskId) {
|
|
|
1200
1205
|
planFeedback: null,
|
|
1201
1206
|
previousStatus: null,
|
|
1202
1207
|
reviewCycleCount: 0,
|
|
1203
|
-
maxReviewCycles:
|
|
1208
|
+
maxReviewCycles: getMaxReviewCycles(),
|
|
1204
1209
|
sessionHistory: [],
|
|
1205
1210
|
progress: 0,
|
|
1206
1211
|
totalTokens: 0,
|
|
@@ -1214,7 +1219,7 @@ async function resetTask(taskId) {
|
|
|
1214
1219
|
|
|
1215
1220
|
async function deleteTask(taskId) {
|
|
1216
1221
|
const task = store.getTask(taskId);
|
|
1217
|
-
if (!task || task.status
|
|
1222
|
+
if (!task || !['done', 'aborted'].includes(task.status)) return false;
|
|
1218
1223
|
|
|
1219
1224
|
if (task.workspacePath) {
|
|
1220
1225
|
await cleanupWorkspace(task);
|
package/server/src/store.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
import bus from './events.js';
|
|
5
|
-
import { getRuntimeStatePaths } from './config.js';
|
|
5
|
+
import { getRuntimeStatePaths, loadSettings } from './config.js';
|
|
6
6
|
|
|
7
7
|
const runtimePaths = getRuntimeStatePaths();
|
|
8
8
|
const DATA_DIR = runtimePaths.dataDir;
|
|
@@ -42,6 +42,10 @@ function isLegacyPlannerPathBlocker(task) {
|
|
|
42
42
|
return false;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function getDefaultMaxReviewCycles() {
|
|
46
|
+
return loadSettings().maxReviewCycles || 3;
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
class TaskStore {
|
|
46
50
|
constructor() {
|
|
47
51
|
this.tasks = [];
|
|
@@ -61,7 +65,7 @@ class TaskStore {
|
|
|
61
65
|
this.tasks = this.tasks.map(task => {
|
|
62
66
|
const normalized = {
|
|
63
67
|
reviewCycleCount: 0,
|
|
64
|
-
maxReviewCycles:
|
|
68
|
+
maxReviewCycles: getDefaultMaxReviewCycles(),
|
|
65
69
|
lastActiveStage: statusToStage(task.status) || 'backlog',
|
|
66
70
|
previousStatus: null,
|
|
67
71
|
totalTokens: 0,
|
|
@@ -82,7 +86,7 @@ class TaskStore {
|
|
|
82
86
|
normalized.reviewCycleCount = 0;
|
|
83
87
|
}
|
|
84
88
|
if (typeof normalized.maxReviewCycles !== 'number' || normalized.maxReviewCycles < 1) {
|
|
85
|
-
normalized.maxReviewCycles =
|
|
89
|
+
normalized.maxReviewCycles = getDefaultMaxReviewCycles();
|
|
86
90
|
}
|
|
87
91
|
if (typeof normalized.totalTokens !== 'number' || normalized.totalTokens < 0) {
|
|
88
92
|
normalized.totalTokens = 0;
|
|
@@ -134,7 +138,7 @@ class TaskStore {
|
|
|
134
138
|
blockedReason: null,
|
|
135
139
|
workspacePath: null,
|
|
136
140
|
reviewCycleCount: 0,
|
|
137
|
-
maxReviewCycles:
|
|
141
|
+
maxReviewCycles: getDefaultMaxReviewCycles(),
|
|
138
142
|
lastActiveStage: 'backlog',
|
|
139
143
|
previousStatus: null,
|
|
140
144
|
totalTokens: 0,
|
|
@@ -251,7 +255,7 @@ class TaskStore {
|
|
|
251
255
|
changed = true;
|
|
252
256
|
}
|
|
253
257
|
if (typeof task.maxReviewCycles !== 'number' || task.maxReviewCycles < 1) {
|
|
254
|
-
task.maxReviewCycles =
|
|
258
|
+
task.maxReviewCycles = getDefaultMaxReviewCycles();
|
|
255
259
|
changed = true;
|
|
256
260
|
}
|
|
257
261
|
if (typeof task.totalTokens !== 'number' || task.totalTokens < 0) {
|