@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/core",
3
- "version": "2026.3.39",
3
+ "version": "2026.3.41",
4
4
  "description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,7 +36,7 @@
36
36
  "write-file-atomic": "^6.0.0",
37
37
  "yaml": "^2.8.2",
38
38
  "zod": "^3.25.76",
39
- "@cleocode/contracts": "2026.3.39"
39
+ "@cleocode/contracts": "2026.3.41"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=24.0.0"
@@ -10,7 +10,7 @@ import { writeFile } from 'node:fs/promises';
10
10
  import type { Task } from '@cleocode/contracts';
11
11
  import { getAccessor } from '../store/data-accessor.js';
12
12
 
13
- export type ExportFormat = 'json' | 'csv' | 'tsv' | 'markdown' | 'todowrite';
13
+ export type ExportFormat = 'json' | 'csv' | 'tsv' | 'markdown';
14
14
 
15
15
  function taskToCsvRow(task: Task, delimiter: string): string {
16
16
  const escapeField = (val: string) => {
@@ -38,16 +38,6 @@ function taskToMarkdown(task: Task): string {
38
38
  return `- [${status}] **${task.id}** ${priority} ${task.title}`;
39
39
  }
40
40
 
41
- function taskToTodoWrite(task: Task): Record<string, unknown> {
42
- return {
43
- id: task.id,
44
- content: task.title,
45
- status:
46
- task.status === 'done' ? 'completed' : task.status === 'active' ? 'in_progress' : 'pending',
47
- priority: task.priority === 'critical' ? 'high' : task.priority,
48
- };
49
- }
50
-
51
41
  export interface ExportParams {
52
42
  format?: ExportFormat;
53
43
  output?: string;
@@ -132,13 +122,8 @@ export async function exportTasks(params: ExportParams): Promise<ExportResult> {
132
122
  content = lines.join('\n');
133
123
  break;
134
124
  }
135
- case 'todowrite': {
136
- const items = tasks.map(taskToTodoWrite);
137
- content = JSON.stringify(items, null, 2);
138
- break;
139
- }
140
125
  default:
141
- throw new Error(`Unknown format: ${format}. Valid: json, csv, tsv, markdown, todowrite`);
126
+ throw new Error(`Unknown format: ${format}. Valid: json, csv, tsv, markdown`);
142
127
  }
143
128
 
144
129
  if (params.output) {
package/src/cleo.ts CHANGED
@@ -18,9 +18,9 @@ import path from 'node:path';
18
18
  import type {
19
19
  DataAccessor,
20
20
  ExternalTask,
21
+ ExternalTaskLink,
21
22
  ReconcileOptions,
22
23
  ReconcileResult,
23
- SyncSessionState,
24
24
  Task,
25
25
  TaskPriority,
26
26
  TaskSize,
@@ -80,7 +80,11 @@ import {
80
80
  } from './orchestration/index.js';
81
81
  // Reconciliation (sync)
82
82
  import { reconcile } from './reconciliation/index.js';
83
- import { clearSyncState, readSyncState, writeSyncState } from './reconciliation/sync-state.js';
83
+ import {
84
+ getLinksByProvider,
85
+ getLinksByTaskId,
86
+ removeLinksByProvider,
87
+ } from './reconciliation/link-store.js';
84
88
 
85
89
  // Release
86
90
  import {
@@ -303,6 +307,7 @@ export interface NexusAPI {
303
307
  }
304
308
 
305
309
  export interface SyncAPI {
310
+ /** Reconcile external tasks with CLEO as SSoT. */
306
311
  reconcile(params: {
307
312
  externalTasks: ExternalTask[];
308
313
  providerId: string;
@@ -311,9 +316,12 @@ export interface SyncAPI {
311
316
  defaultPhase?: string;
312
317
  defaultLabels?: string[];
313
318
  }): Promise<ReconcileResult>;
314
- readState(providerId: string): Promise<SyncSessionState | null>;
315
- writeState(providerId: string, state: SyncSessionState): Promise<void>;
316
- clearState(providerId: string): Promise<void>;
319
+ /** Get all external task links for a provider. */
320
+ getLinks(providerId: string): Promise<ExternalTaskLink[]>;
321
+ /** Get all external task links for a CLEO task. */
322
+ getTaskLinks(taskId: string): Promise<ExternalTaskLink[]>;
323
+ /** Remove all external task links for a provider. */
324
+ removeProviderLinks(providerId: string): Promise<number>;
317
325
  }
318
326
 
319
327
  // ============================================================================
@@ -580,9 +588,9 @@ export class Cleo {
580
588
  },
581
589
  store,
582
590
  ),
583
- readState: (providerId) => readSyncState(providerId, root),
584
- writeState: (providerId, state) => writeSyncState(providerId, state, root),
585
- clearState: (providerId) => clearSyncState(providerId, root),
591
+ getLinks: (providerId) => getLinksByProvider(providerId, root),
592
+ getTaskLinks: (taskId) => getLinksByTaskId(taskId, root),
593
+ removeProviderLinks: (providerId) => removeLinksByProvider(providerId, root),
586
594
  };
587
595
  }
588
596
  }
@@ -1,5 +1,7 @@
1
1
  /**
2
- * TodoWrite injection core module.
2
+ * Task injection core module.
3
+ *
4
+ * Selects and formats tasks for injection into external systems.
3
5
  *
4
6
  * ARCHITECTURE NOTE: Instruction injection is a CAAMP domain responsibility.
5
7
  * Once @cleocode/caamp is available as a dependency, the injection formatting
@@ -15,8 +17,6 @@ import type { Task, TaskFile } from '@cleocode/contracts';
15
17
  import type { DataAccessor } from '../store/data-accessor.js';
16
18
  import { getAccessor } from '../store/data-accessor.js';
17
19
 
18
- // CLEO-native injection (CAAMP InstructionInjector not yet available as separate package)
19
-
20
20
  /**
21
21
  * Select tasks eligible for injection based on filters.
22
22
  * This is CLEO-specific task selection logic (stays in CLEO).
@@ -59,11 +59,11 @@ function selectTasksForInjection(
59
59
  }
60
60
 
61
61
  /**
62
- * Format tasks for TodoWrite injection.
62
+ * Format tasks for injection.
63
63
  * Format: [T###] [!]? [BLOCKED]? <title>
64
64
  * @task T4539
65
65
  */
66
- function formatForTodoWrite(tasks: Task[]): Array<{ id: string; text: string; status: string }> {
66
+ function formatForInjection(tasks: Task[]): Array<{ id: string; text: string; status: string }> {
67
67
  return tasks.map((t) => {
68
68
  let prefix = `[${t.id}]`;
69
69
  if (t.priority === 'critical' || t.priority === 'high') prefix += ' [!]';
@@ -72,7 +72,7 @@ function formatForTodoWrite(tasks: Task[]): Array<{ id: string; text: string; st
72
72
  });
73
73
  }
74
74
 
75
- /** Inject tasks into TodoWrite format. */
75
+ /** Inject tasks for external consumption. */
76
76
  export async function injectTasks(
77
77
  opts: {
78
78
  maxTasks?: number;
@@ -97,7 +97,7 @@ export async function injectTasks(
97
97
  } as TaskFile;
98
98
 
99
99
  const selectedTasks = selectTasksForInjection(data, opts);
100
- const formatted = formatForTodoWrite(selectedTasks);
100
+ const formatted = formatForInjection(selectedTasks);
101
101
 
102
102
  const phase = opts.phase ?? projectMeta?.currentPhase ?? null;
103
103
 
package/src/internal.ts CHANGED
@@ -28,12 +28,6 @@ export { exportTasksPackage } from './admin/export-tasks.js';
28
28
  export { computeHelp } from './admin/help.js';
29
29
  export { importTasks } from './admin/import.js';
30
30
  export { importTasksPackage } from './admin/import-tasks.js';
31
- // Alias for cleo compatibility (admin sync status)
32
- export {
33
- clearSyncState,
34
- getSyncStatus,
35
- getSyncStatus as getAdminSyncStatus,
36
- } from './admin/sync.js';
37
31
  // ADRs
38
32
  export { findAdrs } from './adrs/find.js';
39
33
  export { listAdrs, showAdr, syncAdrsToDb, validateAllAdrs } from './adrs/index.js';
@@ -206,10 +200,13 @@ export { listPhases, showPhase } from './pipeline/index.js';
206
200
  export { getNodeUpgradeInstructions, getNodeVersionInfo } from './platform.js';
207
201
  // Reconciliation (additional)
208
202
  export {
209
- clearSyncState as clearProviderSyncState,
210
- readSyncState,
211
- writeSyncState,
212
- } from './reconciliation/sync-state.js';
203
+ createLink,
204
+ getLinkByExternalId,
205
+ getLinksByProvider,
206
+ getLinksByTaskId,
207
+ removeLinksByProvider,
208
+ touchLink,
209
+ } from './reconciliation/link-store.js';
213
210
  // Release
214
211
  export { channelToDistTag, describeChannel, resolveChannelFromBranch } from './release/channel.js';
215
212
  export type { PRResult } from './release/github-pr.js';
@@ -338,7 +335,13 @@ export { computeChecksum, readJson } from './store/json.js';
338
335
  export { createSession, getActiveSession } from './store/session-store.js';
339
336
  export { getDb, getNativeDb } from './store/sqlite.js';
340
337
  export { createTask } from './store/task-store.js';
341
- export { auditLog, releaseManifests, taskDependencies, tasks } from './store/tasks-schema.js';
338
+ export {
339
+ auditLog,
340
+ externalTaskLinks,
341
+ releaseManifests,
342
+ taskDependencies,
343
+ tasks,
344
+ } from './store/tasks-schema.js';
342
345
  export { AuditLogInsertSchema } from './store/validation-schemas.js';
343
346
  export type {
344
347
  AnalyzeArchiveOptions,
@@ -384,18 +387,6 @@ export {
384
387
  // Task work (additional)
385
388
  export type { TaskWorkHistoryEntry } from './task-work/index.js';
386
389
  export { getTaskHistory } from './task-work/index.js';
387
- export type {
388
- ChangeSet as TodoWriteChangeSet,
389
- SyncSessionState,
390
- TodoWriteItem,
391
- TodoWriteMergeOptions,
392
- TodoWriteMergeResult,
393
- TodoWriteState,
394
- } from './task-work/todowrite-merge.js';
395
- export {
396
- analyzeChanges as analyzeTodoWriteChanges,
397
- mergeTodoWriteState,
398
- } from './task-work/todowrite-merge.js';
399
390
  // Tasks (additional)
400
391
  export { validateLabels } from './tasks/add.js';
401
392
  export { getCriticalPath } from './tasks/graph-ops.js';
@@ -1,8 +1,17 @@
1
1
  /**
2
2
  * Provider-agnostic task reconciliation module.
3
3
  *
4
- * @task T5800
4
+ * Provides the reconciliation engine for syncing external task systems
5
+ * (Linear, Jira, GitHub Issues, etc.) with CLEO as SSoT, plus DB-backed
6
+ * link tracking via the external_task_links table.
5
7
  */
6
8
 
9
+ export {
10
+ createLink,
11
+ getLinkByExternalId,
12
+ getLinksByProvider,
13
+ getLinksByTaskId,
14
+ removeLinksByProvider,
15
+ touchLink,
16
+ } from './link-store.js';
7
17
  export { reconcile } from './reconciliation-engine.js';
8
- export { clearSyncState, readSyncState, writeSyncState } from './sync-state.js';
@@ -0,0 +1,174 @@
1
+ /**
2
+ * External task link persistence — DB-backed link tracking for reconciliation.
3
+ *
4
+ * Manages the external_task_links table in tasks.db. Used by the reconciliation
5
+ * engine to match external tasks to existing CLEO tasks and track sync history.
6
+ */
7
+
8
+ import { randomUUID } from 'node:crypto';
9
+ import type { ExternalTaskLink } from '@cleocode/contracts';
10
+ import { and, eq } from 'drizzle-orm';
11
+ import { getDb } from '../store/sqlite.js';
12
+ import { externalTaskLinks } from '../store/tasks-schema.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Read operations
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Find all links for a given provider.
20
+ */
21
+ export async function getLinksByProvider(
22
+ providerId: string,
23
+ cwd?: string,
24
+ ): Promise<ExternalTaskLink[]> {
25
+ const db = await getDb(cwd);
26
+ const rows = await db
27
+ .select()
28
+ .from(externalTaskLinks)
29
+ .where(eq(externalTaskLinks.providerId, providerId));
30
+ return rows.map(rowToLink);
31
+ }
32
+
33
+ /**
34
+ * Find a link by provider + external ID.
35
+ */
36
+ export async function getLinkByExternalId(
37
+ providerId: string,
38
+ externalId: string,
39
+ cwd?: string,
40
+ ): Promise<ExternalTaskLink | null> {
41
+ const db = await getDb(cwd);
42
+ const rows = await db
43
+ .select()
44
+ .from(externalTaskLinks)
45
+ .where(
46
+ and(
47
+ eq(externalTaskLinks.providerId, providerId),
48
+ eq(externalTaskLinks.externalId, externalId),
49
+ ),
50
+ );
51
+ return rows.length > 0 ? rowToLink(rows[0]!) : null;
52
+ }
53
+
54
+ /**
55
+ * Find all links for a given CLEO task.
56
+ */
57
+ export async function getLinksByTaskId(taskId: string, cwd?: string): Promise<ExternalTaskLink[]> {
58
+ const db = await getDb(cwd);
59
+ const rows = await db
60
+ .select()
61
+ .from(externalTaskLinks)
62
+ .where(eq(externalTaskLinks.taskId, taskId));
63
+ return rows.map(rowToLink);
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Write operations
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Create a new external task link.
72
+ */
73
+ export async function createLink(
74
+ params: {
75
+ taskId: string;
76
+ providerId: string;
77
+ externalId: string;
78
+ externalUrl?: string;
79
+ externalTitle?: string;
80
+ linkType: ExternalTaskLink['linkType'];
81
+ syncDirection?: ExternalTaskLink['syncDirection'];
82
+ metadata?: Record<string, unknown>;
83
+ },
84
+ cwd?: string,
85
+ ): Promise<ExternalTaskLink> {
86
+ const db = await getDb(cwd);
87
+ const now = new Date().toISOString();
88
+ const id = randomUUID();
89
+
90
+ await db.insert(externalTaskLinks).values({
91
+ id,
92
+ taskId: params.taskId,
93
+ providerId: params.providerId,
94
+ externalId: params.externalId,
95
+ externalUrl: params.externalUrl ?? null,
96
+ externalTitle: params.externalTitle ?? null,
97
+ linkType: params.linkType,
98
+ syncDirection: params.syncDirection ?? 'inbound',
99
+ metadataJson: params.metadata ? JSON.stringify(params.metadata) : '{}',
100
+ linkedAt: now,
101
+ lastSyncAt: now,
102
+ });
103
+
104
+ return {
105
+ id,
106
+ taskId: params.taskId,
107
+ providerId: params.providerId,
108
+ externalId: params.externalId,
109
+ externalUrl: params.externalUrl ?? null,
110
+ externalTitle: params.externalTitle ?? null,
111
+ linkType: params.linkType,
112
+ syncDirection: params.syncDirection ?? 'inbound',
113
+ metadata: params.metadata,
114
+ linkedAt: now,
115
+ lastSyncAt: now,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Update the lastSyncAt and optionally the title/metadata for an existing link.
121
+ */
122
+ export async function touchLink(
123
+ linkId: string,
124
+ updates?: {
125
+ externalTitle?: string;
126
+ metadata?: Record<string, unknown>;
127
+ },
128
+ cwd?: string,
129
+ ): Promise<void> {
130
+ const db = await getDb(cwd);
131
+ const now = new Date().toISOString();
132
+ const values: Record<string, unknown> = { lastSyncAt: now };
133
+ if (updates?.externalTitle !== undefined) {
134
+ values.externalTitle = updates.externalTitle;
135
+ }
136
+ if (updates?.metadata !== undefined) {
137
+ values.metadataJson = JSON.stringify(updates.metadata);
138
+ }
139
+
140
+ await db.update(externalTaskLinks).set(values).where(eq(externalTaskLinks.id, linkId));
141
+ }
142
+
143
+ /**
144
+ * Remove all links for a provider (used during provider deregistration).
145
+ */
146
+ export async function removeLinksByProvider(providerId: string, cwd?: string): Promise<number> {
147
+ const db = await getDb(cwd);
148
+ const result = await db
149
+ .delete(externalTaskLinks)
150
+ .where(eq(externalTaskLinks.providerId, providerId));
151
+ return Number(result.changes);
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Helpers
156
+ // ---------------------------------------------------------------------------
157
+
158
+ function rowToLink(row: typeof externalTaskLinks.$inferSelect): ExternalTaskLink {
159
+ return {
160
+ id: row.id,
161
+ taskId: row.taskId,
162
+ providerId: row.providerId,
163
+ externalId: row.externalId,
164
+ externalUrl: row.externalUrl,
165
+ externalTitle: row.externalTitle,
166
+ linkType: row.linkType as ExternalTaskLink['linkType'],
167
+ syncDirection: row.syncDirection as ExternalTaskLink['syncDirection'],
168
+ metadata: row.metadataJson
169
+ ? (JSON.parse(row.metadataJson) as Record<string, unknown>)
170
+ : undefined,
171
+ linkedAt: row.linkedAt,
172
+ lastSyncAt: row.lastSyncAt,
173
+ };
174
+ }