@cleocode/core 2026.3.58 → 2026.3.60

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 (153) hide show
  1. package/dist/agents/agent-registry.d.ts +206 -0
  2. package/dist/agents/agent-registry.d.ts.map +1 -0
  3. package/dist/agents/agent-registry.js +288 -0
  4. package/dist/agents/agent-registry.js.map +1 -0
  5. package/dist/agents/agent-schema.js +5 -0
  6. package/dist/agents/agent-schema.js.map +1 -1
  7. package/dist/agents/execution-learning.js +474 -0
  8. package/dist/agents/execution-learning.js.map +1 -0
  9. package/dist/agents/health-monitor.d.ts +161 -0
  10. package/dist/agents/health-monitor.d.ts.map +1 -0
  11. package/dist/agents/health-monitor.js +217 -0
  12. package/dist/agents/health-monitor.js.map +1 -0
  13. package/dist/agents/index.d.ts +3 -1
  14. package/dist/agents/index.d.ts.map +1 -1
  15. package/dist/agents/index.js +9 -1
  16. package/dist/agents/index.js.map +1 -1
  17. package/dist/agents/retry.d.ts +57 -4
  18. package/dist/agents/retry.d.ts.map +1 -1
  19. package/dist/agents/retry.js +57 -4
  20. package/dist/agents/retry.js.map +1 -1
  21. package/dist/backfill/index.d.ts +27 -0
  22. package/dist/backfill/index.d.ts.map +1 -1
  23. package/dist/backfill/index.js +229 -0
  24. package/dist/backfill/index.js.map +1 -0
  25. package/dist/bootstrap.d.ts +2 -1
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +135 -28
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/cleo.d.ts +40 -0
  30. package/dist/cleo.d.ts.map +1 -1
  31. package/dist/config.js +83 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1036 -536
  36. package/dist/index.js.map +4 -4
  37. package/dist/intelligence/adaptive-validation.js +497 -0
  38. package/dist/intelligence/adaptive-validation.js.map +1 -0
  39. package/dist/intelligence/impact.d.ts +34 -1
  40. package/dist/intelligence/impact.d.ts.map +1 -1
  41. package/dist/intelligence/impact.js +176 -0
  42. package/dist/intelligence/impact.js.map +1 -1
  43. package/dist/intelligence/index.d.ts +2 -2
  44. package/dist/intelligence/index.d.ts.map +1 -1
  45. package/dist/intelligence/index.js +6 -1
  46. package/dist/intelligence/index.js.map +1 -1
  47. package/dist/intelligence/types.d.ts +60 -0
  48. package/dist/intelligence/types.d.ts.map +1 -1
  49. package/dist/internal.d.ts +5 -4
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +11 -2
  52. package/dist/internal.js.map +1 -1
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.d.ts.map +1 -0
  55. package/dist/lib/index.js +10 -0
  56. package/dist/lib/index.js.map +1 -0
  57. package/dist/lib/retry.d.ts +128 -0
  58. package/dist/lib/retry.d.ts.map +1 -0
  59. package/dist/lib/retry.js +152 -0
  60. package/dist/lib/retry.js.map +1 -0
  61. package/dist/nexus/sharing/index.d.ts +48 -2
  62. package/dist/nexus/sharing/index.d.ts.map +1 -1
  63. package/dist/nexus/sharing/index.js +110 -1
  64. package/dist/nexus/sharing/index.js.map +1 -1
  65. package/dist/scaffold.d.ts.map +1 -1
  66. package/dist/scaffold.js +22 -2
  67. package/dist/scaffold.js.map +1 -1
  68. package/dist/sessions/session-enforcement.js +4 -0
  69. package/dist/sessions/session-enforcement.js.map +1 -1
  70. package/dist/stats/index.js +2 -0
  71. package/dist/stats/index.js.map +1 -1
  72. package/dist/stats/workflow-telemetry.d.ts +15 -0
  73. package/dist/stats/workflow-telemetry.d.ts.map +1 -1
  74. package/dist/stats/workflow-telemetry.js +400 -0
  75. package/dist/stats/workflow-telemetry.js.map +1 -0
  76. package/dist/store/brain-schema.js +4 -1
  77. package/dist/store/brain-schema.js.map +1 -1
  78. package/dist/store/converters.js +2 -0
  79. package/dist/store/converters.js.map +1 -1
  80. package/dist/store/cross-db-cleanup.d.ts +35 -0
  81. package/dist/store/cross-db-cleanup.d.ts.map +1 -1
  82. package/dist/store/cross-db-cleanup.js +169 -0
  83. package/dist/store/cross-db-cleanup.js.map +1 -0
  84. package/dist/store/db-helpers.js +2 -0
  85. package/dist/store/db-helpers.js.map +1 -1
  86. package/dist/store/migration-sqlite.js +5 -0
  87. package/dist/store/migration-sqlite.js.map +1 -1
  88. package/dist/store/sqlite-data-accessor.js +20 -28
  89. package/dist/store/sqlite-data-accessor.js.map +1 -1
  90. package/dist/store/sqlite.js +13 -2
  91. package/dist/store/sqlite.js.map +1 -1
  92. package/dist/store/task-store.js +4 -0
  93. package/dist/store/task-store.js.map +1 -1
  94. package/dist/store/tasks-schema.js +50 -20
  95. package/dist/store/tasks-schema.js.map +1 -1
  96. package/dist/tasks/add.js +87 -3
  97. package/dist/tasks/add.js.map +1 -1
  98. package/dist/tasks/complete.d.ts.map +1 -1
  99. package/dist/tasks/complete.js +15 -4
  100. package/dist/tasks/complete.js.map +1 -1
  101. package/dist/tasks/enforcement.d.ts.map +1 -1
  102. package/dist/tasks/enforcement.js +8 -1
  103. package/dist/tasks/enforcement.js.map +1 -1
  104. package/dist/tasks/epic-enforcement.d.ts +61 -0
  105. package/dist/tasks/epic-enforcement.d.ts.map +1 -1
  106. package/dist/tasks/epic-enforcement.js +294 -0
  107. package/dist/tasks/epic-enforcement.js.map +1 -0
  108. package/dist/tasks/index.js +1 -1
  109. package/dist/tasks/index.js.map +1 -1
  110. package/dist/tasks/pipeline-stage.d.ts +70 -1
  111. package/dist/tasks/pipeline-stage.d.ts.map +1 -1
  112. package/dist/tasks/pipeline-stage.js +248 -0
  113. package/dist/tasks/pipeline-stage.js.map +1 -0
  114. package/dist/tasks/update.js +28 -0
  115. package/dist/tasks/update.js.map +1 -1
  116. package/package.json +5 -5
  117. package/schemas/config.schema.json +37 -1547
  118. package/src/__tests__/sharing.test.ts +24 -0
  119. package/src/agents/__tests__/agent-registry.test.ts +351 -0
  120. package/src/agents/__tests__/health-monitor.test.ts +332 -0
  121. package/src/agents/agent-registry.ts +394 -0
  122. package/src/agents/health-monitor.ts +279 -0
  123. package/src/agents/index.ts +24 -1
  124. package/src/agents/retry.ts +57 -4
  125. package/src/backfill/index.ts +27 -0
  126. package/src/bootstrap.ts +171 -30
  127. package/src/cleo.ts +103 -2
  128. package/src/config.ts +3 -3
  129. package/src/index.ts +1 -0
  130. package/src/intelligence/__tests__/impact.test.ts +165 -1
  131. package/src/intelligence/impact.ts +203 -0
  132. package/src/intelligence/index.ts +3 -0
  133. package/src/intelligence/types.ts +76 -0
  134. package/src/internal.ts +20 -0
  135. package/src/lib/__tests__/retry.test.ts +321 -0
  136. package/src/lib/index.ts +16 -0
  137. package/src/lib/retry.ts +224 -0
  138. package/src/nexus/sharing/index.ts +142 -2
  139. package/src/scaffold.ts +24 -2
  140. package/src/stats/workflow-telemetry.ts +15 -0
  141. package/src/store/__tests__/session-store.test.ts +43 -7
  142. package/src/store/__tests__/task-store.test.ts +1 -1
  143. package/src/store/__tests__/test-db-helper.ts +7 -3
  144. package/src/store/cross-db-cleanup.ts +35 -0
  145. package/src/tasks/__tests__/epic-enforcement.test.ts +9 -4
  146. package/src/tasks/__tests__/minimal-test.test.ts +2 -2
  147. package/src/tasks/__tests__/update.test.ts +25 -25
  148. package/src/tasks/complete.ts +11 -6
  149. package/src/tasks/enforcement.ts +6 -3
  150. package/src/tasks/epic-enforcement.ts +61 -0
  151. package/src/tasks/pipeline-stage.ts +70 -1
  152. package/templates/config.template.json +5 -116
  153. package/templates/global-config.template.json +2 -44
@@ -8,13 +8,19 @@
8
8
  * - Blast radius calculation
9
9
  * - Critical path detection
10
10
  * - Edge cases (orphan tasks, circular refs, no deps)
11
+ * - predictImpact: free-text change description matching (T043)
11
12
  *
12
13
  * @module intelligence
13
14
  */
14
15
 
15
16
  import type { DataAccessor, Task } from '@cleocode/contracts';
16
17
  import { describe, expect, it } from 'vitest';
17
- import { analyzeChangeImpact, analyzeTaskImpact, calculateBlastRadius } from '../impact.js';
18
+ import {
19
+ analyzeChangeImpact,
20
+ analyzeTaskImpact,
21
+ calculateBlastRadius,
22
+ predictImpact,
23
+ } from '../impact.js';
18
24
 
19
25
  // ============================================================================
20
26
  // Test Helpers
@@ -451,3 +457,161 @@ describe('calculateBlastRadius', () => {
451
457
  expect(result.transitiveCount).toBe(2);
452
458
  });
453
459
  });
460
+
461
+ // ============================================================================
462
+ // predictImpact (T043)
463
+ // ============================================================================
464
+
465
+ describe('predictImpact', () => {
466
+ it('returns empty report when no tasks match the change description', async () => {
467
+ const tasks = [
468
+ makeTask({ id: 'T001', title: 'Set up CI pipeline', description: 'Configure CI runner' }),
469
+ makeTask({ id: 'T002', title: 'Write unit tests', description: 'Add test coverage' }),
470
+ ];
471
+ const acc = mockAccessor(tasks);
472
+
473
+ const result = await predictImpact('authentication login oauth', undefined, acc);
474
+
475
+ expect(result.change).toBe('authentication login oauth');
476
+ expect(result.matchedTasks).toHaveLength(0);
477
+ expect(result.affectedTasks).toHaveLength(0);
478
+ expect(result.totalAffected).toBe(0);
479
+ expect(result.summary).toContain('No tasks matched');
480
+ });
481
+
482
+ it('returns direct match when task title contains change keywords', async () => {
483
+ const tasks = [
484
+ makeTask({
485
+ id: 'T001',
486
+ title: 'Implement authentication module',
487
+ description: 'Add JWT auth',
488
+ }),
489
+ makeTask({ id: 'T002', title: 'Set up database schema', description: 'Create tables' }),
490
+ ];
491
+ const acc = mockAccessor(tasks);
492
+
493
+ const result = await predictImpact('authentication module', undefined, acc);
494
+
495
+ expect(result.matchedTasks).toHaveLength(1);
496
+ expect(result.matchedTasks[0]?.id).toBe('T001');
497
+ expect(result.matchedTasks[0]?.exposure).toBe('direct');
498
+ expect(result.totalAffected).toBe(1);
499
+ });
500
+
501
+ it('traces downstream dependents from matched seed tasks', async () => {
502
+ const tasks = [
503
+ makeTask({ id: 'T001', title: 'authentication service implementation', description: '' }),
504
+ makeTask({
505
+ id: 'T002',
506
+ title: 'User login form',
507
+ description: 'Depends on auth service',
508
+ depends: ['T001'],
509
+ }),
510
+ makeTask({
511
+ id: 'T003',
512
+ title: 'Dashboard access control',
513
+ description: 'Requires user login',
514
+ depends: ['T002'],
515
+ }),
516
+ makeTask({
517
+ id: 'T004',
518
+ title: 'Unrelated database migration',
519
+ description: 'Schema changes',
520
+ }),
521
+ ];
522
+ const acc = mockAccessor(tasks);
523
+
524
+ const result = await predictImpact('authentication service', undefined, acc);
525
+
526
+ // T001 is the direct match (seed)
527
+ expect(result.matchedTasks.map((t) => t.id)).toContain('T001');
528
+
529
+ // T002 and T003 should be in affectedTasks as dependents/transitive
530
+ const ids = result.affectedTasks.map((t) => t.id);
531
+ expect(ids).toContain('T001');
532
+ expect(ids).toContain('T002');
533
+ expect(ids).toContain('T003');
534
+ // T004 is unrelated and should not appear
535
+ expect(ids).not.toContain('T004');
536
+ });
537
+
538
+ it('classifies exposure correctly: direct, dependent, transitive', async () => {
539
+ const tasks = [
540
+ makeTask({ id: 'T001', title: 'auth login service', description: '' }),
541
+ makeTask({ id: 'T002', title: 'Session management', description: '', depends: ['T001'] }),
542
+ makeTask({ id: 'T003', title: 'Profile page', description: '', depends: ['T002'] }),
543
+ ];
544
+ const acc = mockAccessor(tasks);
545
+
546
+ const result = await predictImpact('auth login', undefined, acc);
547
+
548
+ const t1 = result.affectedTasks.find((t) => t.id === 'T001');
549
+ const t2 = result.affectedTasks.find((t) => t.id === 'T002');
550
+ const t3 = result.affectedTasks.find((t) => t.id === 'T003');
551
+
552
+ expect(t1?.exposure).toBe('direct');
553
+ expect(t2?.exposure).toBe('dependent');
554
+ expect(t3?.exposure).toBe('transitive');
555
+ });
556
+
557
+ it('sorts affectedTasks: direct first, then dependent, then transitive', async () => {
558
+ const tasks = [
559
+ makeTask({ id: 'T001', title: 'auth service core', description: '' }),
560
+ makeTask({ id: 'T002', title: 'Login controller', description: '', depends: ['T001'] }),
561
+ makeTask({ id: 'T003', title: 'Session store', description: '', depends: ['T002'] }),
562
+ ];
563
+ const acc = mockAccessor(tasks);
564
+
565
+ const result = await predictImpact('auth service', undefined, acc);
566
+
567
+ const exposures = result.affectedTasks.map((t) => t.exposure);
568
+ // direct must come before dependent which must come before transitive
569
+ const directIdx = exposures.indexOf('direct');
570
+ const dependentIdx = exposures.indexOf('dependent');
571
+ const transitiveIdx = exposures.indexOf('transitive');
572
+
573
+ if (directIdx !== -1 && dependentIdx !== -1) {
574
+ expect(directIdx).toBeLessThan(dependentIdx);
575
+ }
576
+ if (dependentIdx !== -1 && transitiveIdx !== -1) {
577
+ expect(dependentIdx).toBeLessThan(transitiveIdx);
578
+ }
579
+ });
580
+
581
+ it('respects matchLimit parameter', async () => {
582
+ const tasks = [
583
+ makeTask({ id: 'T001', title: 'auth module setup', description: '' }),
584
+ makeTask({ id: 'T002', title: 'auth token generation', description: '' }),
585
+ makeTask({ id: 'T003', title: 'auth session management', description: '' }),
586
+ ];
587
+ const acc = mockAccessor(tasks);
588
+
589
+ // Limit to 1 seed
590
+ const result = await predictImpact('auth', undefined, acc, 1);
591
+
592
+ // Only 1 direct match seeded
593
+ expect(result.matchedTasks).toHaveLength(1);
594
+ });
595
+
596
+ it('produces a meaningful summary string', async () => {
597
+ const tasks = [
598
+ makeTask({ id: 'T001', title: 'auth service', description: '' }),
599
+ makeTask({ id: 'T002', title: 'Login page', description: '', depends: ['T001'] }),
600
+ ];
601
+ const acc = mockAccessor(tasks);
602
+
603
+ const result = await predictImpact('auth service', undefined, acc);
604
+
605
+ expect(result.summary).toContain('auth service');
606
+ expect(result.summary).toMatch(/\d+ task/);
607
+ });
608
+
609
+ it('downstreamCount is 0 for leaf tasks with no dependents', async () => {
610
+ const tasks = [makeTask({ id: 'T001', title: 'auth service core', description: '' })];
611
+ const acc = mockAccessor(tasks);
612
+
613
+ const result = await predictImpact('auth service', undefined, acc);
614
+
615
+ expect(result.affectedTasks[0]?.downstreamCount).toBe(0);
616
+ });
617
+ });
@@ -6,6 +6,7 @@
6
6
  * - Task impact assessment (direct + transitive dependents)
7
7
  * - Change impact prediction (cancel, block, complete, reprioritize)
8
8
  * - Blast radius calculation (scope quantification)
9
+ * - Free-text impact prediction (predictImpact) — T043
9
10
  *
10
11
  * @module intelligence
11
12
  */
@@ -22,6 +23,8 @@ import type {
22
23
  ChangeImpact,
23
24
  ChangeType,
24
25
  ImpactAssessment,
26
+ ImpactedTask,
27
+ ImpactReport,
25
28
  } from './types.js';
26
29
 
27
30
  // ============================================================================
@@ -636,3 +639,203 @@ function generateRecommendation(
636
639
  );
637
640
  }
638
641
  }
642
+
643
+ // ============================================================================
644
+ // Free-text Impact Prediction (T043)
645
+ // ============================================================================
646
+
647
+ /**
648
+ * Score a task against a change description using simple keyword matching.
649
+ *
650
+ * Normalises both strings to lowercase and counts overlapping tokens (words).
651
+ * Returns a score in [0, 1] — 1 meaning every non-trivial token in the change
652
+ * description was found in the task text.
653
+ */
654
+ function scoreTaskMatch(change: string, task: Task): number {
655
+ const STOP_WORDS = new Set([
656
+ 'a',
657
+ 'an',
658
+ 'the',
659
+ 'and',
660
+ 'or',
661
+ 'in',
662
+ 'of',
663
+ 'to',
664
+ 'for',
665
+ 'with',
666
+ 'on',
667
+ 'at',
668
+ 'by',
669
+ 'is',
670
+ 'it',
671
+ 'be',
672
+ 'as',
673
+ 'if',
674
+ 'do',
675
+ 'not',
676
+ ]);
677
+
678
+ const tokenise = (text: string): string[] =>
679
+ text
680
+ .toLowerCase()
681
+ .split(/\W+/)
682
+ .filter((t) => t.length > 2 && !STOP_WORDS.has(t));
683
+
684
+ const changeTokens = new Set(tokenise(change));
685
+ if (changeTokens.size === 0) return 0;
686
+
687
+ const taskText = `${task.title ?? ''} ${task.description ?? ''}`;
688
+ const taskTokens = new Set(tokenise(taskText));
689
+
690
+ let matches = 0;
691
+ for (const token of changeTokens) {
692
+ if (taskTokens.has(token)) matches++;
693
+ }
694
+
695
+ return matches / changeTokens.size;
696
+ }
697
+
698
+ /**
699
+ * Predict the downstream impact of a free-text change description.
700
+ *
701
+ * Uses fuzzy keyword matching to identify candidate tasks that relate to
702
+ * the change, then walks the reverse dependency graph to enumerate all
703
+ * downstream tasks that may be affected.
704
+ *
705
+ * @remarks
706
+ * The matching is purely lexical (no embeddings). Tasks are ranked by
707
+ * how many tokens from the change description appear in their title and
708
+ * description. The top `matchLimit` (default: 5) matched tasks are used
709
+ * as seeds for downstream dependency tracing.
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * import { predictImpact } from '@cleocode/core';
714
+ *
715
+ * const report = await predictImpact('Modify authentication flow', process.cwd());
716
+ * console.log(report.summary);
717
+ * // "3 tasks matched 'Modify authentication flow'; 7 downstream tasks affected."
718
+ * for (const task of report.affectedTasks) {
719
+ * console.log(`${task.id} (${task.exposure}): ${task.reason}`);
720
+ * }
721
+ * ```
722
+ *
723
+ * @param change - Free-text description of the proposed change (e.g. "Modify X")
724
+ * @param cwd - Working directory used to locate the tasks database
725
+ * @param accessor - Optional pre-created DataAccessor (useful in tests)
726
+ * @param matchLimit - Maximum number of seed tasks to match (default: 5)
727
+ * @returns Full impact prediction report
728
+ */
729
+ export async function predictImpact(
730
+ change: string,
731
+ cwd?: string,
732
+ accessor?: DataAccessor,
733
+ matchLimit = 5,
734
+ ): Promise<ImpactReport> {
735
+ const acc = accessor ?? (await getAccessor(cwd));
736
+ const tasks = await loadAllTasks(acc);
737
+ const taskMap = new Map(tasks.map((t) => [t.id, t]));
738
+ const dependentsMap = buildDependentsMap(tasks);
739
+
740
+ // --- Step 1: Score every task against the change description ---
741
+ const scored = tasks
742
+ .map((t) => ({ task: t, score: scoreTaskMatch(change, t) }))
743
+ .filter(({ score }) => score > 0)
744
+ .sort((a, b) => b.score - a.score);
745
+
746
+ const seeds = scored.slice(0, matchLimit).map(({ task }) => task);
747
+
748
+ if (seeds.length === 0) {
749
+ return {
750
+ change,
751
+ matchedTasks: [],
752
+ affectedTasks: [],
753
+ totalAffected: 0,
754
+ summary: `No tasks matched the change description "${change}".`,
755
+ };
756
+ }
757
+
758
+ // --- Step 2: Collect all affected task IDs via reverse dependency graph ---
759
+ const directMatchIds = new Set(seeds.map((t) => t.id));
760
+
761
+ // Map: taskId -> exposure level
762
+ const exposureMap = new Map<string, ImpactedTask['exposure']>();
763
+ for (const id of directMatchIds) {
764
+ exposureMap.set(id, 'direct');
765
+ }
766
+
767
+ // BFS over dependents for each seed
768
+ for (const seed of seeds) {
769
+ const transitive = collectTransitiveDependents(seed.id, dependentsMap);
770
+ for (const depId of transitive) {
771
+ if (!exposureMap.has(depId)) {
772
+ // Determine whether this is a direct dependent of the seed or further out
773
+ const isDirectDependent = (dependentsMap.get(seed.id) ?? new Set()).has(depId);
774
+ exposureMap.set(depId, isDirectDependent ? 'dependent' : 'transitive');
775
+ }
776
+ }
777
+ }
778
+
779
+ // --- Step 3: Build ImpactedTask list ---
780
+ const EXPOSURE_ORDER: Record<ImpactedTask['exposure'], number> = {
781
+ direct: 0,
782
+ dependent: 1,
783
+ transitive: 2,
784
+ };
785
+
786
+ const affectedTasks: ImpactedTask[] = [];
787
+
788
+ for (const [id, exposure] of exposureMap) {
789
+ const task = taskMap.get(id);
790
+ if (!task) continue;
791
+
792
+ const downstreamTransitive = collectTransitiveDependents(id, dependentsMap);
793
+ const downstreamCount = downstreamTransitive.size;
794
+
795
+ let reason: string;
796
+ if (exposure === 'direct') {
797
+ reason = `Task title/description matched "${change}".`;
798
+ } else if (exposure === 'dependent') {
799
+ const seedNames = seeds
800
+ .filter((s) => (dependentsMap.get(s.id) ?? new Set()).has(id))
801
+ .map((s) => s.id)
802
+ .join(', ');
803
+ reason = `Directly depends on matched task(s): ${seedNames}.`;
804
+ } else {
805
+ reason = 'Downstream of a matched task via transitive dependency chain.';
806
+ }
807
+
808
+ affectedTasks.push({
809
+ id,
810
+ title: task.title,
811
+ status: task.status,
812
+ priority: task.priority,
813
+ exposure,
814
+ downstreamCount,
815
+ reason,
816
+ });
817
+ }
818
+
819
+ // Sort: exposure order first, then descending downstream count
820
+ affectedTasks.sort((a, b) => {
821
+ const expDiff = EXPOSURE_ORDER[a.exposure] - EXPOSURE_ORDER[b.exposure];
822
+ if (expDiff !== 0) return expDiff;
823
+ return b.downstreamCount - a.downstreamCount;
824
+ });
825
+
826
+ const matchedTasks = affectedTasks.filter((t) => t.exposure === 'direct');
827
+ const totalAffected = affectedTasks.length;
828
+
829
+ const summary =
830
+ matchedTasks.length === 0
831
+ ? `No tasks matched "${change}".`
832
+ : `${matchedTasks.length} task(s) matched "${change}"; ${totalAffected} total task(s) affected (including downstream).`;
833
+
834
+ return {
835
+ change,
836
+ matchedTasks,
837
+ affectedTasks,
838
+ totalAffected,
839
+ summary,
840
+ };
841
+ }
@@ -30,6 +30,7 @@ export {
30
30
  analyzeChangeImpact,
31
31
  analyzeTaskImpact,
32
32
  calculateBlastRadius,
33
+ predictImpact,
33
34
  } from './impact.js';
34
35
  // Patterns
35
36
  export {
@@ -53,6 +54,8 @@ export type {
53
54
  ChangeType,
54
55
  DetectedPattern,
55
56
  ImpactAssessment,
57
+ ImpactedTask,
58
+ ImpactReport,
56
59
  LearningContext,
57
60
  PatternExtractionOptions,
58
61
  PatternMatch,
@@ -245,6 +245,82 @@ export interface AffectedTask {
245
245
  reason: string;
246
246
  }
247
247
 
248
+ // ============================================================================
249
+ // Impact Prediction (free-text change description)
250
+ // ============================================================================
251
+
252
+ /**
253
+ * A single task predicted to be affected by a free-text change description.
254
+ *
255
+ * Produced by {@link predictImpact} after matching candidate tasks against
256
+ * the change description and running downstream dependency analysis.
257
+ */
258
+ export interface ImpactedTask {
259
+ /** Task ID. */
260
+ id: string;
261
+
262
+ /** Task title. */
263
+ title: string;
264
+
265
+ /** Current task status. */
266
+ status: string;
267
+
268
+ /** Current task priority. */
269
+ priority: string;
270
+
271
+ /**
272
+ * Severity estimate for this task's exposure to the change.
273
+ *
274
+ * - `direct` — task title/description matched the change description
275
+ * - `dependent` — task depends on a matched task
276
+ * - `transitive` — downstream of a dependent via the dependency graph
277
+ */
278
+ exposure: 'direct' | 'dependent' | 'transitive';
279
+
280
+ /**
281
+ * Number of downstream tasks that depend on this task.
282
+ * Higher values indicate higher cascading risk.
283
+ */
284
+ downstreamCount: number;
285
+
286
+ /** Why this task is predicted to be affected. */
287
+ reason: string;
288
+ }
289
+
290
+ /**
291
+ * Full impact prediction report for a free-text change description.
292
+ *
293
+ * Returned by {@link predictImpact}. Combines fuzzy task search with
294
+ * reverse dependency analysis to enumerate which tasks are at risk.
295
+ */
296
+ export interface ImpactReport {
297
+ /** The original free-text change description. */
298
+ change: string;
299
+
300
+ /**
301
+ * Tasks directly matched by the change description (fuzzy search).
302
+ * These are the "seed" tasks from which downstream impact is traced.
303
+ */
304
+ matchedTasks: ImpactedTask[];
305
+
306
+ /**
307
+ * All tasks predicted to be affected, ordered by exposure severity
308
+ * (direct first, then dependents, then transitive) and then by
309
+ * descending downstream count.
310
+ */
311
+ affectedTasks: ImpactedTask[];
312
+
313
+ /**
314
+ * Total count of distinct affected tasks (including direct matches).
315
+ */
316
+ totalAffected: number;
317
+
318
+ /**
319
+ * Human-readable summary of predicted impact scope.
320
+ */
321
+ summary: string;
322
+ }
323
+
248
324
  // ============================================================================
249
325
  // Blast Radius
250
326
  // ============================================================================
package/src/internal.ts CHANGED
@@ -66,6 +66,7 @@ export {
66
66
  analyzeChangeImpact,
67
67
  analyzeTaskImpact,
68
68
  calculateBlastRadius,
69
+ predictImpact,
69
70
  } from './intelligence/impact.js';
70
71
  export {
71
72
  extractPatternsFromHistory,
@@ -87,6 +88,8 @@ export type {
87
88
  ChangeType,
88
89
  DetectedPattern,
89
90
  ImpactAssessment,
91
+ ImpactedTask,
92
+ ImpactReport,
90
93
  LearningContext,
91
94
  PatternExtractionOptions,
92
95
  PatternMatch,
@@ -98,6 +101,14 @@ export type {
98
101
 
99
102
  // Issue
100
103
  export { collectDiagnostics } from './issue/diagnostics.js';
104
+ // Lib — shared primitives
105
+ export {
106
+ computeDelay,
107
+ type RetryablePredicate,
108
+ type RetryContext,
109
+ type RetryOptions,
110
+ withRetry as withRetryShared,
111
+ } from './lib/retry.js';
101
112
  export {
102
113
  addChain,
103
114
  advanceInstance,
@@ -611,6 +622,7 @@ export type {
611
622
  AgentExecutionEvent,
612
623
  AgentExecutionOutcome,
613
624
  AgentHealthReport,
625
+ AgentHealthStatus,
614
626
  AgentPerformanceSummary,
615
627
  AgentRecoveryResult,
616
628
  CapacitySummary,
@@ -623,12 +635,17 @@ export type {
623
635
  } from './agents/index.js';
624
636
  // Agents — runtime registry, health, retry, capacity
625
637
  export {
638
+ // health-monitor functions (T039)
626
639
  checkAgentHealth,
640
+ // registry / capacity / retry
627
641
  classifyError,
628
642
  createRetryPolicy,
629
643
  DEFAULT_RETRY_POLICY,
630
644
  deregisterAgent,
645
+ detectCrashedAgents,
646
+ detectStaleAgents,
631
647
  findLeastLoadedAgent,
648
+ findStaleAgentRows,
632
649
  generateAgentId,
633
650
  getAgentErrorHistory,
634
651
  getAgentInstance,
@@ -637,6 +654,7 @@ export {
637
654
  getCapacitySummary,
638
655
  getHealthReport,
639
656
  getSelfHealingSuggestions,
657
+ HEARTBEAT_INTERVAL_MS,
640
658
  heartbeat,
641
659
  incrementTasksCompleted,
642
660
  isOverloaded,
@@ -645,8 +663,10 @@ export {
645
663
  processAgentLifecycleEvent,
646
664
  recordAgentExecution,
647
665
  recordFailurePattern,
666
+ recordHeartbeat,
648
667
  recoverCrashedAgents,
649
668
  registerAgent as registerAgentInstance,
669
+ STALE_THRESHOLD_MS,
650
670
  storeHealingStrategy,
651
671
  updateAgentStatus,
652
672
  updateCapacity,