@cleocode/core 2026.3.43 → 2026.3.44

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.
Files changed (161) hide show
  1. package/dist/admin/export-tasks.d.ts.map +1 -1
  2. package/dist/agents/agent-schema.d.ts +358 -0
  3. package/dist/agents/agent-schema.d.ts.map +1 -0
  4. package/dist/agents/capacity.d.ts +57 -0
  5. package/dist/agents/capacity.d.ts.map +1 -0
  6. package/dist/agents/index.d.ts +17 -0
  7. package/dist/agents/index.d.ts.map +1 -0
  8. package/dist/agents/registry.d.ts +115 -0
  9. package/dist/agents/registry.d.ts.map +1 -0
  10. package/dist/agents/retry.d.ts +83 -0
  11. package/dist/agents/retry.d.ts.map +1 -0
  12. package/dist/hooks/index.d.ts +4 -1
  13. package/dist/hooks/index.d.ts.map +1 -1
  14. package/dist/hooks/payload-schemas.d.ts +214 -0
  15. package/dist/hooks/payload-schemas.d.ts.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +16443 -2160
  19. package/dist/index.js.map +4 -4
  20. package/dist/inject/index.d.ts.map +1 -1
  21. package/dist/intelligence/impact.d.ts +51 -0
  22. package/dist/intelligence/impact.d.ts.map +1 -0
  23. package/dist/intelligence/index.d.ts +15 -0
  24. package/dist/intelligence/index.d.ts.map +1 -0
  25. package/dist/intelligence/patterns.d.ts +66 -0
  26. package/dist/intelligence/patterns.d.ts.map +1 -0
  27. package/dist/intelligence/prediction.d.ts +51 -0
  28. package/dist/intelligence/prediction.d.ts.map +1 -0
  29. package/dist/intelligence/types.d.ts +221 -0
  30. package/dist/intelligence/types.d.ts.map +1 -0
  31. package/dist/internal.d.ts +9 -0
  32. package/dist/internal.d.ts.map +1 -1
  33. package/dist/issue/template-parser.d.ts +8 -2
  34. package/dist/issue/template-parser.d.ts.map +1 -1
  35. package/dist/lifecycle/pipeline.d.ts +2 -2
  36. package/dist/lifecycle/pipeline.d.ts.map +1 -1
  37. package/dist/lifecycle/state-machine.d.ts +1 -1
  38. package/dist/lifecycle/state-machine.d.ts.map +1 -1
  39. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  40. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  41. package/dist/memory/brain-row-types.d.ts +40 -6
  42. package/dist/memory/brain-row-types.d.ts.map +1 -1
  43. package/dist/memory/brain-search.d.ts.map +1 -1
  44. package/dist/memory/brain-similarity.d.ts.map +1 -1
  45. package/dist/memory/claude-mem-migration.d.ts.map +1 -1
  46. package/dist/nexus/discover.d.ts.map +1 -1
  47. package/dist/orchestration/bootstrap.d.ts.map +1 -1
  48. package/dist/orchestration/skill-ops.d.ts +4 -4
  49. package/dist/orchestration/skill-ops.d.ts.map +1 -1
  50. package/dist/otel/index.d.ts +1 -1
  51. package/dist/otel/index.d.ts.map +1 -1
  52. package/dist/sessions/briefing.d.ts.map +1 -1
  53. package/dist/sessions/handoff.d.ts.map +1 -1
  54. package/dist/sessions/index.d.ts +1 -1
  55. package/dist/sessions/index.d.ts.map +1 -1
  56. package/dist/sessions/types.d.ts +8 -42
  57. package/dist/sessions/types.d.ts.map +1 -1
  58. package/dist/signaldock/signaldock-transport.d.ts +1 -1
  59. package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
  60. package/dist/skills/injection/subagent.d.ts +3 -3
  61. package/dist/skills/injection/subagent.d.ts.map +1 -1
  62. package/dist/skills/manifests/contribution.d.ts +2 -2
  63. package/dist/skills/manifests/contribution.d.ts.map +1 -1
  64. package/dist/skills/orchestrator/spawn.d.ts +6 -6
  65. package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
  66. package/dist/skills/orchestrator/startup.d.ts +1 -1
  67. package/dist/skills/orchestrator/startup.d.ts.map +1 -1
  68. package/dist/skills/orchestrator/validator.d.ts +2 -2
  69. package/dist/skills/orchestrator/validator.d.ts.map +1 -1
  70. package/dist/skills/precedence-types.d.ts +24 -1
  71. package/dist/skills/precedence-types.d.ts.map +1 -1
  72. package/dist/skills/types.d.ts +70 -4
  73. package/dist/skills/types.d.ts.map +1 -1
  74. package/dist/store/export.d.ts +5 -4
  75. package/dist/store/export.d.ts.map +1 -1
  76. package/dist/store/tasks-schema.d.ts +12 -2
  77. package/dist/store/tasks-schema.d.ts.map +1 -1
  78. package/dist/store/typed-query.d.ts +12 -0
  79. package/dist/store/typed-query.d.ts.map +1 -0
  80. package/dist/store/validation-schemas.d.ts +2422 -50
  81. package/dist/store/validation-schemas.d.ts.map +1 -1
  82. package/dist/system/inject-generate.d.ts.map +1 -1
  83. package/dist/validation/doctor/checks.d.ts +5 -0
  84. package/dist/validation/doctor/checks.d.ts.map +1 -1
  85. package/dist/validation/engine.d.ts +10 -10
  86. package/dist/validation/engine.d.ts.map +1 -1
  87. package/dist/validation/index.d.ts +6 -2
  88. package/dist/validation/index.d.ts.map +1 -1
  89. package/dist/validation/protocol-common.d.ts +10 -2
  90. package/dist/validation/protocol-common.d.ts.map +1 -1
  91. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
  92. package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
  93. package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
  94. package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
  95. package/package.json +2 -2
  96. package/src/admin/export-tasks.ts +2 -5
  97. package/src/agents/__tests__/capacity.test.ts +219 -0
  98. package/src/agents/__tests__/registry.test.ts +457 -0
  99. package/src/agents/__tests__/retry.test.ts +289 -0
  100. package/src/agents/agent-schema.ts +107 -0
  101. package/src/agents/capacity.ts +151 -0
  102. package/src/agents/index.ts +68 -0
  103. package/src/agents/registry.ts +449 -0
  104. package/src/agents/retry.ts +255 -0
  105. package/src/hooks/index.ts +20 -1
  106. package/src/hooks/payload-schemas.ts +199 -0
  107. package/src/index.ts +69 -0
  108. package/src/inject/index.ts +14 -14
  109. package/src/intelligence/__tests__/impact.test.ts +453 -0
  110. package/src/intelligence/__tests__/patterns.test.ts +450 -0
  111. package/src/intelligence/__tests__/prediction.test.ts +418 -0
  112. package/src/intelligence/impact.ts +638 -0
  113. package/src/intelligence/index.ts +47 -0
  114. package/src/intelligence/patterns.ts +621 -0
  115. package/src/intelligence/prediction.ts +621 -0
  116. package/src/intelligence/types.ts +273 -0
  117. package/src/internal.ts +82 -1
  118. package/src/issue/template-parser.ts +65 -4
  119. package/src/lifecycle/pipeline.ts +14 -7
  120. package/src/lifecycle/state-machine.ts +6 -2
  121. package/src/memory/brain-lifecycle.ts +5 -11
  122. package/src/memory/brain-retrieval.ts +44 -38
  123. package/src/memory/brain-row-types.ts +43 -6
  124. package/src/memory/brain-search.ts +53 -32
  125. package/src/memory/brain-similarity.ts +9 -8
  126. package/src/memory/claude-mem-migration.ts +4 -3
  127. package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
  128. package/src/nexus/discover.ts +1 -0
  129. package/src/orchestration/bootstrap.ts +11 -17
  130. package/src/orchestration/skill-ops.ts +52 -32
  131. package/src/otel/index.ts +48 -4
  132. package/src/sessions/__tests__/briefing.test.ts +31 -2
  133. package/src/sessions/briefing.ts +27 -42
  134. package/src/sessions/handoff.ts +52 -86
  135. package/src/sessions/index.ts +5 -1
  136. package/src/sessions/types.ts +9 -43
  137. package/src/signaldock/signaldock-transport.ts +5 -2
  138. package/src/skills/injection/subagent.ts +10 -16
  139. package/src/skills/manifests/contribution.ts +5 -13
  140. package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
  141. package/src/skills/orchestrator/spawn.ts +18 -31
  142. package/src/skills/orchestrator/startup.ts +78 -65
  143. package/src/skills/orchestrator/validator.ts +26 -31
  144. package/src/skills/precedence-types.ts +24 -1
  145. package/src/skills/types.ts +72 -5
  146. package/src/store/__tests__/test-db-helper.d.ts +4 -4
  147. package/src/store/__tests__/test-db-helper.js +5 -16
  148. package/src/store/__tests__/test-db-helper.ts +5 -18
  149. package/src/store/chain-schema.ts +1 -1
  150. package/src/store/export.ts +22 -12
  151. package/src/store/tasks-schema.ts +65 -8
  152. package/src/store/typed-query.ts +17 -0
  153. package/src/store/validation-schemas.ts +347 -23
  154. package/src/system/inject-generate.ts +9 -23
  155. package/src/validation/doctor/checks.ts +24 -2
  156. package/src/validation/engine.ts +11 -11
  157. package/src/validation/index.ts +131 -3
  158. package/src/validation/protocol-common.ts +54 -3
  159. package/dist/tasks/reparent.d.ts +0 -38
  160. package/dist/tasks/reparent.d.ts.map +0 -1
  161. package/src/tasks/reparent.ts +0 -134
@@ -104,6 +104,7 @@ const STOP_WORDS = new Set([
104
104
  */
105
105
  export function extractKeywords(text: string): string[] {
106
106
  return text
107
+ .toLowerCase()
107
108
  .replace(/[^a-z0-9\s-]/g, ' ')
108
109
  .split(/\s+/)
109
110
  .filter((w) => w.length > 2 && !STOP_WORDS.has(w));
@@ -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 sessions.json) ---
28
+ // --- Session (from SQLite, ADR-006/ADR-020) ---
29
+ const acc = accessor ?? (await getAccessor(projectRoot));
30
30
  try {
31
- const sessionsPath = getSessionsPath(projectRoot);
32
- if (existsSync(sessionsPath)) {
33
- const sessionsData = JSON.parse(readFileSync(sessionsPath, 'utf-8'));
34
- const activeSession = (sessionsData.sessions ?? []).find(
35
- (s: { status: string }) => s.status === 'active',
36
- );
37
- if (activeSession) {
38
- brain.session = {
39
- id: activeSession.id,
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(_projectRoot: string): { skills: SkillEntry[]; total: number } {
30
- const skillsDir = getCanonicalSkillsDir();
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
- if (!existsSync(skillsDir)) {
33
- return { skills: [], total: 0 };
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
- const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
37
- .filter((d) => d.isDirectory() && !d.name.startsWith('_'))
38
- .map((d) => {
39
- const skillPath = join(skillsDir, d.name, 'SKILL.md');
40
- let description = '';
41
-
42
- if (existsSync(skillPath)) {
43
- try {
44
- const content = readFileSync(skillPath, 'utf-8');
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
- return {
55
- name: d.name,
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
- return { skills: skillDirs, total: skillDirs.length };
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, _projectRoot: string): SkillContent {
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
- const skillDir = join(getCanonicalSkillsDir(), skillName);
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(ExitCode.NOT_FOUND, `Skill '${skillName}' not found at ${skillDir}`);
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(_opts: {
128
+ export async function getRealTokenUsage(opts: {
129
129
  session?: string;
130
130
  since?: string;
131
131
  }): Promise<Record<string, unknown>> {
132
- // Real token usage requires OTel integration - return placeholder
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
- message: 'Real token usage requires OpenTelemetry configuration',
135
- otelEnabled: process.env.CLAUDE_CODE_ENABLE_TELEMETRY === '1',
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 metadata has lifecycle state', async () => {
341
- setupMockAccessor(makeMockTasks(), { lifecycleState: 'implementation' });
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 () => {
@@ -17,13 +17,13 @@
17
17
  * @epic T4914
18
18
  */
19
19
 
20
- import type { FileMeta, TaskWorkState } from '@cleocode/contracts';
20
+ import type { Task, TaskWorkState } from '@cleocode/contracts';
21
21
  import type { SessionMemoryContext } from '../memory/session-memory.js';
22
22
  import type { DataAccessor } from '../store/data-accessor.js';
23
23
  import { getAccessor } from '../store/data-accessor.js';
24
24
  import { depsReady } from '../tasks/deps-ready.js';
25
25
  import { getLastHandoff, type HandoffData } from './handoff.js';
26
- import type { TaskFileExt } from './types.js';
26
+ import type { TaskWorkStateExt } from './types.js';
27
27
 
28
28
  /**
29
29
  * Task summary for briefing output.
@@ -138,15 +138,9 @@ export async function computeBriefing(
138
138
  ): Promise<SessionBriefing> {
139
139
  const accessor = await getAccessor(projectRoot);
140
140
  const { tasks } = await accessor.queryTasks({});
141
- const focus = await accessor.getMetaValue<TaskWorkState>('focus_state');
142
- const fileMeta = await accessor.getMetaValue<FileMeta>('file_meta');
143
-
144
- // Build a TaskFileExt-compatible shape from targeted queries
145
- const current = {
146
- tasks,
147
- focus: focus ?? undefined,
148
- _meta: fileMeta ?? undefined,
149
- } as unknown as TaskFileExt;
141
+ const focus = (await accessor.getMetaValue<TaskWorkState>('focus_state')) as
142
+ | TaskWorkStateExt
143
+ | undefined;
150
144
 
151
145
  // Build task map for quick lookups
152
146
  const taskMap = new Map(tasks.map((t) => [t.id, t]));
@@ -155,19 +149,16 @@ export async function computeBriefing(
155
149
  const scopeFilter = await parseScope(options.scope, accessor);
156
150
 
157
151
  // Compute in-scope task IDs (undefined = all tasks in scope)
158
- const scopeTaskIds = getScopeTaskIdSet(
159
- scopeFilter,
160
- tasks as unknown as Array<{ id: string; parentId?: string; [key: string]: unknown }>,
161
- );
152
+ const scopeTaskIds = getScopeTaskIdSet(scopeFilter, tasks);
162
153
 
163
154
  // 1. Last session handoff
164
155
  const lastSession = await computeLastSession(projectRoot, scopeFilter);
165
156
 
166
157
  // 2. Current active task
167
- const currentTaskInfo = computeCurrentTask(current, taskMap);
158
+ const currentTaskInfo = computeCurrentTask(focus, taskMap);
168
159
 
169
160
  // 3. Next tasks (leverage-scored)
170
- const nextTasks = computeNextTasks(tasks, taskMap, current, {
161
+ const nextTasks = computeNextTasks(tasks, taskMap, focus, {
171
162
  maxTasks: options.maxNextTasks ?? 5,
172
163
  scopeTaskIds,
173
164
  });
@@ -191,7 +182,7 @@ export async function computeBriefing(
191
182
  });
192
183
 
193
184
  // 7. Pipeline stage (optional - may not be available)
194
- const pipelineStage = computePipelineStage(current);
185
+ const pipelineStage = await computePipelineStage(focus);
195
186
 
196
187
  // 8. Brain memory context (optional, best-effort)
197
188
  let memoryContext: SessionMemoryContext | undefined;
@@ -259,7 +250,7 @@ async function parseScope(
259
250
  */
260
251
  function getScopeTaskIdSet(
261
252
  scopeFilter: { type: 'global' | 'epic'; epicId?: string } | undefined,
262
- tasks: Array<{ id: string; parentId?: string; [key: string]: unknown }>,
253
+ tasks: Task[],
263
254
  ): Set<string> | undefined {
264
255
  if (!scopeFilter || scopeFilter.type === 'global') {
265
256
  return undefined; // All tasks in scope
@@ -325,10 +316,10 @@ async function computeLastSession(
325
316
  * Compute current active task from task file.
326
317
  */
327
318
  function computeCurrentTask(
328
- current: TaskFileExt,
319
+ focus: TaskWorkStateExt | undefined,
329
320
  taskMap: Map<string, unknown>,
330
321
  ): CurrentTaskInfo | null {
331
- const focusTaskId = current.focus?.currentTask;
322
+ const focusTaskId = focus?.currentTask;
332
323
  if (!focusTaskId) return null;
333
324
 
334
325
  const task = taskMap.get(focusTaskId) as
@@ -376,7 +367,7 @@ function calculateLeverage(taskId: string, taskMap: Map<string, unknown>): numbe
376
367
  function computeNextTasks(
377
368
  tasks: unknown[],
378
369
  taskMap: Map<string, unknown>,
379
- current: TaskFileExt,
370
+ focus: TaskWorkStateExt | undefined,
380
371
  options: { maxTasks: number; scopeTaskIds?: Set<string> },
381
372
  ): BriefingTask[] {
382
373
  const pendingTasks = tasks.filter((t) => {
@@ -387,7 +378,7 @@ function computeNextTasks(
387
378
  });
388
379
 
389
380
  const scored: BriefingTask[] = [];
390
- const currentPhase = current.focus?.currentPhase;
381
+ const currentPhase = focus?.currentPhase;
391
382
 
392
383
  for (const task of pendingTasks) {
393
384
  const t = task as {
@@ -593,28 +584,22 @@ function calculateEpicCompletion(epicId: string, taskMap: Map<string, unknown>):
593
584
  /**
594
585
  * Compute pipeline stage info from task file metadata.
595
586
  */
596
- function computePipelineStage(current: TaskFileExt): PipelineStageInfo | undefined {
597
- // Try to get from _meta or focus
598
- const stage = (current._meta as Record<string, unknown>)?.pipelineStage as string | undefined;
599
- const stageStatus = (current._meta as Record<string, unknown>)?.pipelineStageStatus as
600
- | string
601
- | undefined;
587
+ async function computePipelineStage(
588
+ focus: TaskWorkStateExt | undefined,
589
+ ): Promise<PipelineStageInfo | undefined> {
590
+ const taskId = focus?.currentTask;
591
+ if (!taskId) return undefined;
602
592
 
603
- if (stage) {
604
- return {
605
- currentStage: stage,
606
- stageStatus: stageStatus || 'active',
607
- };
608
- }
593
+ try {
594
+ const { getPipeline } = await import('../lifecycle/pipeline.js');
595
+ const pipeline = await getPipeline(taskId);
596
+ if (!pipeline) return undefined;
609
597
 
610
- // Try from lifecycle state if available
611
- const lifecycleState = current._meta?.lifecycleState as string | undefined;
612
- if (lifecycleState) {
613
598
  return {
614
- currentStage: lifecycleState,
615
- stageStatus: 'active',
599
+ currentStage: pipeline.currentStage,
600
+ stageStatus: pipeline.isActive ? 'active' : (pipeline.status ?? 'completed'),
616
601
  };
602
+ } catch {
603
+ return undefined;
617
604
  }
618
-
619
- return undefined;
620
605
  }
@@ -15,12 +15,11 @@
15
15
 
16
16
  import { execFile } from 'node:child_process';
17
17
  import { promisify } from 'node:util';
18
- import type { FileMeta, Session, TaskWorkState } from '@cleocode/contracts';
18
+ import type { Session, Task } from '@cleocode/contracts';
19
19
  import { ExitCode } from '@cleocode/contracts';
20
20
  import { CleoError } from '../errors.js';
21
21
  import { getAccessor } from '../store/data-accessor.js';
22
22
  import { getDecisionLog } from './decisions.js';
23
- import type { TaskFileExt } from './types.js';
24
23
 
25
24
  const execFileAsync = promisify(execFile);
26
25
 
@@ -77,15 +76,8 @@ export async function computeHandoff(
77
76
  throw new CleoError(ExitCode.SESSION_NOT_FOUND, `Session '${options.sessionId}' not found`);
78
77
  }
79
78
 
80
- // Load task data for scope analysis
79
+ // Load tasks directly from SQLite via DataAccessor
81
80
  const { tasks } = await accessor.queryTasks({});
82
- const focus = await accessor.getMetaValue<TaskWorkState>('focus_state');
83
- const fileMeta = await accessor.getMetaValue<FileMeta>('file_meta');
84
- const current = {
85
- tasks,
86
- focus: focus ?? undefined,
87
- _meta: fileMeta ?? undefined,
88
- } as unknown as TaskFileExt;
89
81
 
90
82
  // Get decisions recorded during this session
91
83
  const decisions = await getDecisionLog(projectRoot, { sessionId: options.sessionId });
@@ -97,9 +89,9 @@ export async function computeHandoff(
97
89
  tasksCompleted: session.tasksCompleted ?? [],
98
90
  tasksCreated: session.tasksCreated ?? [],
99
91
  decisionsRecorded: decisions.length,
100
- nextSuggested: computeNextSuggested(session, current),
101
- openBlockers: findOpenBlockers(current, session),
102
- openBugs: findOpenBugs(current, session),
92
+ nextSuggested: computeNextSuggested(session, tasks),
93
+ openBlockers: findOpenBlockers(tasks, session),
94
+ openBugs: findOpenBugs(tasks, session),
103
95
  };
104
96
 
105
97
  // Apply human overrides
@@ -117,20 +109,15 @@ export async function computeHandoff(
117
109
  * Compute top-3 next suggested tasks.
118
110
  * Prioritizes uncompleted tasks within the session scope.
119
111
  */
120
- function computeNextSuggested(session: Session, current: TaskFileExt): string[] {
121
- const suggestions: string[] = [];
122
-
123
- if (!current.tasks) return suggestions;
124
-
112
+ function computeNextSuggested(session: Session, tasks: Task[]): string[] {
125
113
  // Filter to tasks in scope
126
- const scopeTaskIds = getScopeTaskIds(session, current);
114
+ const scopeTaskIds = getScopeTaskIds(session, tasks);
127
115
 
128
116
  // Get uncompleted tasks in scope
129
- const pendingTasks = current.tasks.filter(
117
+ const pendingTasks = tasks.filter(
130
118
  (t) =>
131
119
  scopeTaskIds.has(t.id) &&
132
120
  t.status !== 'done' &&
133
- t.status !== 'completed' &&
134
121
  t.status !== 'archived' &&
135
122
  t.status !== 'cancelled',
136
123
  );
@@ -145,11 +132,9 @@ function computeNextSuggested(session: Session, current: TaskFileExt): string[]
145
132
 
146
133
  pendingTasks.sort((a, b) => {
147
134
  const priorityDiff =
148
- (priorityOrder[a.priority as string] ?? 99) - (priorityOrder[b.priority as string] ?? 99);
135
+ (priorityOrder[a.priority ?? 'medium'] ?? 99) - (priorityOrder[b.priority ?? 'medium'] ?? 99);
149
136
  if (priorityDiff !== 0) return priorityDiff;
150
- const aCreated = typeof a.createdAt === 'string' ? a.createdAt : '1970-01-01T00:00:00Z';
151
- const bCreated = typeof b.createdAt === 'string' ? b.createdAt : '1970-01-01T00:00:00Z';
152
- return new Date(aCreated).getTime() - new Date(bCreated).getTime();
137
+ return (a.createdAt ?? '').localeCompare(b.createdAt ?? '');
153
138
  });
154
139
 
155
140
  // Take top 3
@@ -159,87 +144,68 @@ function computeNextSuggested(session: Session, current: TaskFileExt): string[]
159
144
  /**
160
145
  * Find tasks with blockers in the session scope.
161
146
  */
162
- function findOpenBlockers(current: TaskFileExt, session: Session): string[] {
163
- const blockers: string[] = [];
164
-
165
- if (!current.tasks) return blockers;
166
-
167
- const scopeTaskIds = getScopeTaskIds(session, current);
147
+ function findOpenBlockers(tasks: Task[], session: Session): string[] {
148
+ const scopeTaskIds = getScopeTaskIds(session, tasks);
168
149
 
169
- // Find blocked tasks in scope
170
- const blockedTasks = current.tasks.filter(
171
- (t) => scopeTaskIds.has(t.id) && t.status === 'blocked',
172
- );
173
-
174
- return blockedTasks.map((t) => t.id);
150
+ return tasks.filter((t) => scopeTaskIds.has(t.id) && t.status === 'blocked').map((t) => t.id);
175
151
  }
176
152
 
177
153
  /**
178
154
  * Find open bugs in the session scope.
179
155
  */
180
- function findOpenBugs(current: TaskFileExt, session: Session): string[] {
181
- const bugs: string[] = [];
182
-
183
- if (!current.tasks) return bugs;
184
-
185
- const scopeTaskIds = getScopeTaskIds(session, current);
186
-
187
- // Find bug-type tasks that aren't closed
188
- const bugTasks = current.tasks.filter(
189
- (t) =>
190
- scopeTaskIds.has(t.id) &&
191
- (t.type === 'bug' ||
192
- (Array.isArray(t.labels) && t.labels.some((l: string) => l === 'bug'))) &&
193
- t.status !== 'done' &&
194
- t.status !== 'completed' &&
195
- t.status !== 'archived' &&
196
- t.status !== 'cancelled',
197
- );
198
-
199
- return bugTasks.map((t) => t.id);
156
+ function findOpenBugs(tasks: Task[], session: Session): string[] {
157
+ const scopeTaskIds = getScopeTaskIds(session, tasks);
158
+
159
+ return tasks
160
+ .filter(
161
+ (t) =>
162
+ scopeTaskIds.has(t.id) &&
163
+ (t.labels ?? []).includes('bug') &&
164
+ t.status !== 'done' &&
165
+ t.status !== 'archived' &&
166
+ t.status !== 'cancelled',
167
+ )
168
+ .map((t) => t.id);
200
169
  }
201
170
 
202
171
  /**
203
172
  * Get set of task IDs within the session scope.
204
173
  */
205
- function getScopeTaskIds(session: Session, current: TaskFileExt): Set<string> {
174
+ function getScopeTaskIds(session: Session, tasks: Task[]): Set<string> {
206
175
  const taskIds = new Set<string>();
207
176
 
208
- if (!current.tasks) return taskIds;
209
-
210
177
  if (session.scope.type === 'global') {
211
- // Global scope: all tasks
212
- for (const t of current.tasks) {
178
+ for (const t of tasks) {
213
179
  taskIds.add(t.id);
214
180
  }
215
- } else {
216
- // Epic/task scope: root task and descendants
217
- // Prefer rootTaskId (engine-layer), fall back to epicId (core-layer)
218
- const rootId = session.scope.rootTaskId ?? session.scope.epicId;
219
- if (!rootId) {
220
- // No root ID, fall back to global
221
- for (const t of current.tasks) {
222
- taskIds.add(t.id);
223
- }
224
- return taskIds;
181
+ return taskIds;
182
+ }
183
+
184
+ // Epic/task scope: root task and descendants
185
+ const rootId = session.scope.rootTaskId ?? session.scope.epicId;
186
+ if (!rootId) {
187
+ // No root ID, fall back to global
188
+ for (const t of tasks) {
189
+ taskIds.add(t.id);
225
190
  }
191
+ return taskIds;
192
+ }
226
193
 
227
- const addDescendants = (taskId: string) => {
228
- taskIds.add(taskId);
229
- current.tasks?.forEach((t) => {
230
- if (t.parentId === taskId) {
231
- addDescendants(t.id);
232
- }
233
- });
234
- };
194
+ const addDescendants = (taskId: string) => {
195
+ taskIds.add(taskId);
196
+ for (const t of tasks) {
197
+ if (t.parentId === taskId) {
198
+ addDescendants(t.id);
199
+ }
200
+ }
201
+ };
235
202
 
236
- addDescendants(rootId);
203
+ addDescendants(rootId);
237
204
 
238
- // Include explicitTaskIds if present in scope
239
- if (session.scope.explicitTaskIds) {
240
- for (const id of session.scope.explicitTaskIds) {
241
- taskIds.add(id);
242
- }
205
+ // Include explicitTaskIds if present in scope
206
+ if (session.scope.explicitTaskIds) {
207
+ for (const id of session.scope.explicitTaskIds) {
208
+ taskIds.add(id);
243
209
  }
244
210
  }
245
211
 
@@ -471,4 +471,8 @@ export { getSessionStats } from './session-stats.js';
471
471
  export { suspendSession } from './session-suspend.js';
472
472
  export { switchSession } from './session-switch.js';
473
473
  export { SessionView } from './session-view.js';
474
- export type { AssumptionRecord, DecisionRecord, TaskFileExt, TaskWorkStateExt } from './types.js';
474
+ export type {
475
+ AssumptionRecord,
476
+ DecisionRecord,
477
+ TaskWorkStateExt,
478
+ } from './types.js';