@cleocode/core 2026.3.39 → 2026.3.41
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/dist/index.js +670 -655
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
- package/src/admin/export.ts +2 -17
- package/src/cleo.ts +16 -8
- package/src/inject/index.ts +7 -7
- package/src/internal.ts +14 -23
- package/src/reconciliation/index.ts +11 -2
- package/src/reconciliation/link-store.ts +174 -0
- package/src/reconciliation/reconciliation-engine.ts +136 -83
- package/src/release/release-manifest.ts +55 -1
- package/src/routing/capability-matrix.ts +4 -5
- package/src/store/tasks-schema.ts +52 -0
- package/src/task-work/index.ts +0 -14
- package/src/admin/sync.ts +0 -164
- package/src/reconciliation/sync-state.ts +0 -73
- package/src/task-work/todowrite-merge.ts +0 -289
package/src/admin/sync.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Sync Operations
|
|
3
|
-
*
|
|
4
|
-
* Core functions for sync state management used by dispatch layer.
|
|
5
|
-
*
|
|
6
|
-
* @task T5326
|
|
7
|
-
* @epic T5323
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { rm, rmdir, stat } from 'node:fs/promises';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
import { getCleoDir } from '../paths.js';
|
|
13
|
-
import { readJson } from '../store/json.js';
|
|
14
|
-
|
|
15
|
-
/** Sync session state stored in .cleo/sync/todowrite-session.json. */
|
|
16
|
-
interface SyncSessionState {
|
|
17
|
-
session_id: string;
|
|
18
|
-
injected_at: string;
|
|
19
|
-
injectedPhase?: string;
|
|
20
|
-
injected_tasks: string[];
|
|
21
|
-
task_metadata?: Record<string, { phase?: string }>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Result for sync status operation. */
|
|
25
|
-
export interface SyncStatusResult {
|
|
26
|
-
active: boolean;
|
|
27
|
-
sessionId?: string;
|
|
28
|
-
injectedAt?: string;
|
|
29
|
-
injectedPhase?: string;
|
|
30
|
-
taskCount?: number;
|
|
31
|
-
taskIds?: string[];
|
|
32
|
-
phases?: Array<{ phase: string; count: number }>;
|
|
33
|
-
stateFile: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Result for sync clear operation. */
|
|
37
|
-
export interface SyncClearResult {
|
|
38
|
-
cleared?: { stateFile: string };
|
|
39
|
-
dryRun?: boolean;
|
|
40
|
-
wouldDelete?: { stateFile: string; syncDirectory: string };
|
|
41
|
-
noChange?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get current sync status.
|
|
46
|
-
* @task T5326
|
|
47
|
-
*/
|
|
48
|
-
export async function getSyncStatus(projectRoot: string): Promise<{
|
|
49
|
-
success: boolean;
|
|
50
|
-
data?: SyncStatusResult;
|
|
51
|
-
error?: { code: string; message: string };
|
|
52
|
-
}> {
|
|
53
|
-
try {
|
|
54
|
-
const cleoDir = getCleoDir(projectRoot);
|
|
55
|
-
const stateFile = join(cleoDir, 'sync', 'todowrite-session.json');
|
|
56
|
-
const sessionState = await readJson<SyncSessionState>(stateFile);
|
|
57
|
-
|
|
58
|
-
if (!sessionState) {
|
|
59
|
-
return {
|
|
60
|
-
success: true,
|
|
61
|
-
data: {
|
|
62
|
-
active: false,
|
|
63
|
-
stateFile,
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Build phase distribution from metadata
|
|
69
|
-
let phases: Array<{ phase: string; count: number }> | undefined;
|
|
70
|
-
if (sessionState.task_metadata) {
|
|
71
|
-
const phaseMap = new Map<string, number>();
|
|
72
|
-
for (const meta of Object.values(sessionState.task_metadata)) {
|
|
73
|
-
const phase = meta.phase ?? 'unknown';
|
|
74
|
-
phaseMap.set(phase, (phaseMap.get(phase) ?? 0) + 1);
|
|
75
|
-
}
|
|
76
|
-
phases = [...phaseMap.entries()].map(([phase, count]) => ({ phase, count }));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
success: true,
|
|
81
|
-
data: {
|
|
82
|
-
active: true,
|
|
83
|
-
sessionId: sessionState.session_id,
|
|
84
|
-
injectedAt: sessionState.injected_at,
|
|
85
|
-
injectedPhase: sessionState.injectedPhase ?? 'none',
|
|
86
|
-
taskCount: sessionState.injected_tasks.length,
|
|
87
|
-
taskIds: sessionState.injected_tasks,
|
|
88
|
-
phases,
|
|
89
|
-
stateFile,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
-
return {
|
|
95
|
-
success: false,
|
|
96
|
-
error: { code: 'E_SYNC_STATUS_FAILED', message },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Clear sync state.
|
|
103
|
-
* @task T5326
|
|
104
|
-
*/
|
|
105
|
-
export async function clearSyncState(
|
|
106
|
-
projectRoot: string,
|
|
107
|
-
dryRun?: boolean,
|
|
108
|
-
): Promise<{
|
|
109
|
-
success: boolean;
|
|
110
|
-
data?: SyncClearResult;
|
|
111
|
-
error?: { code: string; message: string };
|
|
112
|
-
}> {
|
|
113
|
-
try {
|
|
114
|
-
const cleoDir = getCleoDir(projectRoot);
|
|
115
|
-
const syncDir = join(cleoDir, 'sync');
|
|
116
|
-
const stateFile = join(syncDir, 'todowrite-session.json');
|
|
117
|
-
|
|
118
|
-
let exists = false;
|
|
119
|
-
try {
|
|
120
|
-
await stat(stateFile);
|
|
121
|
-
exists = true;
|
|
122
|
-
} catch {
|
|
123
|
-
// File doesn't exist
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (!exists) {
|
|
127
|
-
return {
|
|
128
|
-
success: true,
|
|
129
|
-
data: { noChange: true },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (dryRun) {
|
|
134
|
-
return {
|
|
135
|
-
success: true,
|
|
136
|
-
data: {
|
|
137
|
-
dryRun: true,
|
|
138
|
-
wouldDelete: { stateFile, syncDirectory: syncDir },
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
await rm(stateFile, { force: true });
|
|
144
|
-
// Clean up empty sync directory
|
|
145
|
-
try {
|
|
146
|
-
await rmdir(syncDir);
|
|
147
|
-
} catch {
|
|
148
|
-
// not empty or doesn't exist
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
success: true,
|
|
153
|
-
data: {
|
|
154
|
-
cleared: { stateFile },
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
} catch (err) {
|
|
158
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
error: { code: 'E_SYNC_CLEAR_FAILED', message },
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync state persistence for the reconciliation engine.
|
|
3
|
-
*
|
|
4
|
-
* Each provider gets its own session state file under `.cleo/sync/`.
|
|
5
|
-
*
|
|
6
|
-
* @task T5800
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { mkdir, rm } from 'node:fs/promises';
|
|
10
|
-
import { join } from 'node:path';
|
|
11
|
-
|
|
12
|
-
import type { SyncSessionState } from '@cleocode/contracts';
|
|
13
|
-
import { getCleoDir } from '../paths.js';
|
|
14
|
-
import { atomicWriteJson } from '../store/atomic.js';
|
|
15
|
-
import { readJson } from '../store/json.js';
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Helpers
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
function getSyncDir(cwd?: string): string {
|
|
22
|
-
return join(getCleoDir(cwd), 'sync');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function getStateFilePath(providerId: string, cwd?: string): string {
|
|
26
|
-
return join(getSyncDir(cwd), `${providerId}-session.json`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Public API
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Read sync session state for a provider.
|
|
35
|
-
* Returns null if no state file exists.
|
|
36
|
-
*/
|
|
37
|
-
export async function readSyncState(
|
|
38
|
-
providerId: string,
|
|
39
|
-
cwd?: string,
|
|
40
|
-
): Promise<SyncSessionState | null> {
|
|
41
|
-
const filePath = getStateFilePath(providerId, cwd);
|
|
42
|
-
try {
|
|
43
|
-
return await readJson<SyncSessionState>(filePath);
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Write sync session state for a provider.
|
|
51
|
-
*/
|
|
52
|
-
export async function writeSyncState(
|
|
53
|
-
providerId: string,
|
|
54
|
-
state: SyncSessionState,
|
|
55
|
-
cwd?: string,
|
|
56
|
-
): Promise<void> {
|
|
57
|
-
const syncDir = getSyncDir(cwd);
|
|
58
|
-
await mkdir(syncDir, { recursive: true });
|
|
59
|
-
const filePath = getStateFilePath(providerId, cwd);
|
|
60
|
-
await atomicWriteJson(filePath, state);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Clear (delete) sync session state for a provider.
|
|
65
|
-
*/
|
|
66
|
-
export async function clearSyncState(providerId: string, cwd?: string): Promise<void> {
|
|
67
|
-
const filePath = getStateFilePath(providerId, cwd);
|
|
68
|
-
try {
|
|
69
|
-
await rm(filePath);
|
|
70
|
-
} catch {
|
|
71
|
-
// File may not exist — that's fine.
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TodoWrite merge logic — merge Claude TodoWrite state back to CLEO tasks.
|
|
3
|
-
*
|
|
4
|
-
* @deprecated Use the provider-agnostic reconciliation engine instead:
|
|
5
|
-
* import { reconcile } from '../reconciliation/index.js';
|
|
6
|
-
* import { ClaudeCodeTaskSyncProvider } from '@cleocode/adapters';
|
|
7
|
-
*
|
|
8
|
-
* This module is preserved as a thin compatibility wrapper. New code should
|
|
9
|
-
* use the reconciliation engine directly.
|
|
10
|
-
*
|
|
11
|
-
* @task T4551
|
|
12
|
-
* @epic T4545
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { readFile, rm, stat } from 'node:fs/promises';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
|
|
18
|
-
import { ExitCode } from '@cleocode/contracts';
|
|
19
|
-
import { CleoError } from '../errors.js';
|
|
20
|
-
import { getCleoDir } from '../paths.js';
|
|
21
|
-
import type { DataAccessor } from '../store/data-accessor.js';
|
|
22
|
-
import { getAccessor } from '../store/data-accessor.js';
|
|
23
|
-
import { readJson } from '../store/json.js';
|
|
24
|
-
import { addTask } from '../tasks/add.js';
|
|
25
|
-
import { completeTask } from '../tasks/complete.js';
|
|
26
|
-
import { updateTask } from '../tasks/update.js';
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Types (preserved for backward compatibility)
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/** @deprecated Use ExternalTask from @cleocode/contracts instead. */
|
|
33
|
-
export interface TodoWriteItem {
|
|
34
|
-
content: string;
|
|
35
|
-
status: 'pending' | 'in_progress' | 'completed';
|
|
36
|
-
activeForm?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** @deprecated Use AdapterTaskSyncProvider.getExternalTasks() instead. */
|
|
40
|
-
export interface TodoWriteState {
|
|
41
|
-
todos: TodoWriteItem[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** @deprecated Use SyncSessionState from @cleocode/contracts instead. */
|
|
45
|
-
export interface SyncSessionState {
|
|
46
|
-
injected_tasks: string[];
|
|
47
|
-
injectedPhase?: string;
|
|
48
|
-
task_metadata?: Record<string, { phase?: string }>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** @deprecated Use ReconcileAction from @cleocode/contracts instead. */
|
|
52
|
-
export interface ChangeSet {
|
|
53
|
-
completed: string[];
|
|
54
|
-
progressed: string[];
|
|
55
|
-
newTasks: string[];
|
|
56
|
-
removed: string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @deprecated Use ReconcileOptions from @cleocode/contracts instead. */
|
|
60
|
-
export interface TodoWriteMergeOptions {
|
|
61
|
-
/** Path to the TodoWrite JSON state file. */
|
|
62
|
-
file: string;
|
|
63
|
-
/** Show changes without modifying tasks. */
|
|
64
|
-
dryRun?: boolean;
|
|
65
|
-
/** Default phase for newly created tasks. */
|
|
66
|
-
defaultPhase?: string;
|
|
67
|
-
/** Working directory (project root). */
|
|
68
|
-
cwd?: string;
|
|
69
|
-
/** Optional DataAccessor override. */
|
|
70
|
-
accessor?: DataAccessor;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** @deprecated Use ReconcileResult from @cleocode/contracts instead. */
|
|
74
|
-
export interface TodoWriteMergeResult {
|
|
75
|
-
dryRun: boolean;
|
|
76
|
-
changes: {
|
|
77
|
-
completed: number;
|
|
78
|
-
progressed: number;
|
|
79
|
-
new: number;
|
|
80
|
-
removed: number;
|
|
81
|
-
applied: number;
|
|
82
|
-
};
|
|
83
|
-
sessionCleared: boolean;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// Internal helpers
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Parse task ID from content prefix: "[T001] ..." -> "T001".
|
|
92
|
-
*/
|
|
93
|
-
function parseTaskId(content: string): string | null {
|
|
94
|
-
const match = content.match(/^\[T(\d+)\]/);
|
|
95
|
-
return match ? `T${match[1]}` : null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Strip ID and status prefixes from content.
|
|
100
|
-
*/
|
|
101
|
-
function stripPrefixes(content: string): string {
|
|
102
|
-
return content
|
|
103
|
-
.replace(/^\[T\d+\]\s*/, '')
|
|
104
|
-
.replace(/^\[!\]\s*/, '')
|
|
105
|
-
.replace(/^\[BLOCKED\]\s*/, '');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Analyze TodoWrite state and detect changes against injected task IDs.
|
|
110
|
-
* @deprecated Use reconcile() from the reconciliation engine instead.
|
|
111
|
-
*/
|
|
112
|
-
export function analyzeChanges(todowriteState: TodoWriteState, injectedIds: string[]): ChangeSet {
|
|
113
|
-
const foundIds: string[] = [];
|
|
114
|
-
const completed: string[] = [];
|
|
115
|
-
const progressed: string[] = [];
|
|
116
|
-
const newTasks: string[] = [];
|
|
117
|
-
|
|
118
|
-
for (const item of todowriteState.todos) {
|
|
119
|
-
const taskId = parseTaskId(item.content);
|
|
120
|
-
|
|
121
|
-
if (taskId) {
|
|
122
|
-
foundIds.push(taskId);
|
|
123
|
-
if (item.status === 'completed') {
|
|
124
|
-
completed.push(taskId);
|
|
125
|
-
} else if (item.status === 'in_progress') {
|
|
126
|
-
progressed.push(taskId);
|
|
127
|
-
}
|
|
128
|
-
} else {
|
|
129
|
-
const cleanTitle = stripPrefixes(item.content);
|
|
130
|
-
if (cleanTitle.trim()) {
|
|
131
|
-
newTasks.push(cleanTitle);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const foundSet = new Set(foundIds);
|
|
137
|
-
const removed = injectedIds.filter((id) => !foundSet.has(id));
|
|
138
|
-
|
|
139
|
-
return { completed, progressed, newTasks, removed };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Core merge function
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Merge TodoWrite state back to CLEO tasks.
|
|
148
|
-
*
|
|
149
|
-
* @deprecated Use the provider-agnostic reconciliation engine instead:
|
|
150
|
-
* import { reconcile } from '../reconciliation/index.js';
|
|
151
|
-
* import { ClaudeCodeTaskSyncProvider } from '@cleocode/adapters';
|
|
152
|
-
*
|
|
153
|
-
* This function is preserved for backward compatibility.
|
|
154
|
-
*/
|
|
155
|
-
export async function mergeTodoWriteState(
|
|
156
|
-
options: TodoWriteMergeOptions,
|
|
157
|
-
): Promise<TodoWriteMergeResult> {
|
|
158
|
-
const { file, dryRun = false, defaultPhase, cwd } = options;
|
|
159
|
-
const acc = options.accessor ?? (await getAccessor(cwd));
|
|
160
|
-
|
|
161
|
-
// Validate input file exists
|
|
162
|
-
try {
|
|
163
|
-
await stat(file);
|
|
164
|
-
} catch {
|
|
165
|
-
throw new CleoError(ExitCode.NOT_FOUND, `File not found: ${file}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Parse TodoWrite state
|
|
169
|
-
const content = await readFile(file, 'utf-8');
|
|
170
|
-
let todowriteState: TodoWriteState;
|
|
171
|
-
try {
|
|
172
|
-
todowriteState = JSON.parse(content) as TodoWriteState;
|
|
173
|
-
} catch {
|
|
174
|
-
throw new CleoError(ExitCode.INVALID_INPUT, `Invalid JSON in ${file}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (!todowriteState.todos || !Array.isArray(todowriteState.todos)) {
|
|
178
|
-
throw new CleoError(ExitCode.INVALID_INPUT, 'File must contain a "todos" array');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Load sync session state
|
|
182
|
-
const cleoDir = getCleoDir(cwd);
|
|
183
|
-
const stateFile = join(cleoDir, 'sync', 'todowrite-session.json');
|
|
184
|
-
let sessionState: SyncSessionState | null = null;
|
|
185
|
-
try {
|
|
186
|
-
sessionState = await readJson<SyncSessionState>(stateFile);
|
|
187
|
-
} catch {
|
|
188
|
-
// No session state — that's fine
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const injectedIds = sessionState?.injected_tasks ?? [];
|
|
192
|
-
|
|
193
|
-
// Analyze changes
|
|
194
|
-
const changes = analyzeChanges(todowriteState, injectedIds);
|
|
195
|
-
|
|
196
|
-
const totalChanges =
|
|
197
|
-
changes.completed.length + changes.progressed.length + changes.newTasks.length;
|
|
198
|
-
|
|
199
|
-
if (totalChanges === 0) {
|
|
200
|
-
return {
|
|
201
|
-
dryRun,
|
|
202
|
-
changes: {
|
|
203
|
-
completed: 0,
|
|
204
|
-
progressed: 0,
|
|
205
|
-
new: 0,
|
|
206
|
-
removed: changes.removed.length,
|
|
207
|
-
applied: 0,
|
|
208
|
-
},
|
|
209
|
-
sessionCleared: false,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
let appliedCount = 0;
|
|
214
|
-
|
|
215
|
-
if (!dryRun) {
|
|
216
|
-
// Load task data once for existence checks
|
|
217
|
-
const { tasks: allTasks } = await acc.queryTasks({});
|
|
218
|
-
|
|
219
|
-
// Apply completed tasks via core completeTask
|
|
220
|
-
for (const taskId of changes.completed) {
|
|
221
|
-
const task = allTasks.find((t) => t.id === taskId);
|
|
222
|
-
if (!task) continue;
|
|
223
|
-
if (task.status === 'done') continue;
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
await completeTask({ taskId, notes: 'Completed via TodoWrite session sync' }, cwd, acc);
|
|
227
|
-
appliedCount++;
|
|
228
|
-
} catch {
|
|
229
|
-
// Task may have dependency issues — skip silently during merge
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Apply progressed tasks via core updateTask
|
|
234
|
-
for (const taskId of changes.progressed) {
|
|
235
|
-
const task = allTasks.find((t) => t.id === taskId);
|
|
236
|
-
if (!task) continue;
|
|
237
|
-
if (task.status !== 'pending' && task.status !== 'blocked') continue;
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
await updateTask(
|
|
241
|
-
{ taskId, status: 'active', notes: 'Progressed during TodoWrite session' },
|
|
242
|
-
cwd,
|
|
243
|
-
acc,
|
|
244
|
-
);
|
|
245
|
-
appliedCount++;
|
|
246
|
-
} catch {
|
|
247
|
-
// Skip tasks that can't be updated
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Create new tasks via core addTask
|
|
252
|
-
for (const title of changes.newTasks) {
|
|
253
|
-
try {
|
|
254
|
-
await addTask(
|
|
255
|
-
{
|
|
256
|
-
title,
|
|
257
|
-
description: 'Created during TodoWrite session',
|
|
258
|
-
labels: ['session-created'],
|
|
259
|
-
...(defaultPhase ? { phase: defaultPhase, addPhase: true } : {}),
|
|
260
|
-
},
|
|
261
|
-
cwd,
|
|
262
|
-
acc,
|
|
263
|
-
);
|
|
264
|
-
appliedCount++;
|
|
265
|
-
} catch {
|
|
266
|
-
// Skip tasks that fail validation (e.g. duplicate title)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Clean up session state
|
|
271
|
-
try {
|
|
272
|
-
await rm(stateFile);
|
|
273
|
-
} catch {
|
|
274
|
-
// ignore
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
dryRun,
|
|
280
|
-
changes: {
|
|
281
|
-
completed: changes.completed.length,
|
|
282
|
-
progressed: changes.progressed.length,
|
|
283
|
-
new: changes.newTasks.length,
|
|
284
|
-
removed: changes.removed.length,
|
|
285
|
-
applied: dryRun ? 0 : appliedCount,
|
|
286
|
-
},
|
|
287
|
-
sessionCleared: !dryRun,
|
|
288
|
-
};
|
|
289
|
-
}
|