@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.
- package/dist/admin/export-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 +16443 -2160
- 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 +9 -0
- 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/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/export.d.ts +5 -4
- package/dist/store/export.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +12 -2
- 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 +2422 -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/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 +82 -1
- 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/discover.ts +1 -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/chain-schema.ts +1 -1
- package/src/store/export.ts +22 -12
- 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
package/src/nexus/discover.ts
CHANGED
|
@@ -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 () => {
|
package/src/sessions/briefing.ts
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
* @epic T4914
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type {
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
158
|
+
const currentTaskInfo = computeCurrentTask(focus, taskMap);
|
|
168
159
|
|
|
169
160
|
// 3. Next tasks (leverage-scored)
|
|
170
|
-
const nextTasks = computeNextTasks(tasks, taskMap,
|
|
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(
|
|
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:
|
|
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
|
-
|
|
319
|
+
focus: TaskWorkStateExt | undefined,
|
|
329
320
|
taskMap: Map<string, unknown>,
|
|
330
321
|
): CurrentTaskInfo | null {
|
|
331
|
-
const focusTaskId =
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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:
|
|
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
|
}
|
package/src/sessions/handoff.ts
CHANGED
|
@@ -15,12 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import { execFile } from 'node:child_process';
|
|
17
17
|
import { promisify } from 'node:util';
|
|
18
|
-
import type {
|
|
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
|
|
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,
|
|
101
|
-
openBlockers: findOpenBlockers(
|
|
102
|
-
openBugs: findOpenBugs(
|
|
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,
|
|
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,
|
|
114
|
+
const scopeTaskIds = getScopeTaskIds(session, tasks);
|
|
127
115
|
|
|
128
116
|
// Get uncompleted tasks in scope
|
|
129
|
-
const pendingTasks =
|
|
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
|
|
135
|
+
(priorityOrder[a.priority ?? 'medium'] ?? 99) - (priorityOrder[b.priority ?? 'medium'] ?? 99);
|
|
149
136
|
if (priorityDiff !== 0) return priorityDiff;
|
|
150
|
-
|
|
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(
|
|
163
|
-
const
|
|
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
|
-
|
|
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(
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
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
|
-
|
|
212
|
-
for (const t of current.tasks) {
|
|
178
|
+
for (const t of tasks) {
|
|
213
179
|
taskIds.add(t.id);
|
|
214
180
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
203
|
+
addDescendants(rootId);
|
|
237
204
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
package/src/sessions/index.ts
CHANGED
|
@@ -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 {
|
|
474
|
+
export type {
|
|
475
|
+
AssumptionRecord,
|
|
476
|
+
DecisionRecord,
|
|
477
|
+
TaskWorkStateExt,
|
|
478
|
+
} from './types.js';
|