@stilero/bankan 1.1.4 → 1.2.1
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 +41 -5
- 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 +9 -7
- 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.1
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
@@ -92,11 +92,38 @@ Key rules:
|
|
|
92
92
|
- Ask for clarification only when a blocking unknown cannot be resolved from the repository context
|
|
93
93
|
- Do not ask for approval in free-form prose; the human approval flow happens outside your response
|
|
94
94
|
- Keep the plan specific, implementation-ready, and grounded in the current codebase`,
|
|
95
|
-
implementation: `Follow the plan step by step
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
95
|
+
implementation: `Follow the plan step by step. Execute each step fully before moving on.
|
|
96
|
+
|
|
97
|
+
## Use Subagents to Stay Focused
|
|
98
|
+
- Use subagents liberally to keep the main context window clean
|
|
99
|
+
- Offload research, exploration, file search, and parallel analysis to subagents
|
|
100
|
+
- Reserve the main context for implementation decisions and code changes
|
|
101
|
+
|
|
102
|
+
## Be Autonomous
|
|
103
|
+
- When given a task: just do it. Don't ask for hand-holding.
|
|
104
|
+
- If something goes wrong, investigate logs, errors, and failing tests — then resolve them.
|
|
105
|
+
- Fix failing tests without being told how.
|
|
106
|
+
- If required tools or dependencies are missing in the workspace, install them before continuing.
|
|
107
|
+
|
|
108
|
+
## Core Principles
|
|
109
|
+
- **Simplicity First**: Make every change as simple as possible. Impact minimal code.
|
|
110
|
+
- **No Laziness**: Find root causes. No temporary fixes. Senior developer standards.
|
|
111
|
+
- **Minimal Impact**: Only touch what's necessary. No side effects with new bugs.
|
|
112
|
+
- **Demand Elegance (Balanced)**: For non-trivial changes, pause and ask "is there a more elegant way?" Skip this for simple, obvious fixes — don't over-engineer.
|
|
113
|
+
|
|
114
|
+
## Mandatory Test Verification
|
|
115
|
+
- Follow TDD when practical: create or update the failing test first, implement the change second, then rerun the suite.
|
|
116
|
+
- Run existing tests after implementation to verify nothing broke.
|
|
117
|
+
- Run the linter before finishing.
|
|
118
|
+
|
|
119
|
+
## Definition of Done
|
|
120
|
+
- All plan steps are completed.
|
|
121
|
+
- Code compiles/runs without errors.
|
|
122
|
+
- Relevant tests pass (new and existing).
|
|
123
|
+
- Linter passes with no new violations.
|
|
124
|
+
- Commit after each logical unit of work with descriptive commit messages.
|
|
125
|
+
- Version bump if applicable, following the repository's versioning conventions.
|
|
126
|
+
- After all work is done, make a final commit if there are any uncommitted changes.`,
|
|
100
127
|
review: `You are an expert code reviewer.
|
|
101
128
|
|
|
102
129
|
Step 1 — Gather the diff
|
|
@@ -136,6 +163,7 @@ export function getDefaults() {
|
|
|
136
163
|
implementors: { max: 8, cli: getLegacyImplementorCli(), model: '' },
|
|
137
164
|
reviewers: { max: 4, cli: 'claude', model: '' },
|
|
138
165
|
},
|
|
166
|
+
maxReviewCycles: 3,
|
|
139
167
|
prompts: { ...DEFAULT_PROMPTS },
|
|
140
168
|
};
|
|
141
169
|
}
|
|
@@ -170,6 +198,10 @@ function normalizeSettingsShape(data) {
|
|
|
170
198
|
}
|
|
171
199
|
}
|
|
172
200
|
|
|
201
|
+
if (typeof data.maxReviewCycles !== 'number' || data.maxReviewCycles < 1) {
|
|
202
|
+
data.maxReviewCycles = defaults.maxReviewCycles;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
data.prompts = {
|
|
174
206
|
...defaults.prompts,
|
|
175
207
|
...(data.prompts || {}),
|
|
@@ -239,6 +271,10 @@ export function validateSettings(settings) {
|
|
|
239
271
|
}
|
|
240
272
|
}
|
|
241
273
|
|
|
274
|
+
if (typeof settings.maxReviewCycles !== 'number' || settings.maxReviewCycles < 1 || settings.maxReviewCycles > 20) {
|
|
275
|
+
errors.push('maxReviewCycles must be a number between 1 and 20');
|
|
276
|
+
}
|
|
277
|
+
|
|
242
278
|
if (!settings.prompts || typeof settings.prompts !== 'object') {
|
|
243
279
|
errors.push('prompts configuration is required');
|
|
244
280
|
} 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;
|
|
@@ -789,7 +791,7 @@ async function startPlanning(task) {
|
|
|
789
791
|
review: null,
|
|
790
792
|
reviewFeedback: null,
|
|
791
793
|
reviewCycleCount: 0,
|
|
792
|
-
maxReviewCycles: resolveTaskMaxReviewCycles(task,
|
|
794
|
+
maxReviewCycles: resolveTaskMaxReviewCycles(task, getMaxReviewCycles()),
|
|
793
795
|
blockedReason: null,
|
|
794
796
|
assignedTo: null,
|
|
795
797
|
});
|
|
@@ -870,7 +872,7 @@ function onPlanComplete(agentId, taskId) {
|
|
|
870
872
|
review: null,
|
|
871
873
|
reviewFeedback: null,
|
|
872
874
|
reviewCycleCount: 0,
|
|
873
|
-
maxReviewCycles: resolveTaskMaxReviewCycles(task,
|
|
875
|
+
maxReviewCycles: resolveTaskMaxReviewCycles(task, getMaxReviewCycles()),
|
|
874
876
|
blockedReason: null,
|
|
875
877
|
assignedTo: null,
|
|
876
878
|
});
|
|
@@ -1051,7 +1053,7 @@ async function onReviewComplete(agentId, taskId) {
|
|
|
1051
1053
|
|
|
1052
1054
|
const task = store.getTask(taskId);
|
|
1053
1055
|
const nextReviewCycleCount = (task?.reviewCycleCount || 0) + 1;
|
|
1054
|
-
const maxReviewCycles = Math.max(1, task?.maxReviewCycles ||
|
|
1056
|
+
const maxReviewCycles = Math.max(1, task?.maxReviewCycles || getMaxReviewCycles());
|
|
1055
1057
|
|
|
1056
1058
|
if (nextReviewCycleCount >= maxReviewCycles) {
|
|
1057
1059
|
store.updateTask(taskId, {
|
|
@@ -1171,7 +1173,7 @@ async function abortTask(taskId) {
|
|
|
1171
1173
|
reviewFeedback: null,
|
|
1172
1174
|
previousStatus: null,
|
|
1173
1175
|
reviewCycleCount: 0,
|
|
1174
|
-
maxReviewCycles:
|
|
1176
|
+
maxReviewCycles: getMaxReviewCycles(),
|
|
1175
1177
|
});
|
|
1176
1178
|
|
|
1177
1179
|
bus.emit('task:aborted', { taskId });
|
|
@@ -1203,7 +1205,7 @@ async function resetTask(taskId) {
|
|
|
1203
1205
|
planFeedback: null,
|
|
1204
1206
|
previousStatus: null,
|
|
1205
1207
|
reviewCycleCount: 0,
|
|
1206
|
-
maxReviewCycles:
|
|
1208
|
+
maxReviewCycles: getMaxReviewCycles(),
|
|
1207
1209
|
sessionHistory: [],
|
|
1208
1210
|
progress: 0,
|
|
1209
1211
|
totalTokens: 0,
|
|
@@ -1217,7 +1219,7 @@ async function resetTask(taskId) {
|
|
|
1217
1219
|
|
|
1218
1220
|
async function deleteTask(taskId) {
|
|
1219
1221
|
const task = store.getTask(taskId);
|
|
1220
|
-
if (!task || task.status
|
|
1222
|
+
if (!task || !['done', 'aborted'].includes(task.status)) return false;
|
|
1221
1223
|
|
|
1222
1224
|
if (task.workspacePath) {
|
|
1223
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) {
|