@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
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Extraction module for the CLEO Intelligence dimension.
|
|
3
|
+
*
|
|
4
|
+
* Provides automatic pattern detection from brain_observations and task history,
|
|
5
|
+
* pattern matching against brain_patterns, and pattern storage/stat updates.
|
|
6
|
+
*
|
|
7
|
+
* Uses the existing brain_patterns and brain_learnings tables — no new tables.
|
|
8
|
+
*
|
|
9
|
+
* @task Wave3A
|
|
10
|
+
* @epic T5149
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { randomBytes } from 'node:crypto';
|
|
14
|
+
import type { BrainDataAccessor } from '../store/brain-accessor.js';
|
|
15
|
+
import type { BrainPatternRow } from '../store/brain-schema.js';
|
|
16
|
+
import type { DataAccessor } from '../store/data-accessor.js';
|
|
17
|
+
import type {
|
|
18
|
+
DetectedPattern,
|
|
19
|
+
PatternExtractionOptions,
|
|
20
|
+
PatternMatch,
|
|
21
|
+
PatternStatsUpdate,
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Pattern Extraction
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Analyze brain_observations and task history to find recurring patterns.
|
|
30
|
+
*
|
|
31
|
+
* Detects:
|
|
32
|
+
* - Workflow patterns: common task sequences that succeed/fail
|
|
33
|
+
* - Blocker patterns: what commonly blocks tasks
|
|
34
|
+
* - Success patterns: what correlates with successful task completion
|
|
35
|
+
* - Time patterns: recurring label/type distributions by task status
|
|
36
|
+
*
|
|
37
|
+
* @param taskAccessor - DataAccessor for tasks.db
|
|
38
|
+
* @param brainAccessor - BrainDataAccessor for brain.db
|
|
39
|
+
* @param options - Extraction options (min frequency, confidence, limit)
|
|
40
|
+
* @returns Array of detected patterns sorted by frequency descending
|
|
41
|
+
*/
|
|
42
|
+
export async function extractPatternsFromHistory(
|
|
43
|
+
taskAccessor: DataAccessor,
|
|
44
|
+
brainAccessor: BrainDataAccessor,
|
|
45
|
+
options?: PatternExtractionOptions,
|
|
46
|
+
): Promise<DetectedPattern[]> {
|
|
47
|
+
const minFrequency = options?.minFrequency ?? 2;
|
|
48
|
+
const minConfidence = options?.minConfidence ?? 0.3;
|
|
49
|
+
const limit = options?.limit ?? 50;
|
|
50
|
+
|
|
51
|
+
const patterns: DetectedPattern[] = [];
|
|
52
|
+
|
|
53
|
+
// Extract blocker patterns from blocked tasks
|
|
54
|
+
const blockerPatterns = await extractBlockerPatterns(taskAccessor, minFrequency);
|
|
55
|
+
patterns.push(...blockerPatterns);
|
|
56
|
+
|
|
57
|
+
// Extract success patterns from completed tasks
|
|
58
|
+
const successPatterns = await extractSuccessPatterns(taskAccessor, minFrequency);
|
|
59
|
+
patterns.push(...successPatterns);
|
|
60
|
+
|
|
61
|
+
// Extract workflow patterns from task label co-occurrence
|
|
62
|
+
const workflowPatterns = await extractWorkflowPatterns(taskAccessor, minFrequency);
|
|
63
|
+
patterns.push(...workflowPatterns);
|
|
64
|
+
|
|
65
|
+
// Extract patterns from existing brain_observations
|
|
66
|
+
const observationPatterns = await extractObservationPatterns(brainAccessor, minFrequency);
|
|
67
|
+
patterns.push(...observationPatterns);
|
|
68
|
+
|
|
69
|
+
// Filter by type if specified
|
|
70
|
+
let filtered = options?.type ? patterns.filter((p) => p.type === options.type) : patterns;
|
|
71
|
+
|
|
72
|
+
// Filter by minimum confidence
|
|
73
|
+
filtered = filtered.filter((p) => p.confidence >= minConfidence);
|
|
74
|
+
|
|
75
|
+
// Sort by frequency descending, then by confidence
|
|
76
|
+
filtered.sort((a, b) => b.frequency - a.frequency || b.confidence - a.confidence);
|
|
77
|
+
|
|
78
|
+
return filtered.slice(0, limit);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Pattern Matching
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Find which known patterns from brain_patterns apply to a given task.
|
|
87
|
+
*
|
|
88
|
+
* Compares task attributes (labels, title, description, type, size, status)
|
|
89
|
+
* against stored patterns and returns matches with relevance scores.
|
|
90
|
+
*
|
|
91
|
+
* @param taskId - The task to match patterns against
|
|
92
|
+
* @param taskAccessor - DataAccessor for tasks.db
|
|
93
|
+
* @param brainAccessor - BrainDataAccessor for brain.db
|
|
94
|
+
* @returns Array of pattern matches sorted by relevance descending
|
|
95
|
+
*/
|
|
96
|
+
export async function matchPatterns(
|
|
97
|
+
taskId: string,
|
|
98
|
+
taskAccessor: DataAccessor,
|
|
99
|
+
brainAccessor: BrainDataAccessor,
|
|
100
|
+
): Promise<PatternMatch[]> {
|
|
101
|
+
const task = await taskAccessor.loadSingleTask(taskId);
|
|
102
|
+
if (!task) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const allPatterns = await brainAccessor.findPatterns({ limit: 200 });
|
|
107
|
+
const matches: PatternMatch[] = [];
|
|
108
|
+
|
|
109
|
+
const taskLabels = new Set((task.labels ?? []).map((l) => l.toLowerCase()));
|
|
110
|
+
const taskTitle = task.title.toLowerCase();
|
|
111
|
+
const taskDesc = (task.description ?? '').toLowerCase();
|
|
112
|
+
const taskType = task.type?.toLowerCase() ?? '';
|
|
113
|
+
|
|
114
|
+
for (const pattern of allPatterns) {
|
|
115
|
+
const score = computePatternRelevance(pattern, taskLabels, taskTitle, taskDesc, taskType);
|
|
116
|
+
|
|
117
|
+
if (score > 0) {
|
|
118
|
+
const matchReason = buildMatchReason(pattern, taskLabels, taskTitle, taskDesc);
|
|
119
|
+
|
|
120
|
+
matches.push({
|
|
121
|
+
pattern,
|
|
122
|
+
relevanceScore: Math.round(score * 1000) / 1000,
|
|
123
|
+
matchReason,
|
|
124
|
+
isAntiPattern: pattern.antiPattern !== null && pattern.antiPattern.length > 0,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Sort by relevance descending
|
|
130
|
+
matches.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
131
|
+
|
|
132
|
+
return matches;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Pattern Storage
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Save a detected pattern to the brain_patterns table.
|
|
141
|
+
*
|
|
142
|
+
* Uses the existing brain_patterns schema: type, pattern, context, frequency,
|
|
143
|
+
* success_rate, impact, anti_pattern, mitigation, examples_json.
|
|
144
|
+
*
|
|
145
|
+
* @param detected - The pattern to store
|
|
146
|
+
* @param brainAccessor - BrainDataAccessor for brain.db
|
|
147
|
+
* @returns The stored pattern row
|
|
148
|
+
*/
|
|
149
|
+
export async function storeDetectedPattern(
|
|
150
|
+
detected: DetectedPattern,
|
|
151
|
+
brainAccessor: BrainDataAccessor,
|
|
152
|
+
): Promise<BrainPatternRow> {
|
|
153
|
+
const id = `P-${randomBytes(4).toString('hex')}`;
|
|
154
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
155
|
+
|
|
156
|
+
return brainAccessor.addPattern({
|
|
157
|
+
id,
|
|
158
|
+
type: detected.type,
|
|
159
|
+
pattern: detected.pattern,
|
|
160
|
+
context: detected.context,
|
|
161
|
+
frequency: detected.frequency,
|
|
162
|
+
successRate: detected.successRate,
|
|
163
|
+
impact: detected.impact,
|
|
164
|
+
antiPattern: detected.antiPattern,
|
|
165
|
+
mitigation: detected.mitigation,
|
|
166
|
+
examplesJson: JSON.stringify(detected.examples),
|
|
167
|
+
extractedAt: now,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update the frequency and success_rate of an existing pattern after an outcome.
|
|
173
|
+
*
|
|
174
|
+
* Increments frequency by 1 and recalculates success_rate using the running
|
|
175
|
+
* average formula: newRate = (oldRate * oldFreq + (success ? 1 : 0)) / newFreq.
|
|
176
|
+
*
|
|
177
|
+
* @param patternId - The brain_patterns ID to update
|
|
178
|
+
* @param outcome - Whether the outcome was successful
|
|
179
|
+
* @param brainAccessor - BrainDataAccessor for brain.db
|
|
180
|
+
* @returns The update result or null if the pattern was not found
|
|
181
|
+
*/
|
|
182
|
+
export async function updatePatternStats(
|
|
183
|
+
patternId: string,
|
|
184
|
+
outcome: boolean,
|
|
185
|
+
brainAccessor: BrainDataAccessor,
|
|
186
|
+
): Promise<PatternStatsUpdate | null> {
|
|
187
|
+
const existing = await brainAccessor.getPattern(patternId);
|
|
188
|
+
if (!existing) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const oldFrequency = existing.frequency;
|
|
193
|
+
const newFrequency = oldFrequency + 1;
|
|
194
|
+
|
|
195
|
+
// Recalculate success rate with running average
|
|
196
|
+
const oldRate = existing.successRate ?? 0.5;
|
|
197
|
+
const newRate = (oldRate * oldFrequency + (outcome ? 1 : 0)) / newFrequency;
|
|
198
|
+
|
|
199
|
+
await brainAccessor.updatePattern(patternId, {
|
|
200
|
+
frequency: newFrequency,
|
|
201
|
+
successRate: Math.round(newRate * 1000) / 1000,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
patternId,
|
|
206
|
+
newFrequency,
|
|
207
|
+
newSuccessRate: Math.round(newRate * 1000) / 1000,
|
|
208
|
+
outcomeSuccess: outcome,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Internal: Pattern Extraction Helpers
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
async function extractBlockerPatterns(
|
|
217
|
+
accessor: DataAccessor,
|
|
218
|
+
minFrequency: number,
|
|
219
|
+
): Promise<DetectedPattern[]> {
|
|
220
|
+
const patterns: DetectedPattern[] = [];
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const { tasks: blockedTasks } = await accessor.queryTasks({ status: 'blocked' });
|
|
224
|
+
|
|
225
|
+
if (blockedTasks.length < minFrequency) {
|
|
226
|
+
return patterns;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Group by blockedBy reason
|
|
230
|
+
const reasonCounts = new Map<string, string[]>();
|
|
231
|
+
for (const t of blockedTasks) {
|
|
232
|
+
const reason = t.blockedBy?.trim() ?? 'unspecified';
|
|
233
|
+
const existing = reasonCounts.get(reason) ?? [];
|
|
234
|
+
existing.push(t.id);
|
|
235
|
+
reasonCounts.set(reason, existing);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const [reason, taskIds] of reasonCounts) {
|
|
239
|
+
if (taskIds.length >= minFrequency) {
|
|
240
|
+
patterns.push({
|
|
241
|
+
type: 'blocker',
|
|
242
|
+
pattern: `Tasks blocked by: ${reason}`,
|
|
243
|
+
context: `${taskIds.length} tasks are currently blocked with this reason`,
|
|
244
|
+
frequency: taskIds.length,
|
|
245
|
+
successRate: null,
|
|
246
|
+
impact: taskIds.length >= 5 ? 'high' : taskIds.length >= 3 ? 'medium' : 'low',
|
|
247
|
+
antiPattern: `Recurring blocker: ${reason}`,
|
|
248
|
+
mitigation: `Address root cause of "${reason}" to unblock ${taskIds.length} tasks`,
|
|
249
|
+
examples: taskIds.slice(0, 10),
|
|
250
|
+
confidence: Math.min(0.5 + taskIds.length * 0.1, 0.9),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Overall blocked pattern if many tasks are blocked
|
|
256
|
+
if (blockedTasks.length >= 3) {
|
|
257
|
+
patterns.push({
|
|
258
|
+
type: 'blocker',
|
|
259
|
+
pattern: `${blockedTasks.length} tasks currently in blocked status`,
|
|
260
|
+
context: 'Project-wide blocked task analysis',
|
|
261
|
+
frequency: blockedTasks.length,
|
|
262
|
+
successRate: null,
|
|
263
|
+
impact: blockedTasks.length >= 10 ? 'high' : 'medium',
|
|
264
|
+
antiPattern: 'High number of blocked tasks indicates systemic issues',
|
|
265
|
+
mitigation: 'Review and triage blocked tasks, identify common root causes',
|
|
266
|
+
examples: blockedTasks.slice(0, 10).map((t) => t.id),
|
|
267
|
+
confidence: 0.8,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Best-effort extraction
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return patterns;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function extractSuccessPatterns(
|
|
278
|
+
accessor: DataAccessor,
|
|
279
|
+
minFrequency: number,
|
|
280
|
+
): Promise<DetectedPattern[]> {
|
|
281
|
+
const patterns: DetectedPattern[] = [];
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const { tasks: doneTasks } = await accessor.queryTasks({ status: 'done' });
|
|
285
|
+
|
|
286
|
+
if (doneTasks.length < minFrequency) {
|
|
287
|
+
return patterns;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Analyze label distribution in completed tasks
|
|
291
|
+
const labelCounts = new Map<string, string[]>();
|
|
292
|
+
for (const t of doneTasks) {
|
|
293
|
+
for (const label of t.labels ?? []) {
|
|
294
|
+
const existing = labelCounts.get(label) ?? [];
|
|
295
|
+
existing.push(t.id);
|
|
296
|
+
labelCounts.set(label, existing);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [label, taskIds] of labelCounts) {
|
|
301
|
+
if (taskIds.length >= minFrequency) {
|
|
302
|
+
patterns.push({
|
|
303
|
+
type: 'success',
|
|
304
|
+
pattern: `Label "${label}" appears in ${taskIds.length} completed tasks`,
|
|
305
|
+
context: `Recurring success pattern detected from completed task labels`,
|
|
306
|
+
frequency: taskIds.length,
|
|
307
|
+
successRate: 1.0,
|
|
308
|
+
impact: taskIds.length >= 10 ? 'high' : taskIds.length >= 5 ? 'medium' : 'low',
|
|
309
|
+
antiPattern: null,
|
|
310
|
+
mitigation: null,
|
|
311
|
+
examples: taskIds.slice(0, 10),
|
|
312
|
+
confidence: Math.min(0.4 + taskIds.length * 0.05, 0.9),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Size distribution pattern
|
|
318
|
+
const sizeCounts: Record<string, number> = {};
|
|
319
|
+
for (const t of doneTasks) {
|
|
320
|
+
const size = t.size ?? 'unspecified';
|
|
321
|
+
sizeCounts[size] = (sizeCounts[size] || 0) + 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const dominantSize = Object.entries(sizeCounts).sort((a, b) => b[1] - a[1])[0];
|
|
325
|
+
if (dominantSize && dominantSize[1] >= minFrequency) {
|
|
326
|
+
patterns.push({
|
|
327
|
+
type: 'success',
|
|
328
|
+
pattern: `Most completed tasks are "${dominantSize[0]}" sized (${dominantSize[1]}/${doneTasks.length})`,
|
|
329
|
+
context: 'Task size distribution analysis of completed work',
|
|
330
|
+
frequency: dominantSize[1],
|
|
331
|
+
successRate: dominantSize[1] / doneTasks.length,
|
|
332
|
+
impact: 'medium',
|
|
333
|
+
antiPattern: null,
|
|
334
|
+
mitigation: null,
|
|
335
|
+
examples: [],
|
|
336
|
+
confidence: 0.6,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
} catch {
|
|
340
|
+
// Best-effort extraction
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return patterns;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function extractWorkflowPatterns(
|
|
347
|
+
accessor: DataAccessor,
|
|
348
|
+
minFrequency: number,
|
|
349
|
+
): Promise<DetectedPattern[]> {
|
|
350
|
+
const patterns: DetectedPattern[] = [];
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Get all tasks to analyze dependency chains
|
|
354
|
+
const { tasks: allTasks } = await accessor.queryTasks({});
|
|
355
|
+
|
|
356
|
+
if (allTasks.length === 0) {
|
|
357
|
+
return patterns;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Find tasks that frequently appear as dependencies
|
|
361
|
+
const depTargetCounts = new Map<string, number>();
|
|
362
|
+
for (const t of allTasks) {
|
|
363
|
+
for (const dep of t.depends ?? []) {
|
|
364
|
+
depTargetCounts.set(dep, (depTargetCounts.get(dep) || 0) + 1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Hub tasks (many things depend on them)
|
|
369
|
+
for (const [taskId, count] of depTargetCounts) {
|
|
370
|
+
if (count >= minFrequency) {
|
|
371
|
+
const hubTask = allTasks.find((t) => t.id === taskId);
|
|
372
|
+
const title = hubTask?.title ?? taskId;
|
|
373
|
+
|
|
374
|
+
patterns.push({
|
|
375
|
+
type: 'workflow',
|
|
376
|
+
pattern: `Task "${title}" (${taskId}) is a dependency hub with ${count} dependents`,
|
|
377
|
+
context: 'Dependency graph analysis — hub tasks are critical path candidates',
|
|
378
|
+
frequency: count,
|
|
379
|
+
successRate: hubTask?.status === 'done' ? 1.0 : null,
|
|
380
|
+
impact: count >= 5 ? 'high' : 'medium',
|
|
381
|
+
antiPattern: count >= 8 ? `Task ${taskId} may be an overly-centralized bottleneck` : null,
|
|
382
|
+
mitigation: count >= 8 ? 'Consider decomposing this task to reduce coupling' : null,
|
|
383
|
+
examples: allTasks
|
|
384
|
+
.filter((t) => t.depends?.includes(taskId))
|
|
385
|
+
.slice(0, 10)
|
|
386
|
+
.map((t) => t.id),
|
|
387
|
+
confidence: 0.7,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Parent task completion rate analysis
|
|
393
|
+
const parentIds = new Set(allTasks.filter((t) => t.parentId).map((t) => t.parentId!));
|
|
394
|
+
for (const parentId of parentIds) {
|
|
395
|
+
const children = allTasks.filter((t) => t.parentId === parentId);
|
|
396
|
+
if (children.length < minFrequency) continue;
|
|
397
|
+
|
|
398
|
+
const doneChildren = children.filter((t) => t.status === 'done');
|
|
399
|
+
const blockedChildren = children.filter((t) => t.status === 'blocked');
|
|
400
|
+
|
|
401
|
+
if (blockedChildren.length >= 2) {
|
|
402
|
+
patterns.push({
|
|
403
|
+
type: 'failure',
|
|
404
|
+
pattern: `Epic/parent ${parentId} has ${blockedChildren.length}/${children.length} children blocked`,
|
|
405
|
+
context: 'Parent task analysis — high blocked child ratio indicates systemic issues',
|
|
406
|
+
frequency: blockedChildren.length,
|
|
407
|
+
successRate: doneChildren.length / children.length,
|
|
408
|
+
impact: blockedChildren.length / children.length >= 0.5 ? 'high' : 'medium',
|
|
409
|
+
antiPattern: 'High ratio of blocked children under a parent task',
|
|
410
|
+
mitigation: 'Review blocked children and address common dependencies or blockers',
|
|
411
|
+
examples: blockedChildren.slice(0, 10).map((t) => t.id),
|
|
412
|
+
confidence: 0.6,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {
|
|
417
|
+
// Best-effort extraction
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return patterns;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function extractObservationPatterns(
|
|
424
|
+
brainAccessor: BrainDataAccessor,
|
|
425
|
+
minFrequency: number,
|
|
426
|
+
): Promise<DetectedPattern[]> {
|
|
427
|
+
const patterns: DetectedPattern[] = [];
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const observations = await brainAccessor.findObservations({ limit: 200 });
|
|
431
|
+
|
|
432
|
+
if (observations.length < minFrequency) {
|
|
433
|
+
return patterns;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Group observations by type
|
|
437
|
+
const typeCounts = new Map<string, number>();
|
|
438
|
+
for (const obs of observations) {
|
|
439
|
+
typeCounts.set(obs.type, (typeCounts.get(obs.type) || 0) + 1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const [type, count] of typeCounts) {
|
|
443
|
+
if (count >= minFrequency) {
|
|
444
|
+
// Map observation type to pattern type
|
|
445
|
+
const patternType = observationTypeToPatternType(type);
|
|
446
|
+
|
|
447
|
+
patterns.push({
|
|
448
|
+
type: patternType,
|
|
449
|
+
pattern: `${count} "${type}" observations recorded`,
|
|
450
|
+
context: 'Brain observation frequency analysis',
|
|
451
|
+
frequency: count,
|
|
452
|
+
successRate: type === 'feature' || type === 'refactor' ? 0.8 : null,
|
|
453
|
+
impact: count >= 10 ? 'high' : count >= 5 ? 'medium' : 'low',
|
|
454
|
+
antiPattern:
|
|
455
|
+
type === 'bugfix' && count >= 5
|
|
456
|
+
? 'High number of bugfix observations may indicate quality issues'
|
|
457
|
+
: null,
|
|
458
|
+
mitigation:
|
|
459
|
+
type === 'bugfix' && count >= 5
|
|
460
|
+
? 'Consider adding more automated tests and code review'
|
|
461
|
+
: null,
|
|
462
|
+
examples: observations
|
|
463
|
+
.filter((o) => o.type === type)
|
|
464
|
+
.slice(0, 5)
|
|
465
|
+
.map((o) => o.id),
|
|
466
|
+
confidence: 0.5,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Detect project-specific observation clusters
|
|
472
|
+
const projectCounts = new Map<string, number>();
|
|
473
|
+
for (const obs of observations) {
|
|
474
|
+
if (obs.project) {
|
|
475
|
+
projectCounts.set(obs.project, (projectCounts.get(obs.project) || 0) + 1);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
for (const [project, count] of projectCounts) {
|
|
480
|
+
if (count >= minFrequency * 2) {
|
|
481
|
+
patterns.push({
|
|
482
|
+
type: 'workflow',
|
|
483
|
+
pattern: `Project "${project}" has ${count} observations — high activity area`,
|
|
484
|
+
context: 'Cross-project observation density analysis',
|
|
485
|
+
frequency: count,
|
|
486
|
+
successRate: null,
|
|
487
|
+
impact: 'medium',
|
|
488
|
+
antiPattern: null,
|
|
489
|
+
mitigation: null,
|
|
490
|
+
examples: [],
|
|
491
|
+
confidence: 0.4,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch {
|
|
496
|
+
// Best-effort extraction
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return patterns;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// Internal: Relevance Scoring
|
|
504
|
+
// ============================================================================
|
|
505
|
+
|
|
506
|
+
function computePatternRelevance(
|
|
507
|
+
pattern: BrainPatternRow,
|
|
508
|
+
taskLabels: Set<string>,
|
|
509
|
+
taskTitle: string,
|
|
510
|
+
taskDesc: string,
|
|
511
|
+
taskType: string,
|
|
512
|
+
): number {
|
|
513
|
+
let score = 0;
|
|
514
|
+
const patternText = pattern.pattern.toLowerCase();
|
|
515
|
+
const contextText = pattern.context.toLowerCase();
|
|
516
|
+
|
|
517
|
+
// Label match (strong signal)
|
|
518
|
+
if (taskLabels.size > 0) {
|
|
519
|
+
for (const label of taskLabels) {
|
|
520
|
+
if (patternText.includes(label) || contextText.includes(label)) {
|
|
521
|
+
score += 0.3;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Title keyword overlap
|
|
528
|
+
const titleWords = taskTitle.split(/\s+/).filter((w) => w.length > 3);
|
|
529
|
+
const matchingTitleWords = titleWords.filter(
|
|
530
|
+
(w) => patternText.includes(w) || contextText.includes(w),
|
|
531
|
+
);
|
|
532
|
+
if (matchingTitleWords.length > 0) {
|
|
533
|
+
score += Math.min(matchingTitleWords.length * 0.15, 0.3);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Description keyword overlap
|
|
537
|
+
if (taskDesc.length > 10) {
|
|
538
|
+
const descWords = taskDesc.split(/\s+/).filter((w) => w.length > 4);
|
|
539
|
+
const matchingDescWords = descWords.filter(
|
|
540
|
+
(w) => patternText.includes(w) || contextText.includes(w),
|
|
541
|
+
);
|
|
542
|
+
if (matchingDescWords.length > 0) {
|
|
543
|
+
score += Math.min(matchingDescWords.length * 0.1, 0.2);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Type match (if pattern mentions task type)
|
|
548
|
+
if (taskType && (patternText.includes(taskType) || contextText.includes(taskType))) {
|
|
549
|
+
score += 0.2;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Boost high-impact patterns
|
|
553
|
+
if (pattern.impact === 'high' && score > 0) {
|
|
554
|
+
score *= 1.2;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Boost high-frequency patterns
|
|
558
|
+
if (pattern.frequency >= 5 && score > 0) {
|
|
559
|
+
score *= 1.1;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return Math.min(score, 1.0);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function buildMatchReason(
|
|
566
|
+
pattern: BrainPatternRow,
|
|
567
|
+
taskLabels: Set<string>,
|
|
568
|
+
taskTitle: string,
|
|
569
|
+
taskDesc: string,
|
|
570
|
+
): string {
|
|
571
|
+
const reasons: string[] = [];
|
|
572
|
+
const patternText = pattern.pattern.toLowerCase();
|
|
573
|
+
const contextText = pattern.context.toLowerCase();
|
|
574
|
+
|
|
575
|
+
for (const label of taskLabels) {
|
|
576
|
+
if (patternText.includes(label) || contextText.includes(label)) {
|
|
577
|
+
reasons.push(`label "${label}" matches`);
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const titleWords = taskTitle.split(/\s+/).filter((w) => w.length > 3);
|
|
583
|
+
const matchingWords = titleWords.filter(
|
|
584
|
+
(w) => patternText.includes(w) || contextText.includes(w),
|
|
585
|
+
);
|
|
586
|
+
if (matchingWords.length > 0) {
|
|
587
|
+
reasons.push(`title keywords [${matchingWords.join(', ')}] match`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Check description keywords against pattern
|
|
591
|
+
const descWords = taskDesc
|
|
592
|
+
.toLowerCase()
|
|
593
|
+
.split(/\s+/)
|
|
594
|
+
.filter((w) => w.length > 4);
|
|
595
|
+
const descMatches = descWords.filter((w) => patternText.includes(w) || contextText.includes(w));
|
|
596
|
+
if (descMatches.length > 0) {
|
|
597
|
+
reasons.push(`description keywords [${descMatches.slice(0, 3).join(', ')}] match`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (reasons.length === 0) {
|
|
601
|
+
reasons.push('general textual similarity');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return reasons.join('; ');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function observationTypeToPatternType(obsType: string): DetectedPattern['type'] {
|
|
608
|
+
switch (obsType) {
|
|
609
|
+
case 'bugfix':
|
|
610
|
+
return 'failure';
|
|
611
|
+
case 'feature':
|
|
612
|
+
case 'refactor':
|
|
613
|
+
return 'success';
|
|
614
|
+
case 'change':
|
|
615
|
+
return 'workflow';
|
|
616
|
+
case 'decision':
|
|
617
|
+
return 'optimization';
|
|
618
|
+
default:
|
|
619
|
+
return 'workflow';
|
|
620
|
+
}
|
|
621
|
+
}
|