@cleocode/core 2026.3.43 → 2026.3.45
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/admin/export-tasks.d.ts.map +1 -1
- package/dist/admin/import-tasks.d.ts +10 -2
- package/dist/admin/import-tasks.d.ts.map +1 -1
- package/dist/agents/agent-schema.d.ts +358 -0
- package/dist/agents/agent-schema.d.ts.map +1 -0
- package/dist/agents/capacity.d.ts +57 -0
- package/dist/agents/capacity.d.ts.map +1 -0
- package/dist/agents/index.d.ts +17 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/registry.d.ts +115 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/retry.d.ts +83 -0
- package/dist/agents/retry.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +4 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +214 -0
- package/dist/hooks/payload-schemas.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16937 -2371
- package/dist/index.js.map +4 -4
- package/dist/inject/index.d.ts.map +1 -1
- package/dist/intelligence/impact.d.ts +51 -0
- package/dist/intelligence/impact.d.ts.map +1 -0
- package/dist/intelligence/index.d.ts +15 -0
- package/dist/intelligence/index.d.ts.map +1 -0
- package/dist/intelligence/patterns.d.ts +66 -0
- package/dist/intelligence/patterns.d.ts.map +1 -0
- package/dist/intelligence/prediction.d.ts +51 -0
- package/dist/intelligence/prediction.d.ts.map +1 -0
- package/dist/intelligence/types.d.ts +221 -0
- package/dist/intelligence/types.d.ts.map +1 -0
- package/dist/internal.d.ts +12 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/issue/template-parser.d.ts +8 -2
- package/dist/issue/template-parser.d.ts.map +1 -1
- package/dist/lifecycle/pipeline.d.ts +2 -2
- package/dist/lifecycle/pipeline.d.ts.map +1 -1
- package/dist/lifecycle/state-machine.d.ts +1 -1
- package/dist/lifecycle/state-machine.d.ts.map +1 -1
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-row-types.d.ts +40 -6
- package/dist/memory/brain-row-types.d.ts.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-similarity.d.ts.map +1 -1
- package/dist/memory/claude-mem-migration.d.ts.map +1 -1
- package/dist/nexus/discover.d.ts.map +1 -1
- package/dist/nexus/index.d.ts +2 -0
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/transfer-types.d.ts +123 -0
- package/dist/nexus/transfer-types.d.ts.map +1 -0
- package/dist/nexus/transfer.d.ts +31 -0
- package/dist/nexus/transfer.d.ts.map +1 -0
- package/dist/orchestration/bootstrap.d.ts.map +1 -1
- package/dist/orchestration/skill-ops.d.ts +4 -4
- package/dist/orchestration/skill-ops.d.ts.map +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.d.ts.map +1 -1
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/types.d.ts +8 -42
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/signaldock/signaldock-transport.d.ts +1 -1
- package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
- package/dist/skills/injection/subagent.d.ts +3 -3
- package/dist/skills/injection/subagent.d.ts.map +1 -1
- package/dist/skills/manifests/contribution.d.ts +2 -2
- package/dist/skills/manifests/contribution.d.ts.map +1 -1
- package/dist/skills/orchestrator/spawn.d.ts +6 -6
- package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
- package/dist/skills/orchestrator/startup.d.ts +1 -1
- package/dist/skills/orchestrator/startup.d.ts.map +1 -1
- package/dist/skills/orchestrator/validator.d.ts +2 -2
- package/dist/skills/orchestrator/validator.d.ts.map +1 -1
- package/dist/skills/precedence-types.d.ts +24 -1
- package/dist/skills/precedence-types.d.ts.map +1 -1
- package/dist/skills/types.d.ts +70 -4
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/store/brain-sqlite.d.ts +4 -1
- package/dist/store/brain-sqlite.d.ts.map +1 -1
- package/dist/store/export.d.ts +5 -4
- package/dist/store/export.d.ts.map +1 -1
- package/dist/store/nexus-sqlite.d.ts +4 -1
- package/dist/store/nexus-sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts +4 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +14 -4
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/typed-query.d.ts +12 -0
- package/dist/store/typed-query.d.ts.map +1 -0
- package/dist/store/validation-schemas.d.ts +2423 -50
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/inject-generate.d.ts.map +1 -1
- package/dist/validation/doctor/checks.d.ts +5 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/engine.d.ts +10 -10
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/index.d.ts +6 -2
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/protocol-common.d.ts +10 -2
- package/dist/validation/protocol-common.d.ts.map +1 -1
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
- package/package.json +2 -2
- package/src/admin/export-tasks.ts +2 -5
- package/src/admin/import-tasks.ts +53 -29
- package/src/agents/__tests__/capacity.test.ts +219 -0
- package/src/agents/__tests__/registry.test.ts +457 -0
- package/src/agents/__tests__/retry.test.ts +289 -0
- package/src/agents/agent-schema.ts +107 -0
- package/src/agents/capacity.ts +151 -0
- package/src/agents/index.ts +68 -0
- package/src/agents/registry.ts +449 -0
- package/src/agents/retry.ts +255 -0
- package/src/hooks/index.ts +20 -1
- package/src/hooks/payload-schemas.ts +199 -0
- package/src/index.ts +69 -0
- package/src/inject/index.ts +14 -14
- package/src/intelligence/__tests__/impact.test.ts +453 -0
- package/src/intelligence/__tests__/patterns.test.ts +450 -0
- package/src/intelligence/__tests__/prediction.test.ts +418 -0
- package/src/intelligence/impact.ts +638 -0
- package/src/intelligence/index.ts +47 -0
- package/src/intelligence/patterns.ts +621 -0
- package/src/intelligence/prediction.ts +621 -0
- package/src/intelligence/types.ts +273 -0
- package/src/internal.ts +89 -2
- package/src/issue/template-parser.ts +65 -4
- package/src/lifecycle/pipeline.ts +14 -7
- package/src/lifecycle/state-machine.ts +6 -2
- package/src/memory/brain-lifecycle.ts +5 -11
- package/src/memory/brain-retrieval.ts +44 -38
- package/src/memory/brain-row-types.ts +43 -6
- package/src/memory/brain-search.ts +53 -32
- package/src/memory/brain-similarity.ts +9 -8
- package/src/memory/claude-mem-migration.ts +4 -3
- package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
- package/src/nexus/__tests__/transfer.test.ts +446 -0
- package/src/nexus/discover.ts +1 -0
- package/src/nexus/index.ts +14 -0
- package/src/nexus/transfer-types.ts +129 -0
- package/src/nexus/transfer.ts +314 -0
- package/src/orchestration/bootstrap.ts +11 -17
- package/src/orchestration/skill-ops.ts +52 -32
- package/src/otel/index.ts +48 -4
- package/src/sessions/__tests__/briefing.test.ts +31 -2
- package/src/sessions/briefing.ts +27 -42
- package/src/sessions/handoff.ts +52 -86
- package/src/sessions/index.ts +5 -1
- package/src/sessions/types.ts +9 -43
- package/src/signaldock/signaldock-transport.ts +5 -2
- package/src/skills/injection/subagent.ts +10 -16
- package/src/skills/manifests/contribution.ts +5 -13
- package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
- package/src/skills/orchestrator/spawn.ts +18 -31
- package/src/skills/orchestrator/startup.ts +78 -65
- package/src/skills/orchestrator/validator.ts +26 -31
- package/src/skills/precedence-types.ts +24 -1
- package/src/skills/types.ts +72 -5
- package/src/store/__tests__/test-db-helper.d.ts +4 -4
- package/src/store/__tests__/test-db-helper.js +5 -16
- package/src/store/__tests__/test-db-helper.ts +5 -18
- package/src/store/brain-sqlite.ts +7 -3
- package/src/store/chain-schema.ts +1 -1
- package/src/store/export.ts +22 -12
- package/src/store/nexus-sqlite.ts +7 -3
- package/src/store/sqlite.ts +9 -3
- package/src/store/tasks-schema.ts +65 -8
- package/src/store/typed-query.ts +17 -0
- package/src/store/validation-schemas.ts +347 -23
- package/src/system/inject-generate.ts +9 -23
- package/src/validation/doctor/checks.ts +24 -2
- package/src/validation/engine.ts +11 -11
- package/src/validation/index.ts +131 -3
- package/src/validation/protocol-common.ts +54 -3
- package/dist/tasks/reparent.d.ts +0 -38
- package/dist/tasks/reparent.d.ts.map +0 -1
- package/src/tasks/reparent.ts +0 -134
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-project task transfer engine for NEXUS.
|
|
3
|
+
*
|
|
4
|
+
* Provides executeTransfer() and previewTransfer() for moving/copying
|
|
5
|
+
* tasks between registered NEXUS projects with full provenance tracking.
|
|
6
|
+
*
|
|
7
|
+
* @task T046, T049, T050, T051, T052, T053
|
|
8
|
+
* @epic T4540
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { importFromPackage } from '../admin/import-tasks.js';
|
|
13
|
+
import { getLogger } from '../logger.js';
|
|
14
|
+
import { createLink } from '../reconciliation/link-store.js';
|
|
15
|
+
import { BrainDataAccessor } from '../store/brain-accessor.js';
|
|
16
|
+
import { getBrainDb } from '../store/brain-sqlite.js';
|
|
17
|
+
import { getAccessor } from '../store/data-accessor.js';
|
|
18
|
+
import { exportSingle, exportSubtree } from '../store/export.js';
|
|
19
|
+
import { requirePermission } from './permissions.js';
|
|
20
|
+
import { nexusGetProject } from './registry.js';
|
|
21
|
+
import type {
|
|
22
|
+
TransferManifest,
|
|
23
|
+
TransferManifestEntry,
|
|
24
|
+
TransferParams,
|
|
25
|
+
TransferResult,
|
|
26
|
+
} from './transfer-types.js';
|
|
27
|
+
|
|
28
|
+
const log = getLogger('nexus:transfer');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Preview a transfer without writing any data.
|
|
32
|
+
* Validates projects, permissions, and builds the manifest.
|
|
33
|
+
*/
|
|
34
|
+
export async function previewTransfer(params: TransferParams): Promise<TransferResult> {
|
|
35
|
+
return executeTransferInternal({ ...params, dryRun: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Execute a cross-project task transfer.
|
|
40
|
+
*
|
|
41
|
+
* Pipeline:
|
|
42
|
+
* 1. Validate source/target projects via nexusGetProject()
|
|
43
|
+
* 2. Check permissions: read on source, write on target
|
|
44
|
+
* 3. Read source tasks
|
|
45
|
+
* 4. Build ExportPackage via exportSubtree/exportSingle
|
|
46
|
+
* 5. Import into target via importFromPackage()
|
|
47
|
+
* 6. Create bidirectional external_task_links
|
|
48
|
+
* 7. Write nexus_audit_log entry
|
|
49
|
+
* 8. If move mode: archive source tasks
|
|
50
|
+
* 9. If transferBrain: copy brain observations
|
|
51
|
+
*/
|
|
52
|
+
export async function executeTransfer(params: TransferParams): Promise<TransferResult> {
|
|
53
|
+
return executeTransferInternal(params);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function executeTransferInternal(params: TransferParams): Promise<TransferResult> {
|
|
57
|
+
const {
|
|
58
|
+
taskIds,
|
|
59
|
+
sourceProject: sourceProjectRef,
|
|
60
|
+
targetProject: targetProjectRef,
|
|
61
|
+
mode = 'copy',
|
|
62
|
+
scope = 'subtree',
|
|
63
|
+
onConflict = 'rename',
|
|
64
|
+
onMissingDep = 'strip',
|
|
65
|
+
provenance = true,
|
|
66
|
+
targetParent,
|
|
67
|
+
transferBrain = false,
|
|
68
|
+
dryRun = false,
|
|
69
|
+
} = params;
|
|
70
|
+
|
|
71
|
+
if (!taskIds.length) {
|
|
72
|
+
throw new Error('No task IDs specified for transfer');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 1: Validate projects
|
|
76
|
+
const sourceProject = await nexusGetProject(sourceProjectRef);
|
|
77
|
+
if (!sourceProject) {
|
|
78
|
+
throw new Error(`Source project not found: ${sourceProjectRef}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const targetProject = await nexusGetProject(targetProjectRef);
|
|
82
|
+
if (!targetProject) {
|
|
83
|
+
throw new Error(`Target project not found: ${targetProjectRef}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (sourceProject.hash === targetProject.hash) {
|
|
87
|
+
throw new Error('Source and target projects must be different');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Step 2: Check permissions
|
|
91
|
+
await requirePermission(sourceProject.hash, 'read', 'nexus.transfer');
|
|
92
|
+
await requirePermission(targetProject.hash, 'write', 'nexus.transfer');
|
|
93
|
+
|
|
94
|
+
// Step 3: Read source tasks
|
|
95
|
+
const sourceAccessor = await getAccessor(sourceProject.path);
|
|
96
|
+
const { tasks: allSourceTasks } = await sourceAccessor.queryTasks({});
|
|
97
|
+
|
|
98
|
+
// Step 4: Build ExportPackage for each task
|
|
99
|
+
const exportPackages = [];
|
|
100
|
+
for (const taskId of taskIds) {
|
|
101
|
+
const pkg =
|
|
102
|
+
scope === 'subtree'
|
|
103
|
+
? exportSubtree(taskId, allSourceTasks, sourceProject.name)
|
|
104
|
+
: exportSingle(taskId, allSourceTasks, sourceProject.name);
|
|
105
|
+
|
|
106
|
+
if (!pkg) {
|
|
107
|
+
throw new Error(`Task not found in source project: ${taskId}`);
|
|
108
|
+
}
|
|
109
|
+
exportPackages.push(pkg);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge all packages into one (dedup by task ID)
|
|
113
|
+
const seenIds = new Set<string>();
|
|
114
|
+
const mergedTasks = [];
|
|
115
|
+
for (const pkg of exportPackages) {
|
|
116
|
+
for (const task of pkg.tasks) {
|
|
117
|
+
if (!seenIds.has(task.id)) {
|
|
118
|
+
seenIds.add(task.id);
|
|
119
|
+
mergedTasks.push(task);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Use the first package as base and replace tasks
|
|
125
|
+
const mergedPkg = { ...exportPackages[0]! };
|
|
126
|
+
mergedPkg.tasks = mergedTasks;
|
|
127
|
+
mergedPkg._meta = { ...mergedPkg._meta, taskCount: mergedTasks.length };
|
|
128
|
+
|
|
129
|
+
// Step 5: Import into target
|
|
130
|
+
const importResult = await importFromPackage(mergedPkg, {
|
|
131
|
+
cwd: targetProject.path,
|
|
132
|
+
dryRun,
|
|
133
|
+
parent: targetParent,
|
|
134
|
+
provenance,
|
|
135
|
+
onConflict,
|
|
136
|
+
onMissingDep,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Build manifest
|
|
140
|
+
const entries: TransferManifestEntry[] = mergedTasks.map((t) => ({
|
|
141
|
+
sourceId: t.id,
|
|
142
|
+
targetId: importResult.idRemap[t.id] ?? t.id,
|
|
143
|
+
title: t.title,
|
|
144
|
+
type: t.type ?? 'task',
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const manifest: TransferManifest = {
|
|
148
|
+
sourceProject: sourceProject.name,
|
|
149
|
+
targetProject: targetProject.name,
|
|
150
|
+
mode,
|
|
151
|
+
scope,
|
|
152
|
+
entries,
|
|
153
|
+
idRemap: importResult.idRemap,
|
|
154
|
+
brainObservationsTransferred: 0,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const result: TransferResult = {
|
|
158
|
+
dryRun,
|
|
159
|
+
transferred: importResult.imported,
|
|
160
|
+
skipped: importResult.skipped,
|
|
161
|
+
archived: 0,
|
|
162
|
+
linksCreated: 0,
|
|
163
|
+
brainObservationsTransferred: 0,
|
|
164
|
+
manifest,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (dryRun) {
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Step 6: Create bidirectional external_task_links
|
|
172
|
+
// Only create links for tasks that were actually imported (verify they exist in target)
|
|
173
|
+
let linksCreated = 0;
|
|
174
|
+
const targetAccessor = await getAccessor(targetProject.path);
|
|
175
|
+
const { tasks: targetTasks } = await targetAccessor.queryTasks({});
|
|
176
|
+
const targetTaskIds = new Set(targetTasks.map((t) => t.id));
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (importResult.idRemap[entry.sourceId] && targetTaskIds.has(entry.targetId)) {
|
|
180
|
+
// Link in target: points back to source
|
|
181
|
+
await createLink(
|
|
182
|
+
{
|
|
183
|
+
taskId: entry.targetId,
|
|
184
|
+
providerId: `nexus:${sourceProject.name}`,
|
|
185
|
+
externalId: entry.sourceId,
|
|
186
|
+
externalTitle: entry.title,
|
|
187
|
+
linkType: 'transferred',
|
|
188
|
+
syncDirection: 'inbound',
|
|
189
|
+
metadata: {
|
|
190
|
+
transferMode: mode,
|
|
191
|
+
transferScope: scope,
|
|
192
|
+
sourceProject: sourceProject.name,
|
|
193
|
+
transferredAt: new Date().toISOString(),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
targetProject.path,
|
|
197
|
+
);
|
|
198
|
+
linksCreated++;
|
|
199
|
+
|
|
200
|
+
// Link in source: points to target
|
|
201
|
+
await createLink(
|
|
202
|
+
{
|
|
203
|
+
taskId: entry.sourceId,
|
|
204
|
+
providerId: `nexus:${targetProject.name}`,
|
|
205
|
+
externalId: entry.targetId,
|
|
206
|
+
externalTitle: entry.title,
|
|
207
|
+
linkType: 'transferred',
|
|
208
|
+
syncDirection: 'outbound',
|
|
209
|
+
metadata: {
|
|
210
|
+
transferMode: mode,
|
|
211
|
+
transferScope: scope,
|
|
212
|
+
targetProject: targetProject.name,
|
|
213
|
+
transferredAt: new Date().toISOString(),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
sourceProject.path,
|
|
217
|
+
);
|
|
218
|
+
linksCreated++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
result.linksCreated = linksCreated;
|
|
222
|
+
|
|
223
|
+
// Step 7: Write audit log
|
|
224
|
+
try {
|
|
225
|
+
const { getNexusDb } = await import('../store/nexus-sqlite.js');
|
|
226
|
+
const { nexusAuditLog } = await import('../store/nexus-schema.js');
|
|
227
|
+
const db = await getNexusDb();
|
|
228
|
+
await db.insert(nexusAuditLog).values({
|
|
229
|
+
id: randomUUID(),
|
|
230
|
+
action: 'transfer',
|
|
231
|
+
projectHash: sourceProject.hash,
|
|
232
|
+
projectId: sourceProject.projectId,
|
|
233
|
+
domain: 'nexus',
|
|
234
|
+
operation: 'transfer',
|
|
235
|
+
success: 1,
|
|
236
|
+
detailsJson: JSON.stringify({
|
|
237
|
+
sourceProject: sourceProject.name,
|
|
238
|
+
targetProject: targetProject.name,
|
|
239
|
+
mode,
|
|
240
|
+
scope,
|
|
241
|
+
taskCount: result.transferred,
|
|
242
|
+
idRemap: importResult.idRemap,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
} catch (err) {
|
|
246
|
+
log.warn({ err }, 'nexus transfer audit write failed');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 8: Move mode — archive source tasks
|
|
250
|
+
if (mode === 'move') {
|
|
251
|
+
let archived = 0;
|
|
252
|
+
for (const entry of entries) {
|
|
253
|
+
if (importResult.idRemap[entry.sourceId]) {
|
|
254
|
+
try {
|
|
255
|
+
await sourceAccessor.archiveSingleTask(entry.sourceId, {
|
|
256
|
+
archivedAt: new Date().toISOString(),
|
|
257
|
+
archiveReason: `Transferred to ${targetProject.name} as ${entry.targetId}`,
|
|
258
|
+
});
|
|
259
|
+
archived++;
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.warn({ err, taskId: entry.sourceId }, 'failed to archive source task after transfer');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
result.archived = archived;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Step 9: Brain observation transfer
|
|
269
|
+
if (transferBrain) {
|
|
270
|
+
let brainTransferred = 0;
|
|
271
|
+
try {
|
|
272
|
+
const sourceBrainDb = await getBrainDb(sourceProject.path);
|
|
273
|
+
const targetBrainDb = await getBrainDb(targetProject.path);
|
|
274
|
+
const sourceBrain = new BrainDataAccessor(sourceBrainDb);
|
|
275
|
+
const targetBrain = new BrainDataAccessor(targetBrainDb);
|
|
276
|
+
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
if (!importResult.idRemap[entry.sourceId]) continue;
|
|
279
|
+
|
|
280
|
+
const links = await sourceBrain.getLinksForTask(entry.sourceId);
|
|
281
|
+
for (const link of links) {
|
|
282
|
+
if (link.memoryType !== 'observation') continue;
|
|
283
|
+
|
|
284
|
+
const observation = await sourceBrain.getObservation(link.memoryId);
|
|
285
|
+
if (!observation) continue;
|
|
286
|
+
|
|
287
|
+
const newObsId = `O-${randomUUID().slice(0, 8)}`;
|
|
288
|
+
await targetBrain.addObservation({
|
|
289
|
+
...observation,
|
|
290
|
+
id: newObsId,
|
|
291
|
+
createdAt: observation.createdAt,
|
|
292
|
+
updatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await targetBrain.addLink({
|
|
296
|
+
memoryType: 'observation',
|
|
297
|
+
memoryId: newObsId,
|
|
298
|
+
taskId: entry.targetId,
|
|
299
|
+
linkType: 'applies_to',
|
|
300
|
+
createdAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
brainTransferred++;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
log.warn({ err }, 'brain observation transfer failed');
|
|
308
|
+
}
|
|
309
|
+
result.brainObservationsTransferred = brainTransferred;
|
|
310
|
+
result.manifest.brainObservationsTransferred = brainTransferred;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import type { BrainState } from '@cleocode/contracts';
|
|
10
|
-
import { getSessionsPath } from '../paths.js';
|
|
11
10
|
import type { DataAccessor } from '../store/data-accessor.js';
|
|
12
11
|
import { getAccessor } from '../store/data-accessor.js';
|
|
13
12
|
|
|
@@ -26,29 +25,24 @@ export async function buildBrainState(
|
|
|
26
25
|
},
|
|
27
26
|
};
|
|
28
27
|
|
|
29
|
-
// --- Session (from
|
|
28
|
+
// --- Session (from SQLite, ADR-006/ADR-020) ---
|
|
29
|
+
const acc = accessor ?? (await getAccessor(projectRoot));
|
|
30
30
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
name: activeSession.name || activeSession.id,
|
|
41
|
-
status: activeSession.status,
|
|
42
|
-
startedAt: activeSession.startedAt,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
31
|
+
const sessions = await acc.loadSessions();
|
|
32
|
+
const activeSession = sessions.find((s) => s.status === 'active');
|
|
33
|
+
if (activeSession) {
|
|
34
|
+
brain.session = {
|
|
35
|
+
id: activeSession.id,
|
|
36
|
+
name: activeSession.name || activeSession.id,
|
|
37
|
+
status: activeSession.status,
|
|
38
|
+
startedAt: activeSession.startedAt,
|
|
39
|
+
};
|
|
45
40
|
}
|
|
46
41
|
} catch {
|
|
47
42
|
// skip
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
// --- Tasks & Progress ---
|
|
51
|
-
const acc = accessor ?? (await getAccessor(projectRoot));
|
|
52
46
|
const { tasks } = await acc.queryTasks({});
|
|
53
47
|
|
|
54
48
|
brain.progress = {
|
|
@@ -25,52 +25,72 @@ export interface SkillContent {
|
|
|
25
25
|
path: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
/** List available skills. */
|
|
29
|
-
export function listSkills(
|
|
30
|
-
const
|
|
28
|
+
/** List available skills from canonical and project-local directories. */
|
|
29
|
+
export function listSkills(projectRoot: string): { skills: SkillEntry[]; total: number } {
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
const allSkills: SkillEntry[] = [];
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Scan a skills directory and collect entries
|
|
34
|
+
function scanSkillsDir(dir: string): void {
|
|
35
|
+
if (!existsSync(dir)) return;
|
|
36
|
+
try {
|
|
37
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
38
|
+
for (const d of entries) {
|
|
39
|
+
if (!d.isDirectory() || d.name.startsWith('_') || seen.has(d.name)) continue;
|
|
40
|
+
seen.add(d.name);
|
|
41
|
+
|
|
42
|
+
const skillPath = join(dir, d.name, 'SKILL.md');
|
|
43
|
+
let description = '';
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const descMatch = content.match(/description:\s*[|>]?\s*\n?\s*(.+)/);
|
|
46
|
-
if (descMatch) {
|
|
47
|
-
description = descMatch[1]!.trim();
|
|
45
|
+
if (existsSync(skillPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(skillPath, 'utf-8');
|
|
48
|
+
const descMatch = content.match(/description:\s*[|>]?\s*\n?\s*(.+)/);
|
|
49
|
+
if (descMatch) {
|
|
50
|
+
description = descMatch[1]!.trim();
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
48
54
|
}
|
|
49
|
-
} catch {
|
|
50
|
-
// ignore
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
allSkills.push({
|
|
58
|
+
name: d.name,
|
|
59
|
+
path: join(dir, d.name),
|
|
60
|
+
hasSkillFile: existsSync(skillPath),
|
|
61
|
+
description,
|
|
62
|
+
});
|
|
52
63
|
}
|
|
64
|
+
} catch {
|
|
65
|
+
// ignore unreadable directories
|
|
66
|
+
}
|
|
67
|
+
}
|
|
53
68
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
path: join(skillsDir, d.name),
|
|
57
|
-
hasSkillFile: existsSync(skillPath),
|
|
58
|
-
description,
|
|
59
|
-
};
|
|
60
|
-
});
|
|
69
|
+
// 1. Scan project-local skills (higher priority, listed first)
|
|
70
|
+
scanSkillsDir(join(projectRoot, '.cleo', 'skills'));
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
// 2. Scan canonical (global) skills
|
|
73
|
+
scanSkillsDir(getCanonicalSkillsDir());
|
|
74
|
+
|
|
75
|
+
return { skills: allSkills, total: allSkills.length };
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
/** Read skill content for injection into agent context. */
|
|
66
|
-
export function getSkillContent(skillName: string,
|
|
78
|
+
/** Read skill content for injection into agent context. Checks project-local skills first. */
|
|
79
|
+
export function getSkillContent(skillName: string, projectRoot: string): SkillContent {
|
|
67
80
|
if (!skillName) {
|
|
68
81
|
throw new CleoError(ExitCode.INVALID_INPUT, 'skill name is required');
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
// Check project-local skills first, then canonical
|
|
85
|
+
const projectSkillDir = join(projectRoot, '.cleo', 'skills', skillName);
|
|
86
|
+
const canonicalSkillDir = join(getCanonicalSkillsDir(), skillName);
|
|
87
|
+
const skillDir = existsSync(projectSkillDir) ? projectSkillDir : canonicalSkillDir;
|
|
88
|
+
|
|
72
89
|
if (!existsSync(skillDir)) {
|
|
73
|
-
throw new CleoError(
|
|
90
|
+
throw new CleoError(
|
|
91
|
+
ExitCode.NOT_FOUND,
|
|
92
|
+
`Skill '${skillName}' not found at ${canonicalSkillDir} or ${projectSkillDir}`,
|
|
93
|
+
);
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
const skillFilePath = join(skillDir, 'SKILL.md');
|
package/src/otel/index.ts
CHANGED
|
@@ -125,14 +125,58 @@ export async function getOtelSpawns(opts: {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/** Get real token usage from Claude Code API. */
|
|
128
|
-
export async function getRealTokenUsage(
|
|
128
|
+
export async function getRealTokenUsage(opts: {
|
|
129
129
|
session?: string;
|
|
130
130
|
since?: string;
|
|
131
131
|
}): Promise<Record<string, unknown>> {
|
|
132
|
-
|
|
132
|
+
const otelEnabled = process.env.CLAUDE_CODE_ENABLE_TELEMETRY === '1';
|
|
133
|
+
|
|
134
|
+
// Read JSONL token data and filter by opts
|
|
135
|
+
const entries = readJsonlFile(getTokenFilePath());
|
|
136
|
+
|
|
137
|
+
if (entries.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
message: otelEnabled
|
|
140
|
+
? 'OTel enabled but no token data recorded yet'
|
|
141
|
+
: 'Real token usage requires OpenTelemetry configuration',
|
|
142
|
+
otelEnabled,
|
|
143
|
+
totalEvents: 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let filtered = entries;
|
|
148
|
+
|
|
149
|
+
// Filter by session if provided
|
|
150
|
+
if (opts.session) {
|
|
151
|
+
filtered = filtered.filter((e) => {
|
|
152
|
+
const ctx = (e.context ?? {}) as Record<string, unknown>;
|
|
153
|
+
return ctx.session_id === opts.session || e.session_id === opts.session;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Filter by since timestamp if provided
|
|
158
|
+
if (opts.since) {
|
|
159
|
+
const sinceDate = new Date(opts.since).getTime();
|
|
160
|
+
filtered = filtered.filter((e) => {
|
|
161
|
+
const ts = (e.timestamp ?? e.recorded_at) as string | undefined;
|
|
162
|
+
return ts ? new Date(ts).getTime() >= sinceDate : true;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const totalTokens = filtered.reduce((sum, e) => sum + ((e.estimated_tokens as number) ?? 0), 0);
|
|
167
|
+
const inputTokens = filtered.reduce((sum, e) => sum + ((e.input_tokens as number) ?? 0), 0);
|
|
168
|
+
const outputTokens = filtered.reduce((sum, e) => sum + ((e.output_tokens as number) ?? 0), 0);
|
|
169
|
+
|
|
133
170
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
otelEnabled,
|
|
172
|
+
totalEvents: filtered.length,
|
|
173
|
+
totalTokens,
|
|
174
|
+
inputTokens,
|
|
175
|
+
outputTokens,
|
|
176
|
+
filters: {
|
|
177
|
+
session: opts.session ?? null,
|
|
178
|
+
since: opts.since ?? null,
|
|
179
|
+
},
|
|
136
180
|
};
|
|
137
181
|
}
|
|
138
182
|
|
|
@@ -21,6 +21,12 @@ vi.mock('../handoff.js', () => ({
|
|
|
21
21
|
getLastHandoff: vi.fn().mockResolvedValue(null),
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
|
+
// Mock lifecycle pipeline — computePipelineStage dynamically imports this
|
|
25
|
+
let mockPipeline: unknown = null;
|
|
26
|
+
vi.mock('../../lifecycle/pipeline.js', () => ({
|
|
27
|
+
getPipeline: vi.fn().mockImplementation(() => Promise.resolve(mockPipeline)),
|
|
28
|
+
}));
|
|
29
|
+
|
|
24
30
|
import { getAccessor } from '../../store/data-accessor.js';
|
|
25
31
|
import { computeBriefing } from '../briefing.js';
|
|
26
32
|
|
|
@@ -337,8 +343,28 @@ describe('computeBriefing scope filtering', () => {
|
|
|
337
343
|
expect(briefing.currentTask!.title).toBe('Child 1');
|
|
338
344
|
});
|
|
339
345
|
|
|
340
|
-
it('pipelineStage is included when
|
|
341
|
-
|
|
346
|
+
it('pipelineStage is included when active lifecycle pipeline exists', async () => {
|
|
347
|
+
// Mock the lifecycle pipeline BEFORE calling computeBriefing
|
|
348
|
+
mockPipeline = { currentStage: 'implementation', status: 'active', isActive: true };
|
|
349
|
+
|
|
350
|
+
// Set up focus state pointing to a task that has a pipeline
|
|
351
|
+
const focus = { currentTask: 'T100', currentPhase: null };
|
|
352
|
+
const metaStore: Record<string, unknown> = { focus_state: focus };
|
|
353
|
+
const tasks = makeMockTasks();
|
|
354
|
+
const mockAccessor = {
|
|
355
|
+
loadSessions: vi.fn().mockResolvedValue([]),
|
|
356
|
+
queryTasks: vi.fn().mockResolvedValue({ tasks, total: tasks.length }),
|
|
357
|
+
getMetaValue: vi
|
|
358
|
+
.fn()
|
|
359
|
+
.mockImplementation((key: string) => Promise.resolve(metaStore[key] ?? null)),
|
|
360
|
+
setMetaValue: vi.fn().mockResolvedValue(undefined),
|
|
361
|
+
loadArchive: vi.fn().mockResolvedValue(null),
|
|
362
|
+
saveArchive: vi.fn().mockResolvedValue(undefined),
|
|
363
|
+
appendLog: vi.fn().mockResolvedValue(undefined),
|
|
364
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
365
|
+
engine: 'sqlite' as const,
|
|
366
|
+
};
|
|
367
|
+
(getAccessor as ReturnType<typeof vi.fn>).mockResolvedValue(mockAccessor);
|
|
342
368
|
|
|
343
369
|
const briefing = await computeBriefing('/fake/project', {
|
|
344
370
|
scope: 'global',
|
|
@@ -346,6 +372,9 @@ describe('computeBriefing scope filtering', () => {
|
|
|
346
372
|
|
|
347
373
|
expect(briefing.pipelineStage).toBeDefined();
|
|
348
374
|
expect(briefing.pipelineStage!.currentStage).toBe('implementation');
|
|
375
|
+
|
|
376
|
+
// Reset
|
|
377
|
+
mockPipeline = null;
|
|
349
378
|
});
|
|
350
379
|
|
|
351
380
|
it('blocked tasks include those with unresolved dependencies', async () => {
|