@cleocode/core 2026.4.50 → 2026.4.52

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 (52) hide show
  1. package/dist/index.js +511 -41
  2. package/dist/index.js.map +4 -4
  3. package/dist/internal.d.ts +4 -0
  4. package/dist/internal.d.ts.map +1 -1
  5. package/dist/internal.js +669 -45
  6. package/dist/internal.js.map +4 -4
  7. package/dist/memory/brain-export.d.ts +70 -0
  8. package/dist/memory/brain-export.d.ts.map +1 -0
  9. package/dist/memory/brain-lifecycle.d.ts +7 -0
  10. package/dist/memory/brain-lifecycle.d.ts.map +1 -1
  11. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  12. package/dist/memory/brain-stdp.d.ts +122 -0
  13. package/dist/memory/brain-stdp.d.ts.map +1 -0
  14. package/dist/memory/decision-cross-link.d.ts +70 -0
  15. package/dist/memory/decision-cross-link.d.ts.map +1 -0
  16. package/dist/memory/decisions.d.ts.map +1 -1
  17. package/dist/memory/edge-types.d.ts +24 -0
  18. package/dist/memory/edge-types.d.ts.map +1 -0
  19. package/dist/memory/index.d.ts +1 -0
  20. package/dist/memory/index.d.ts.map +1 -1
  21. package/dist/store/brain-schema.d.ts +150 -3
  22. package/dist/store/brain-schema.d.ts.map +1 -1
  23. package/dist/store/brain-sqlite.d.ts.map +1 -1
  24. package/dist/store/validation-schemas.d.ts +1 -0
  25. package/dist/store/validation-schemas.d.ts.map +1 -1
  26. package/dist/validation/verification.d.ts.map +1 -1
  27. package/migrations/drizzle-brain/20260415000001_t626-normalize-co-retrieved-edge-type/migration.sql +14 -0
  28. package/package.json +8 -8
  29. package/src/internal.ts +14 -0
  30. package/src/memory/__tests__/brain-stdp.test.ts +452 -0
  31. package/src/memory/__tests__/decision-cross-link.test.ts +240 -0
  32. package/src/memory/brain-embedding.ts +1 -1
  33. package/src/memory/brain-export.ts +286 -0
  34. package/src/memory/brain-lifecycle.ts +23 -4
  35. package/src/memory/brain-retrieval.ts +80 -14
  36. package/src/memory/brain-similarity.ts +1 -1
  37. package/src/memory/brain-stdp.ts +448 -0
  38. package/src/memory/claude-mem-migration.ts +1 -1
  39. package/src/memory/decision-cross-link.ts +276 -0
  40. package/src/memory/decisions.ts +7 -0
  41. package/src/memory/edge-types.ts +31 -0
  42. package/src/memory/index.ts +2 -0
  43. package/src/sessions/briefing.ts +1 -1
  44. package/src/skills/dispatch.ts +1 -1
  45. package/src/skills/injection/subagent.ts +1 -1
  46. package/src/skills/orchestrator/spawn.ts +1 -1
  47. package/src/store/brain-schema.ts +54 -0
  48. package/src/store/brain-sqlite.ts +17 -0
  49. package/src/store/json.ts +2 -2
  50. package/src/system/archive-analytics.ts +1 -1
  51. package/src/tasks/task-ops.ts +2 -2
  52. package/src/validation/verification.ts +2 -6
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Decision cross-link module for CLEO BRAIN.
3
+ *
4
+ * Extracts file paths and symbol names referenced in a decision's text and
5
+ * rationale, then creates `affects` edges from the decision graph node to
6
+ * matching `file` / `symbol` nodes in brain_page_nodes. This implements
7
+ * the cross-substrate edge described in
8
+ * docs/plans/brain-synaptic-visualization-research.md §3.2.
9
+ *
10
+ * All database operations are best-effort — they never throw or block the
11
+ * caller. Nodes for referenced files / symbols are upserted on demand so
12
+ * the graph remains consistent even when the target has not yet been
13
+ * independently indexed.
14
+ *
15
+ * @task T626
16
+ * @epic T626
17
+ */
18
+
19
+ import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Extraction
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** A reference extracted from a decision or rationale string. */
26
+ export interface ExtractedRef {
27
+ /** Raw text that matched. */
28
+ raw: string;
29
+ /** Resolved graph node ID: 'file:<path>' or 'symbol:<name>'. */
30
+ nodeId: string;
31
+ /** Discriminated node type. */
32
+ nodeType: 'file' | 'symbol';
33
+ /** Human-readable label for the graph node. */
34
+ label: string;
35
+ }
36
+
37
+ /**
38
+ * Regex patterns used to locate file paths and symbol names inside text.
39
+ *
40
+ * File-path pattern — matches:
41
+ * - Relative paths: `src/store/brain-schema.ts`
42
+ * - Absolute paths: `/mnt/projects/cleocode/packages/core/src/…`
43
+ * - Extension-gated: only `.ts`, `.tsx`, `.js`, `.jsx`, `.rs`, `.json`
44
+ *
45
+ * Symbol pattern — matches:
46
+ * - PascalCase class/interface names: `BrainPageNodes`
47
+ * - camelCase function names at word boundaries: `upsertGraphNode`
48
+ * - snake_case identifiers: `brain_page_edges`
49
+ *
50
+ * Overlapping matches are deduplicated by nodeId before edge creation.
51
+ */
52
+
53
+ const FILE_PATH_RE =
54
+ /(?:^|[\s`"'([\]{,])((\/[\w.\-/]+|[\w.-]+(?:\/[\w.-]+)+)\.(ts|tsx|js|jsx|rs|json))(?=$|[\s`"')[\]{,])/gm;
55
+
56
+ const SYMBOL_RE =
57
+ /(?<![`"'/\w.])(?:[A-Z][a-zA-Z0-9]{2,}|[a-z][a-zA-Z0-9]*(?:[A-Z][a-zA-Z0-9]*)+|[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*){2,})(?![`"'/\w])/g;
58
+
59
+ /**
60
+ * Extract file-path and symbol references from free-form text.
61
+ *
62
+ * Symbols shorter than 4 characters or matching common English stop-words
63
+ * are filtered to reduce noise.
64
+ *
65
+ * @param text - Decision text and/or rationale to scan.
66
+ * @returns Deduplicated array of extracted references.
67
+ */
68
+ export function extractReferencedSymbols(text: string): ExtractedRef[] {
69
+ const seen = new Set<string>();
70
+ const refs: ExtractedRef[] = [];
71
+
72
+ // --- File paths ---
73
+ for (const match of text.matchAll(FILE_PATH_RE)) {
74
+ const raw = match[1];
75
+ if (!raw) continue;
76
+ const nodeId = `file:${raw}`;
77
+ if (seen.has(nodeId)) continue;
78
+ seen.add(nodeId);
79
+ refs.push({ raw, nodeId, nodeType: 'file', label: raw });
80
+ }
81
+
82
+ // --- Symbol names ---
83
+ for (const match of text.matchAll(SYMBOL_RE)) {
84
+ const raw = match[0];
85
+ if (!raw || raw.length < 4) continue;
86
+ if (SYMBOL_STOP_WORDS.has(raw.toLowerCase())) continue;
87
+ const nodeId = `symbol:${raw}`;
88
+ if (seen.has(nodeId)) continue;
89
+ seen.add(nodeId);
90
+ refs.push({ raw, nodeId, nodeType: 'symbol', label: raw });
91
+ }
92
+
93
+ return refs;
94
+ }
95
+
96
+ /**
97
+ * Common English / technical words that look like camelCase or PascalCase
98
+ * symbols but carry no meaningful code reference. Filtered out to keep the
99
+ * extracted reference set signal-rich.
100
+ */
101
+ const SYMBOL_STOP_WORDS = new Set([
102
+ 'this',
103
+ 'that',
104
+ 'with',
105
+ 'from',
106
+ 'into',
107
+ 'when',
108
+ 'then',
109
+ 'also',
110
+ 'both',
111
+ 'each',
112
+ 'such',
113
+ 'over',
114
+ 'after',
115
+ 'before',
116
+ 'always',
117
+ 'never',
118
+ 'should',
119
+ 'must',
120
+ 'will',
121
+ 'would',
122
+ 'could',
123
+ 'have',
124
+ 'been',
125
+ 'there',
126
+ 'their',
127
+ 'they',
128
+ 'them',
129
+ 'these',
130
+ 'those',
131
+ 'some',
132
+ 'only',
133
+ 'just',
134
+ 'more',
135
+ 'most',
136
+ 'many',
137
+ 'much',
138
+ 'well',
139
+ 'very',
140
+ 'here',
141
+ 'where',
142
+ 'which',
143
+ 'what',
144
+ 'why',
145
+ 'how',
146
+ 'the',
147
+ 'and',
148
+ 'but',
149
+ 'for',
150
+ 'not',
151
+ 'are',
152
+ 'was',
153
+ 'were',
154
+ 'has',
155
+ 'had',
156
+ 'its',
157
+ 'the',
158
+ 'data',
159
+ 'true',
160
+ 'false',
161
+ 'null',
162
+ 'none',
163
+ 'type',
164
+ 'test',
165
+ 'spec',
166
+ 'todo',
167
+ 'fixme',
168
+ 'note',
169
+ 'example',
170
+ 'index',
171
+ 'config',
172
+ 'error',
173
+ 'value',
174
+ 'input',
175
+ 'output',
176
+ 'result',
177
+ 'return',
178
+ 'default',
179
+ 'source',
180
+ 'target',
181
+ 'import',
182
+ 'export',
183
+ 'class',
184
+ 'interface',
185
+ 'function',
186
+ 'const',
187
+ 'async',
188
+ 'await',
189
+ ]);
190
+
191
+ // ---------------------------------------------------------------------------
192
+ // Edge creation
193
+ // ---------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Create `affects` edges from a decision graph node to every referenced
197
+ * file / symbol node.
198
+ *
199
+ * For each reference:
200
+ * 1. Upsert the target node (file or symbol) so the graph stays consistent.
201
+ * 2. Insert an `applies_to` edge from `decision:<id>` to the target node.
202
+ *
203
+ * All writes are best-effort via {@link upsertGraphNode} and
204
+ * {@link addGraphEdge} — failures are swallowed internally.
205
+ *
206
+ * @param projectRoot - Absolute path to the project root directory.
207
+ * @param decisionId - The decision ID (e.g. `D001`).
208
+ * @param refs - Extracted references returned by {@link extractReferencedSymbols}.
209
+ */
210
+ export async function linkDecisionToTargets(
211
+ projectRoot: string,
212
+ decisionId: string,
213
+ refs: ExtractedRef[],
214
+ ): Promise<void> {
215
+ const fromId = `decision:${decisionId}`;
216
+
217
+ const writes = refs.map(async (ref) => {
218
+ // Upsert the target node so the edge has a valid destination even if the
219
+ // file / symbol has not been independently indexed yet.
220
+ await upsertGraphNode(
221
+ projectRoot,
222
+ ref.nodeId,
223
+ ref.nodeType,
224
+ ref.label,
225
+ 0.5, // placeholder quality until nexus indexes it
226
+ ref.raw,
227
+ );
228
+
229
+ await addGraphEdge(
230
+ projectRoot,
231
+ fromId,
232
+ ref.nodeId,
233
+ 'applies_to',
234
+ 1.0,
235
+ 'auto:decision-cross-link',
236
+ );
237
+ });
238
+
239
+ // Fire all writes concurrently — individual failures are swallowed inside
240
+ // upsertGraphNode / addGraphEdge.
241
+ await Promise.allSettled(writes);
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Convenience facade
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Extract file/symbol references from a decision and create `applies_to`
250
+ * edges in the brain graph. Combines {@link extractReferencedSymbols} and
251
+ * {@link linkDecisionToTargets} in one call.
252
+ *
253
+ * This is the function wired into {@link storeDecision} after a new decision
254
+ * is saved. It is always fire-and-forget: the caller should NOT await it
255
+ * when used inside the decision write path.
256
+ *
257
+ * @param projectRoot - Absolute path to the project root directory.
258
+ * @param decisionId - The saved decision ID (e.g. `D001`).
259
+ * @param decisionText - Full decision text.
260
+ * @param rationale - Full rationale text.
261
+ */
262
+ export async function autoCrossLinkDecision(
263
+ projectRoot: string,
264
+ decisionId: string,
265
+ decisionText: string,
266
+ rationale: string,
267
+ ): Promise<void> {
268
+ try {
269
+ const combined = `${decisionText} ${rationale}`;
270
+ const refs = extractReferencedSymbols(combined);
271
+ if (refs.length === 0) return;
272
+ await linkDecisionToTargets(projectRoot, decisionId, refs);
273
+ } catch {
274
+ /* best-effort — never surface errors to caller */
275
+ }
276
+ }
@@ -13,6 +13,7 @@ import { getBrainAccessor } from '../store/brain-accessor.js';
13
13
  import type { BrainDecisionRow, NewBrainDecisionRow } from '../store/brain-schema.js';
14
14
  import { taskExistsInTasksDb } from '../store/cross-db-cleanup.js';
15
15
  import { getDb } from '../store/sqlite.js';
16
+ import { autoCrossLinkDecision } from './decision-cross-link.js';
16
17
  import { addGraphEdge, upsertGraphNode } from './graph-auto-populate.js';
17
18
  import { computeDecisionQuality } from './quality-scoring.js';
18
19
  import { detectSupersession, supersedeMemory } from './temporal-supersession.js';
@@ -229,6 +230,12 @@ export async function storeDecision(
229
230
  'auto:store-decision',
230
231
  );
231
232
  }
233
+
234
+ // Cross-link decision → referenced file/symbol nodes (T626 phase 1).
235
+ // Fire-and-forget — autoCrossLinkDecision swallows its own errors.
236
+ autoCrossLinkDecision(projectRoot, saved.id, saved.decision, saved.rationale).catch(() => {
237
+ /* best-effort */
238
+ });
232
239
  } catch {
233
240
  /* Graph population is best-effort — never block the primary return */
234
241
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Canonical edge-type constants for `brain_page_edges`.
3
+ *
4
+ * All code that writes or queries `brain_page_edges.edge_type` MUST use
5
+ * these constants instead of raw string literals to prevent enum drift.
6
+ *
7
+ * The values here are a subset of `BRAIN_EDGE_TYPES` (brain-schema.ts).
8
+ * They are duplicated as constants so callers do not need to import the
9
+ * schema module (which carries Drizzle + SQLite dependencies).
10
+ *
11
+ * @epic T626
12
+ */
13
+ export const EDGE_TYPES = {
14
+ // Plasticity (Hebbian / STDP co-retrieval)
15
+ CO_RETRIEVED: 'co_retrieved',
16
+ // Temporal supersession
17
+ SUPERSEDES: 'supersedes',
18
+ // Task / decision / pattern → target context
19
+ APPLIES_TO: 'applies_to',
20
+ // Provenance
21
+ DERIVED_FROM: 'derived_from',
22
+ // Observation → symbol/file impact
23
+ AFFECTS: 'affects',
24
+ // Observation → symbol name mention
25
+ MENTIONS: 'mentions',
26
+ // Observation → symbol/file structural link
27
+ DOCUMENTS: 'documents',
28
+ } as const;
29
+
30
+ /** Discriminated union of the canonical edge type constant values. */
31
+ export type EdgeType = (typeof EDGE_TYPES)[keyof typeof EDGE_TYPES];
@@ -1501,6 +1501,8 @@ export * from './brain-retrieval.js';
1501
1501
  export * from './brain-search.js';
1502
1502
  // === BRAIN Memory modules (brain.db backed) ===
1503
1503
  export * from './decisions.js';
1504
+ // === T626-M1: Canonical edge-type constants ===
1505
+ export * from './edge-types.js';
1504
1506
  // === T549 Wave 2: Extraction Gate ===
1505
1507
  export * from './extraction-gate.js';
1506
1508
  export * from './learnings.js';
@@ -292,7 +292,7 @@ async function computeLastSession(
292
292
  const allSessions = await accessor.loadSessions();
293
293
 
294
294
  const session = allSessions.find((s) => s.id === sessionId);
295
- if (!session || !session.endedAt) return null;
295
+ if (!session?.endedAt) return null;
296
296
 
297
297
  // Calculate duration if startedAt is available
298
298
  let duration = 0;
@@ -382,7 +382,7 @@ export function prepareSpawnMulti(
382
382
  const isPrimary = i === 0;
383
383
 
384
384
  const skill = findSkill(skillName, cwd);
385
- if (!skill || !skill.content) {
385
+ if (!skill?.content) {
386
386
  continue;
387
387
  }
388
388
 
@@ -205,7 +205,7 @@ export async function orchestratorSpawnSkill(
205
205
  ): Promise<string> {
206
206
  // Find the skill
207
207
  const skill = findSkill(skillName, cwd);
208
- if (!skill || !skill.content) {
208
+ if (!skill?.content) {
209
209
  throw new CleoError(ExitCode.NOT_FOUND, `Skill not found: ${skillName}`, {
210
210
  fix: `Check skills directory for ${skillName}/SKILL.md`,
211
211
  });
@@ -45,7 +45,7 @@ export async function buildPrompt(
45
45
 
46
46
  // Find skill template
47
47
  const skill = findSkill(templateName, cwd);
48
- if (!skill || !skill.content) {
48
+ if (!skill?.content) {
49
49
  const { canonical } = mapSkillName(templateName);
50
50
  throw new CleoError(ExitCode.NOT_FOUND, `Skill template ${templateName} not found`, {
51
51
  fix: `Expected at skills/${canonical}/SKILL.md`,
@@ -563,6 +563,8 @@ export const BRAIN_EDGE_TYPES = [
563
563
  // Graph bridging (memory ↔ code)
564
564
  'references', // observation → references → symbol
565
565
  'modified_by', // file → modified_by → session
566
+ // Plasticity (Hebbian + STDP co-retrieval)
567
+ 'co_retrieved', // A → co_retrieved → B (Hebbian: frequently retrieved together)
566
568
  ] as const;
567
569
 
568
570
  /** Discriminated union of all supported brain graph edge types. */
@@ -705,11 +707,61 @@ export const brainRetrievalLog = sqliteTable(
705
707
  /** Estimated tokens consumed by this retrieval. */
706
708
  tokensUsed: integer('tokens_used'),
707
709
 
710
+ /** Session ID (soft FK to tasks.db sessions). Enables grouping retrievals by session for STDP analysis. */
711
+ sessionId: text('session_id'),
712
+
708
713
  createdAt: text('created_at').notNull().default(sql`(datetime('now'))`),
709
714
  },
710
715
  (table) => [
711
716
  index('idx_retrieval_log_created').on(table.createdAt),
712
717
  index('idx_retrieval_log_source').on(table.source),
718
+ index('idx_retrieval_log_session').on(table.sessionId),
719
+ ],
720
+ );
721
+
722
+ // ============================================================================
723
+ // PLASTICITY EVENTS — STDP weight-change audit log (T626 phase 5)
724
+ // ============================================================================
725
+
726
+ /**
727
+ * Records every STDP weight-change event applied to a brain_page_edges row.
728
+ *
729
+ * Each row captures the causal pair (source_node, target_node), the signed
730
+ * delta applied to the edge weight, whether it was a potentiation or
731
+ * depression event, and which session and timestamp triggered it.
732
+ *
733
+ * @task T626
734
+ * @epic T626
735
+ */
736
+ export const brainPlasticityEvents = sqliteTable(
737
+ 'brain_plasticity_events',
738
+ {
739
+ id: integer('id').primaryKey({ autoIncrement: true }),
740
+ /** from_id of the affected brain_page_edges row. */
741
+ sourceNode: text('source_node').notNull(),
742
+ /** to_id of the affected brain_page_edges row. */
743
+ targetNode: text('target_node').notNull(),
744
+ /**
745
+ * Signed weight delta applied to the edge.
746
+ * Positive = potentiation (LTP), negative = depression (LTD).
747
+ */
748
+ deltaW: real('delta_w').notNull(),
749
+ /**
750
+ * STDP event kind: `ltp` (Long-Term Potentiation) or `ltd` (Long-Term
751
+ * Depression).
752
+ */
753
+ kind: text('kind', { enum: ['ltp', 'ltd'] }).notNull(),
754
+ /** ISO 8601 timestamp when this event was applied. */
755
+ timestamp: text('timestamp').notNull().default(sql`(datetime('now'))`),
756
+ /** Session ID that triggered the STDP pass, if available. */
757
+ sessionId: text('session_id'),
758
+ },
759
+ (table) => [
760
+ index('idx_plasticity_source').on(table.sourceNode),
761
+ index('idx_plasticity_target').on(table.targetNode),
762
+ index('idx_plasticity_timestamp').on(table.timestamp),
763
+ index('idx_plasticity_session').on(table.sessionId),
764
+ index('idx_plasticity_kind').on(table.kind),
713
765
  ],
714
766
  );
715
767
 
@@ -733,4 +785,6 @@ export type BrainPageEdgeRow = typeof brainPageEdges.$inferSelect;
733
785
  export type NewBrainPageEdgeRow = typeof brainPageEdges.$inferInsert;
734
786
  export type BrainStickyNoteRow = typeof brainStickyNotes.$inferSelect;
735
787
  export type NewBrainStickyNoteRow = typeof brainStickyNotes.$inferInsert;
788
+ export type BrainPlasticityEventRow = typeof brainPlasticityEvents.$inferSelect;
789
+ export type NewBrainPlasticityEventRow = typeof brainPlasticityEvents.$inferInsert;
736
790
  // BrainNodeType and BrainEdgeType are declared alongside their enum arrays above.
@@ -158,6 +158,23 @@ function runBrainMigrations(
158
158
  // all migrations as applied without actually running them (Scenario 2 race).
159
159
  // ensureColumns is idempotent — no-op if the column already exists.
160
160
  ensureColumns(nativeDb, 'brain_observations', [{ name: 'agent', ddl: 'text' }], 'brain');
161
+
162
+ // T626-M1: Normalize co_retrieved edge type — idempotent safety-net UPDATE.
163
+ // The shipped Hebbian strengthener emitted edge_type = 'relates_to' instead of
164
+ // 'co_retrieved'. Relabel only rows from the consolidation provenance so no
165
+ // semantic edges are affected. The Drizzle migration file does the same UPDATE;
166
+ // this guard handles installs where the journal reconciler already marked
167
+ // the migration applied before the SQL ran.
168
+ if (tableExists(nativeDb, 'brain_page_edges')) {
169
+ nativeDb
170
+ .prepare(
171
+ `UPDATE brain_page_edges
172
+ SET edge_type = 'co_retrieved'
173
+ WHERE edge_type = 'relates_to'
174
+ AND provenance LIKE 'consolidation:%'`,
175
+ )
176
+ .run();
177
+ }
161
178
  }
162
179
 
163
180
  /**
package/src/store/json.ts CHANGED
@@ -206,7 +206,7 @@ export async function readLogEntries(filePath: string): Promise<Record<string, u
206
206
  if (remainder) {
207
207
  for (const line of remainder.split('\n')) {
208
208
  const l = line.trim();
209
- if (!l || !l.startsWith('{')) continue;
209
+ if (!l?.startsWith('{')) continue;
210
210
  try {
211
211
  entries.push(JSON.parse(l) as Record<string, unknown>);
212
212
  } catch {
@@ -219,7 +219,7 @@ export async function readLogEntries(filePath: string): Promise<Record<string, u
219
219
  // Pure JSONL (no initial JSON object)
220
220
  for (const line of trimmed.split('\n')) {
221
221
  const l = line.trim();
222
- if (!l || !l.startsWith('{')) continue;
222
+ if (!l?.startsWith('{')) continue;
223
223
  try {
224
224
  entries.push(JSON.parse(l) as Record<string, unknown>);
225
225
  } catch {
@@ -420,7 +420,7 @@ export async function analyzeArchive(
420
420
 
421
421
  const reportType = opts.report ?? 'summary';
422
422
 
423
- if (!data || !data.archivedTasks?.length) {
423
+ if (!data?.archivedTasks?.length) {
424
424
  return {
425
425
  report: reportType,
426
426
  filters: null,
@@ -144,7 +144,7 @@ function measureDependencyDepth(
144
144
  visited.add(taskId);
145
145
 
146
146
  const task = taskMap.get(taskId);
147
- if (!task || !task.depends || task.depends.length === 0) return 0;
147
+ if (!task?.depends || task.depends.length === 0) return 0;
148
148
 
149
149
  let maxDepth = 0;
150
150
  for (const depId of task.depends) {
@@ -921,7 +921,7 @@ export async function coreTaskUnarchive(
921
921
  }
922
922
 
923
923
  const archive = await accessor.loadArchive();
924
- if (!archive || !archive.archivedTasks) {
924
+ if (!archive?.archivedTasks) {
925
925
  throw new Error('No archive file found');
926
926
  }
927
927
 
@@ -438,9 +438,7 @@ export function allEpicChildrenVerified(epicId: string, tasks: TaskForVerificati
438
438
  const incomplete = children.filter((t) => t.status !== 'done');
439
439
  if (incomplete.length > 0) return false;
440
440
 
441
- const unverified = children.filter(
442
- (t) => t.status === 'done' && (!t.verification || !t.verification.passed),
443
- );
441
+ const unverified = children.filter((t) => t.status === 'done' && !t.verification?.passed);
444
442
  return unverified.length === 0;
445
443
  }
446
444
 
@@ -451,9 +449,7 @@ export function allEpicChildrenVerified(epicId: string, tasks: TaskForVerificati
451
449
  export function allSiblingsVerified(parentId: string, tasks: TaskForVerification[]): boolean {
452
450
  const siblings = tasks.filter((t) => t.parentId === parentId);
453
451
 
454
- const unverifiedDone = siblings.filter(
455
- (t) => t.status === 'done' && (!t.verification || !t.verification.passed),
456
- );
452
+ const unverifiedDone = siblings.filter((t) => t.status === 'done' && !t.verification?.passed);
457
453
 
458
454
  const incomplete = siblings.filter(
459
455
  (t) => t.status === 'pending' || t.status === 'active' || t.status === 'blocked',