Skip to content

Commit 63ee384

Browse files
withsivramclaude
andcommitted
feat: auto-slim completed tasks to reduce tasks.json size (closes #1642)
When a task transitions to "done" status, automatically slim it by: - Clearing `details` and `testStrategy` fields (set to empty string) - Truncating `description` to 200 characters with "..." suffix - Slimming all subtasks when a parent task is marked done This is controlled by the `slimDoneTasks` config option (default: true). Git history preserves pre-slim content. Only triggers on transition TO done, not on already-done tasks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4882163 commit 63ee384

File tree

6 files changed

+180
-9
lines changed

6 files changed

+180
-9
lines changed

scripts/modules/config-manager.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ const DEFAULTS = {
5858
responseLanguage: 'English',
5959
enableCodebaseAnalysis: true,
6060
enableProxy: false,
61-
anonymousTelemetry: true // Allow users to opt out of Sentry telemetry for local storage
61+
anonymousTelemetry: true, // Allow users to opt out of Sentry telemetry for local storage
62+
slimDoneTasks: true // Auto-slim completed tasks to reduce tasks.json size
6263
},
6364
claudeCode: {},
6465
codexCli: {},
@@ -747,6 +748,12 @@ function getAnonymousTelemetryEnabled(explicitRoot = null) {
747748
return config.anonymousTelemetry !== false; // Default true if undefined
748749
}
749750

751+
function isSlimDoneTasksEnabled(explicitRoot = null) {
752+
// Return boolean-safe value with default true (opt-in by default)
753+
const config = getGlobalConfig(explicitRoot);
754+
return config.slimDoneTasks !== false; // Default true if undefined
755+
}
756+
750757
function isProxyEnabled(session = null, projectRoot = null) {
751758
// Priority 1: Environment variable
752759
const envFlag = resolveEnvVariable(
@@ -1305,6 +1312,7 @@ export {
13051312
getProxyEnabled,
13061313
isProxyEnabled,
13071314
getAnonymousTelemetryEnabled,
1315+
isSlimDoneTasksEnabled,
13081316
getParametersForRole,
13091317
getUserId,
13101318
// Operating mode

scripts/modules/task-manager/set-task-status.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
TASK_STATUS_OPTIONS,
77
isValidTaskStatus
88
} from '../../../src/constants/task-status.js';
9-
import { getDebugFlag } from '../config-manager.js';
9+
import { getDebugFlag, isSlimDoneTasksEnabled } from '../config-manager.js';
1010
import { validateTaskDependencies } from '../dependency-manager.js';
1111
import { displayBanner } from '../ui.js';
1212
import {
@@ -84,6 +84,9 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
8484
const taskIds = taskIdInput.split(',').map((id) => id.trim());
8585
const updatedTasks = [];
8686

87+
// Check if auto-slim on done is enabled
88+
const slimOnDone = isSlimDoneTasksEnabled(projectRoot);
89+
8790
// Update each task and capture old status for display
8891
for (const id of taskIds) {
8992
// Capture old status before updating
@@ -106,7 +109,9 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
106109
oldStatus = task?.status || 'pending';
107110
}
108111

109-
await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode);
112+
await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode, {
113+
slimOnDone
114+
});
110115
updatedTasks.push({ id, oldStatus, newStatus });
111116
}
112117

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { log } from '../utils.js';
2+
3+
/**
4+
* Maximum length for truncated description on slimmed tasks.
5+
* @type {number}
6+
*/
7+
const DESCRIPTION_TRUNCATE_LENGTH = 200;
8+
9+
/**
10+
* Slim a completed task by removing verbose fields that are no longer actionable.
11+
* Removes `details` and `testStrategy`, truncates `description` to 200 chars.
12+
* This is a one-way operation — git history preserves the original content.
13+
*
14+
* @param {Object} task - The task object to slim
15+
* @returns {Object} The same task object, mutated in place
16+
*/
17+
function slimTask(task) {
18+
if (!task) return task;
19+
20+
// Clear verbose fields
21+
if (task.details) {
22+
task.details = '';
23+
}
24+
25+
if (task.testStrategy) {
26+
task.testStrategy = '';
27+
}
28+
29+
// Truncate description to max length
30+
if (
31+
task.description &&
32+
task.description.length > DESCRIPTION_TRUNCATE_LENGTH
33+
) {
34+
task.description =
35+
task.description.substring(0, DESCRIPTION_TRUNCATE_LENGTH) + '...';
36+
}
37+
38+
return task;
39+
}
40+
41+
/**
42+
* Slim a completed subtask by removing verbose fields.
43+
* Subtasks typically have fewer fields, but we still clear details/testStrategy
44+
* and truncate description.
45+
*
46+
* @param {Object} subtask - The subtask object to slim
47+
* @returns {Object} The same subtask object, mutated in place
48+
*/
49+
function slimSubtask(subtask) {
50+
if (!subtask) return subtask;
51+
52+
if (subtask.details) {
53+
subtask.details = '';
54+
}
55+
56+
if (subtask.testStrategy) {
57+
subtask.testStrategy = '';
58+
}
59+
60+
if (
61+
subtask.description &&
62+
subtask.description.length > DESCRIPTION_TRUNCATE_LENGTH
63+
) {
64+
subtask.description =
65+
subtask.description.substring(0, DESCRIPTION_TRUNCATE_LENGTH) + '...';
66+
}
67+
68+
return subtask;
69+
}
70+
71+
/**
72+
* Slim a task and all its subtasks when the task transitions to "done".
73+
* Only slims if the transition is TO a done/completed status.
74+
*
75+
* @param {Object} task - The task object
76+
* @param {string} oldStatus - The previous status
77+
* @param {string} newStatus - The new status being set
78+
* @returns {Object} The task, slimmed if transitioning to done
79+
*/
80+
function slimTaskOnComplete(task, oldStatus, newStatus) {
81+
const isDoneStatus =
82+
newStatus.toLowerCase() === 'done' ||
83+
newStatus.toLowerCase() === 'completed';
84+
const wasDone =
85+
oldStatus.toLowerCase() === 'done' ||
86+
oldStatus.toLowerCase() === 'completed';
87+
88+
// Only slim on transition TO done, not if already done
89+
if (!isDoneStatus || wasDone) {
90+
return task;
91+
}
92+
93+
log('info', `Slimming completed task ${task.id}: "${task.title}"`);
94+
95+
slimTask(task);
96+
97+
// Also slim all subtasks
98+
if (task.subtasks && task.subtasks.length > 0) {
99+
for (const subtask of task.subtasks) {
100+
slimSubtask(subtask);
101+
}
102+
}
103+
104+
return task;
105+
}
106+
107+
/**
108+
* Slim a subtask when it transitions to "done".
109+
*
110+
* @param {Object} subtask - The subtask object
111+
* @param {string} oldStatus - The previous status
112+
* @param {string} newStatus - The new status being set
113+
* @returns {Object} The subtask, slimmed if transitioning to done
114+
*/
115+
function slimSubtaskOnComplete(subtask, oldStatus, newStatus) {
116+
const isDoneStatus =
117+
newStatus.toLowerCase() === 'done' ||
118+
newStatus.toLowerCase() === 'completed';
119+
const wasDone =
120+
oldStatus.toLowerCase() === 'done' ||
121+
oldStatus.toLowerCase() === 'completed';
122+
123+
if (!isDoneStatus || wasDone) {
124+
return subtask;
125+
}
126+
127+
log('info', `Slimming completed subtask ${subtask.id}: "${subtask.title}"`);
128+
129+
return slimSubtask(subtask);
130+
}
131+
132+
export {
133+
DESCRIPTION_TRUNCATE_LENGTH,
134+
slimTask,
135+
slimSubtask,
136+
slimTaskOnComplete,
137+
slimSubtaskOnComplete
138+
};

scripts/modules/task-manager/update-single-task-status.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import chalk from 'chalk';
22

33
import { isValidTaskStatus } from '../../../src/constants/task-status.js';
44
import { log } from '../utils.js';
5+
import { slimSubtaskOnComplete, slimTaskOnComplete } from './slim-task.js';
56

67
/**
78
* Update the status of a single task
@@ -10,14 +11,18 @@ import { log } from '../utils.js';
1011
* @param {string} newStatus - New status
1112
* @param {Object} data - Tasks data
1213
* @param {boolean} showUi - Whether to show UI elements
14+
* @param {Object} [statusOptions] - Additional options
15+
* @param {boolean} [statusOptions.slimOnDone=false] - Whether to slim tasks when marking as done
1316
*/
1417
async function updateSingleTaskStatus(
1518
tasksPath,
1619
taskIdInput,
1720
newStatus,
1821
data,
19-
showUi = true
22+
showUi = true,
23+
statusOptions = {}
2024
) {
25+
const { slimOnDone = false } = statusOptions;
2126
if (!isValidTaskStatus(newStatus)) {
2227
throw new Error(
2328
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
@@ -52,6 +57,11 @@ async function updateSingleTaskStatus(
5257
const oldStatus = subtask.status || 'pending';
5358
subtask.status = newStatus;
5459

60+
// Slim subtask if transitioning to done and slimming is enabled
61+
if (slimOnDone) {
62+
slimSubtaskOnComplete(subtask, oldStatus, newStatus);
63+
}
64+
5565
log(
5666
'info',
5767
`Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'`
@@ -127,6 +137,11 @@ async function updateSingleTaskStatus(
127137
});
128138
}
129139
}
140+
141+
// Slim task (and its subtasks) if transitioning to done and slimming is enabled
142+
if (slimOnDone) {
143+
slimTaskOnComplete(task, oldStatus, newStatus);
144+
}
130145
}
131146
}
132147

tests/unit/config-manager.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,8 @@ const DEFAULT_CONFIG = {
148148
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
149149
enableCodebaseAnalysis: true,
150150
enableProxy: false,
151-
responseLanguage: 'English'
151+
responseLanguage: 'English',
152+
slimDoneTasks: true
152153
},
153154
claudeCode: {},
154155
codexCli: {},

tests/unit/scripts/modules/task-manager/set-task-status.test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ jest.unstable_mockModule(
7171
jest.unstable_mockModule(
7272
'../../../../../scripts/modules/config-manager.js',
7373
() => ({
74-
getDebugFlag: jest.fn(() => false)
74+
getDebugFlag: jest.fn(() => false),
75+
isSlimDoneTasksEnabled: jest.fn(() => true)
7576
})
7677
);
7778

@@ -507,7 +508,8 @@ describe('setTaskStatus', () => {
507508
tag: 'master',
508509
_rawTaggedData: expect.any(Object)
509510
}),
510-
false
511+
false,
512+
expect.objectContaining({ slimOnDone: expect.any(Boolean) })
511513
);
512514
expect(updateSingleTaskStatus).toHaveBeenCalledWith(
513515
tasksPath,
@@ -518,7 +520,8 @@ describe('setTaskStatus', () => {
518520
tag: 'master',
519521
_rawTaggedData: expect.any(Object)
520522
}),
521-
false
523+
false,
524+
expect.objectContaining({ slimOnDone: expect.any(Boolean) })
522525
);
523526
expect(updateSingleTaskStatus).toHaveBeenCalledWith(
524527
tasksPath,
@@ -529,7 +532,8 @@ describe('setTaskStatus', () => {
529532
tag: 'master',
530533
_rawTaggedData: expect.any(Object)
531534
}),
532-
false
535+
false,
536+
expect.objectContaining({ slimOnDone: expect.any(Boolean) })
533537
);
534538
expect(result).toBeDefined();
535539
});

0 commit comments

Comments
 (0)