@soleri/core 7.0.0 → 8.0.0

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 (210) hide show
  1. package/dist/agency/agency-manager.d.ts +27 -1
  2. package/dist/agency/agency-manager.d.ts.map +1 -1
  3. package/dist/agency/agency-manager.js +180 -9
  4. package/dist/agency/agency-manager.js.map +1 -1
  5. package/dist/agency/default-rules.d.ts +7 -0
  6. package/dist/agency/default-rules.d.ts.map +1 -0
  7. package/dist/agency/default-rules.js +79 -0
  8. package/dist/agency/default-rules.js.map +1 -0
  9. package/dist/agency/types.d.ts +48 -0
  10. package/dist/agency/types.d.ts.map +1 -1
  11. package/dist/brain/brain.d.ts +17 -2
  12. package/dist/brain/brain.d.ts.map +1 -1
  13. package/dist/brain/brain.js +118 -8
  14. package/dist/brain/brain.js.map +1 -1
  15. package/dist/brain/knowledge-synthesizer.d.ts +37 -0
  16. package/dist/brain/knowledge-synthesizer.d.ts.map +1 -0
  17. package/dist/brain/knowledge-synthesizer.js +161 -0
  18. package/dist/brain/knowledge-synthesizer.js.map +1 -0
  19. package/dist/brain/learning-radar.d.ts +96 -0
  20. package/dist/brain/learning-radar.d.ts.map +1 -0
  21. package/dist/brain/learning-radar.js +202 -0
  22. package/dist/brain/learning-radar.js.map +1 -0
  23. package/dist/brain/types.d.ts +15 -0
  24. package/dist/brain/types.d.ts.map +1 -1
  25. package/dist/context/context-engine.d.ts.map +1 -1
  26. package/dist/context/context-engine.js +82 -17
  27. package/dist/context/context-engine.js.map +1 -1
  28. package/dist/context/types.d.ts +5 -0
  29. package/dist/context/types.d.ts.map +1 -1
  30. package/dist/control/intent-router.d.ts +12 -1
  31. package/dist/control/intent-router.d.ts.map +1 -1
  32. package/dist/control/intent-router.js +68 -0
  33. package/dist/control/intent-router.js.map +1 -1
  34. package/dist/control/types.d.ts +17 -0
  35. package/dist/control/types.d.ts.map +1 -1
  36. package/dist/curator/classifier.d.ts +18 -0
  37. package/dist/curator/classifier.d.ts.map +1 -0
  38. package/dist/curator/classifier.js +61 -0
  39. package/dist/curator/classifier.js.map +1 -0
  40. package/dist/curator/quality-gate.d.ts +29 -0
  41. package/dist/curator/quality-gate.d.ts.map +1 -0
  42. package/dist/curator/quality-gate.js +88 -0
  43. package/dist/curator/quality-gate.js.map +1 -0
  44. package/dist/engine/bin/soleri-engine.js +1 -0
  45. package/dist/engine/bin/soleri-engine.js.map +1 -1
  46. package/dist/events/event-bus.d.ts +30 -0
  47. package/dist/events/event-bus.d.ts.map +1 -0
  48. package/dist/events/event-bus.js +51 -0
  49. package/dist/events/event-bus.js.map +1 -0
  50. package/dist/flows/chain-runner.d.ts +46 -0
  51. package/dist/flows/chain-runner.d.ts.map +1 -0
  52. package/dist/flows/chain-runner.js +271 -0
  53. package/dist/flows/chain-runner.js.map +1 -0
  54. package/dist/flows/chain-types.d.ts +103 -0
  55. package/dist/flows/chain-types.d.ts.map +1 -0
  56. package/dist/flows/chain-types.js +23 -0
  57. package/dist/flows/chain-types.js.map +1 -0
  58. package/dist/health/doctor-checks.d.ts +15 -0
  59. package/dist/health/doctor-checks.d.ts.map +1 -0
  60. package/dist/health/doctor-checks.js +98 -0
  61. package/dist/health/doctor-checks.js.map +1 -0
  62. package/dist/intake/text-ingester.d.ts +52 -0
  63. package/dist/intake/text-ingester.d.ts.map +1 -0
  64. package/dist/intake/text-ingester.js +181 -0
  65. package/dist/intake/text-ingester.js.map +1 -0
  66. package/dist/llm/llm-client.d.ts.map +1 -1
  67. package/dist/llm/llm-client.js +37 -1
  68. package/dist/llm/llm-client.js.map +1 -1
  69. package/dist/llm/oauth-discovery.d.ts +26 -0
  70. package/dist/llm/oauth-discovery.d.ts.map +1 -0
  71. package/dist/llm/oauth-discovery.js +149 -0
  72. package/dist/llm/oauth-discovery.js.map +1 -0
  73. package/dist/planning/evidence-collector.d.ts +41 -0
  74. package/dist/planning/evidence-collector.d.ts.map +1 -0
  75. package/dist/planning/evidence-collector.js +194 -0
  76. package/dist/planning/evidence-collector.js.map +1 -0
  77. package/dist/planning/planner.d.ts +4 -0
  78. package/dist/planning/planner.d.ts.map +1 -1
  79. package/dist/planning/planner.js +11 -0
  80. package/dist/planning/planner.js.map +1 -1
  81. package/dist/queue/job-queue.d.ts +92 -0
  82. package/dist/queue/job-queue.d.ts.map +1 -0
  83. package/dist/queue/job-queue.js +180 -0
  84. package/dist/queue/job-queue.js.map +1 -0
  85. package/dist/queue/pipeline-runner.d.ts +62 -0
  86. package/dist/queue/pipeline-runner.d.ts.map +1 -0
  87. package/dist/queue/pipeline-runner.js +126 -0
  88. package/dist/queue/pipeline-runner.js.map +1 -0
  89. package/dist/runtime/admin-setup-ops.d.ts +20 -0
  90. package/dist/runtime/admin-setup-ops.d.ts.map +1 -0
  91. package/dist/runtime/admin-setup-ops.js +583 -0
  92. package/dist/runtime/admin-setup-ops.js.map +1 -0
  93. package/dist/runtime/chain-ops.d.ts +9 -0
  94. package/dist/runtime/chain-ops.d.ts.map +1 -0
  95. package/dist/runtime/chain-ops.js +107 -0
  96. package/dist/runtime/chain-ops.js.map +1 -0
  97. package/dist/runtime/claude-md-helpers.d.ts +65 -0
  98. package/dist/runtime/claude-md-helpers.d.ts.map +1 -0
  99. package/dist/runtime/claude-md-helpers.js +173 -0
  100. package/dist/runtime/claude-md-helpers.js.map +1 -0
  101. package/dist/runtime/curator-extra-ops.d.ts +3 -2
  102. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  103. package/dist/runtime/curator-extra-ops.js +81 -3
  104. package/dist/runtime/curator-extra-ops.js.map +1 -1
  105. package/dist/runtime/facades/admin-facade.d.ts.map +1 -1
  106. package/dist/runtime/facades/admin-facade.js +4 -0
  107. package/dist/runtime/facades/admin-facade.js.map +1 -1
  108. package/dist/runtime/facades/agency-facade.d.ts.map +1 -1
  109. package/dist/runtime/facades/agency-facade.js +64 -0
  110. package/dist/runtime/facades/agency-facade.js.map +1 -1
  111. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  112. package/dist/runtime/facades/brain-facade.js +122 -1
  113. package/dist/runtime/facades/brain-facade.js.map +1 -1
  114. package/dist/runtime/facades/control-facade.d.ts.map +1 -1
  115. package/dist/runtime/facades/control-facade.js +42 -0
  116. package/dist/runtime/facades/control-facade.js.map +1 -1
  117. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  118. package/dist/runtime/facades/memory-facade.js +20 -2
  119. package/dist/runtime/facades/memory-facade.js.map +1 -1
  120. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  121. package/dist/runtime/facades/plan-facade.js +2 -0
  122. package/dist/runtime/facades/plan-facade.js.map +1 -1
  123. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  124. package/dist/runtime/facades/vault-facade.js +25 -5
  125. package/dist/runtime/facades/vault-facade.js.map +1 -1
  126. package/dist/runtime/intake-ops.d.ts +7 -5
  127. package/dist/runtime/intake-ops.d.ts.map +1 -1
  128. package/dist/runtime/intake-ops.js +98 -5
  129. package/dist/runtime/intake-ops.js.map +1 -1
  130. package/dist/runtime/memory-extra-ops.d.ts +6 -3
  131. package/dist/runtime/memory-extra-ops.d.ts.map +1 -1
  132. package/dist/runtime/memory-extra-ops.js +292 -4
  133. package/dist/runtime/memory-extra-ops.js.map +1 -1
  134. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  135. package/dist/runtime/planning-extra-ops.js +85 -0
  136. package/dist/runtime/planning-extra-ops.js.map +1 -1
  137. package/dist/runtime/playbook-ops.js +1 -1
  138. package/dist/runtime/playbook-ops.js.map +1 -1
  139. package/dist/runtime/runtime.d.ts.map +1 -1
  140. package/dist/runtime/runtime.js +143 -2
  141. package/dist/runtime/runtime.js.map +1 -1
  142. package/dist/runtime/session-briefing.d.ts +23 -0
  143. package/dist/runtime/session-briefing.d.ts.map +1 -0
  144. package/dist/runtime/session-briefing.js +140 -0
  145. package/dist/runtime/session-briefing.js.map +1 -0
  146. package/dist/runtime/types.d.ts +23 -0
  147. package/dist/runtime/types.d.ts.map +1 -1
  148. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  149. package/dist/runtime/vault-linking-ops.js +1 -3
  150. package/dist/runtime/vault-linking-ops.js.map +1 -1
  151. package/dist/vault/vault.d.ts +25 -0
  152. package/dist/vault/vault.d.ts.map +1 -1
  153. package/dist/vault/vault.js +67 -3
  154. package/dist/vault/vault.js.map +1 -1
  155. package/package.json +1 -1
  156. package/src/__tests__/admin-setup-ops.test.ts +355 -0
  157. package/src/__tests__/async-infrastructure.test.ts +307 -0
  158. package/src/__tests__/cognee-client-gaps.test.ts +6 -2
  159. package/src/__tests__/cognee-hybrid-search.test.ts +49 -35
  160. package/src/__tests__/cognee-sync-manager-deep.test.ts +89 -65
  161. package/src/__tests__/curator-extra-ops.test.ts +6 -2
  162. package/src/__tests__/curator-pipeline-e2e.test.ts +358 -0
  163. package/src/__tests__/memory-extra-ops.test.ts +2 -2
  164. package/src/__tests__/planning-extra-ops.test.ts +2 -2
  165. package/src/__tests__/second-brain-features.test.ts +583 -0
  166. package/src/agency/agency-manager.ts +217 -9
  167. package/src/agency/default-rules.ts +83 -0
  168. package/src/agency/types.ts +61 -0
  169. package/src/brain/brain.ts +110 -8
  170. package/src/brain/knowledge-synthesizer.ts +218 -0
  171. package/src/brain/learning-radar.ts +340 -0
  172. package/src/brain/types.ts +16 -0
  173. package/src/context/context-engine.ts +114 -15
  174. package/src/context/types.ts +5 -0
  175. package/src/control/intent-router.ts +107 -0
  176. package/src/control/types.ts +10 -0
  177. package/src/curator/classifier.ts +88 -0
  178. package/src/curator/quality-gate.ts +129 -0
  179. package/src/engine/bin/soleri-engine.ts +1 -0
  180. package/src/events/event-bus.ts +58 -0
  181. package/src/flows/chain-runner.ts +369 -0
  182. package/src/flows/chain-types.ts +57 -0
  183. package/src/health/doctor-checks.ts +115 -0
  184. package/src/intake/text-ingester.ts +234 -0
  185. package/src/llm/llm-client.ts +38 -1
  186. package/src/llm/oauth-discovery.ts +169 -0
  187. package/src/planning/evidence-collector.ts +247 -0
  188. package/src/planning/planner.ts +11 -0
  189. package/src/queue/job-queue.ts +281 -0
  190. package/src/queue/pipeline-runner.ts +149 -0
  191. package/src/runtime/admin-setup-ops.ts +664 -0
  192. package/src/runtime/chain-ops.ts +121 -0
  193. package/src/runtime/claude-md-helpers.ts +236 -0
  194. package/src/runtime/curator-extra-ops.ts +86 -3
  195. package/src/runtime/facades/admin-facade.ts +4 -0
  196. package/src/runtime/facades/agency-facade.ts +68 -0
  197. package/src/runtime/facades/brain-facade.ts +142 -1
  198. package/src/runtime/facades/control-facade.ts +45 -0
  199. package/src/runtime/facades/memory-facade.ts +20 -2
  200. package/src/runtime/facades/plan-facade.ts +2 -0
  201. package/src/runtime/facades/vault-facade.ts +28 -5
  202. package/src/runtime/intake-ops.ts +107 -5
  203. package/src/runtime/memory-extra-ops.ts +312 -4
  204. package/src/runtime/planning-extra-ops.ts +94 -0
  205. package/src/runtime/playbook-ops.ts +1 -1
  206. package/src/runtime/runtime.ts +138 -2
  207. package/src/runtime/session-briefing.ts +161 -0
  208. package/src/runtime/types.ts +23 -0
  209. package/src/runtime/vault-linking-ops.ts +1 -3
  210. package/src/vault/vault.ts +79 -4
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Text Ingester — ingest articles, transcripts, and plain text into the vault.
3
+ *
4
+ * Reuses existing content-classifier (LLM extraction) and dedup-gate (TF-IDF).
5
+ * No new dependencies — fetch() is built-in, HTML stripping is regex-based.
6
+ */
7
+
8
+ import type { Vault } from '../vault/vault.js';
9
+ import type { LLMClient } from '../llm/llm-client.js';
10
+ import type { IntelligenceEntry } from '../intelligence/types.js';
11
+ import type { ClassifiedItem } from './types.js';
12
+ import { classifyChunk } from './content-classifier.js';
13
+ import { dedupItems } from './dedup-gate.js';
14
+
15
+ // ─── Types ───────────────────────────────────────────────────────────
16
+
17
+ export interface IngestSource {
18
+ type: 'article' | 'transcript' | 'notes' | 'documentation';
19
+ title: string;
20
+ url?: string;
21
+ author?: string;
22
+ }
23
+
24
+ export interface IngestOptions {
25
+ domain?: string;
26
+ tags?: string[];
27
+ /** Max chars per chunk for LLM classification. Default 4000. */
28
+ chunkSize?: number;
29
+ }
30
+
31
+ export interface IngestResult {
32
+ source: IngestSource;
33
+ ingested: number;
34
+ duplicates: number;
35
+ entries: Array<{ id: string; title: string; type: string }>;
36
+ }
37
+
38
+ // ─── Constants ───────────────────────────────────────────────────────
39
+
40
+ const DEFAULT_CHUNK_SIZE = 4000;
41
+ const FETCH_TIMEOUT_MS = 15000;
42
+
43
+ // ─── Class ───────────────────────────────────────────────────────────
44
+
45
+ export class TextIngester {
46
+ private vault: Vault;
47
+ private llm: LLMClient | null;
48
+
49
+ constructor(vault: Vault, llm: LLMClient | null) {
50
+ this.vault = vault;
51
+ this.llm = llm;
52
+ }
53
+
54
+ /**
55
+ * Ingest a URL — fetch, strip HTML, classify, dedup, store.
56
+ */
57
+ async ingestUrl(url: string, opts?: IngestOptions): Promise<IngestResult> {
58
+ if (!this.llm) {
59
+ return { source: { type: 'article', title: url }, ingested: 0, duplicates: 0, entries: [] };
60
+ }
61
+
62
+ let text: string;
63
+ let title = url;
64
+ try {
65
+ const response = await fetch(url, {
66
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
67
+ headers: { 'User-Agent': 'Soleri/1.0 (knowledge ingestion)' },
68
+ });
69
+ if (!response.ok) {
70
+ return { source: { type: 'article', title }, ingested: 0, duplicates: 0, entries: [] };
71
+ }
72
+ const html = await response.text();
73
+ title = extractTitle(html) ?? url;
74
+ text = stripHtml(html);
75
+ } catch {
76
+ return { source: { type: 'article', title }, ingested: 0, duplicates: 0, entries: [] };
77
+ }
78
+
79
+ if (text.length < 50) {
80
+ return { source: { type: 'article', title }, ingested: 0, duplicates: 0, entries: [] };
81
+ }
82
+
83
+ const source: IngestSource = { type: 'article', title, url };
84
+ return this.ingestText(text, source, opts);
85
+ }
86
+
87
+ /**
88
+ * Ingest raw text — classify, dedup, store.
89
+ */
90
+ async ingestText(
91
+ text: string,
92
+ source: IngestSource,
93
+ opts?: IngestOptions,
94
+ ): Promise<IngestResult> {
95
+ if (!this.llm) {
96
+ return { source, ingested: 0, duplicates: 0, entries: [] };
97
+ }
98
+
99
+ const chunkSize = opts?.chunkSize ?? DEFAULT_CHUNK_SIZE;
100
+ const chunks = splitIntoChunks(text, chunkSize);
101
+ const domain = opts?.domain ?? 'general';
102
+ const extraTags = opts?.tags ?? [];
103
+
104
+ // Classify all chunks
105
+ const allItems: ClassifiedItem[] = [];
106
+ for (const chunk of chunks) {
107
+ const items = await classifyChunk(this.llm, chunk, `${source.type}: ${source.title}`);
108
+ allItems.push(...items);
109
+ }
110
+
111
+ if (allItems.length === 0) {
112
+ return { source, ingested: 0, duplicates: 0, entries: [] };
113
+ }
114
+
115
+ // Dedup against vault
116
+ const dedupResults = dedupItems(allItems, this.vault);
117
+ const unique = dedupResults.filter((r) => !r.isDuplicate).map((r) => r.item);
118
+ const duplicateCount = dedupResults.filter((r) => r.isDuplicate).length;
119
+
120
+ // Build source attribution for context field
121
+ const attribution = buildAttribution(source);
122
+
123
+ // Store in vault
124
+ const entries: IntelligenceEntry[] = unique.map((item, i) => ({
125
+ id: `ingest-${source.type}-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 6)}`,
126
+ type: mapType(item.type),
127
+ domain,
128
+ title: item.title,
129
+ description: item.description,
130
+ severity: mapSeverity(item.severity),
131
+ tags: [...(item.tags ?? []), ...extraTags, 'ingested', source.type],
132
+ context: attribution,
133
+ origin: 'user' as const,
134
+ }));
135
+
136
+ if (entries.length > 0) {
137
+ this.vault.seed(entries);
138
+ }
139
+
140
+ return {
141
+ source,
142
+ ingested: entries.length,
143
+ duplicates: duplicateCount,
144
+ entries: entries.map((e) => ({ id: e.id, title: e.title, type: e.type })),
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Ingest multiple items in sequence.
150
+ */
151
+ async ingestBatch(
152
+ items: Array<{ text: string; source: IngestSource; opts?: IngestOptions }>,
153
+ ): Promise<IngestResult[]> {
154
+ const results: IngestResult[] = [];
155
+ for (const item of items) {
156
+ const result = await this.ingestText(item.text, item.source, item.opts);
157
+ results.push(result);
158
+ }
159
+ return results;
160
+ }
161
+ }
162
+
163
+ // ─── Helpers ─────────────────────────────────────────────────────────
164
+
165
+ function stripHtml(html: string): string {
166
+ return (
167
+ html
168
+ // Remove script and style blocks
169
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
170
+ .replace(/<style[\s\S]*?<\/style>/gi, '')
171
+ // Remove nav, header, footer, aside
172
+ .replace(/<(nav|header|footer|aside)[\s\S]*?<\/\1>/gi, '')
173
+ // Remove all HTML tags
174
+ .replace(/<[^>]+>/g, ' ')
175
+ // Decode common entities
176
+ .replace(/&amp;/g, '&')
177
+ .replace(/&lt;/g, '<')
178
+ .replace(/&gt;/g, '>')
179
+ .replace(/&quot;/g, '"')
180
+ .replace(/&#39;/g, "'")
181
+ .replace(/&nbsp;/g, ' ')
182
+ // Collapse whitespace
183
+ .replace(/\s+/g, ' ')
184
+ .trim()
185
+ );
186
+ }
187
+
188
+ function extractTitle(html: string): string | null {
189
+ const match = html.match(/<title[^>]*>(.*?)<\/title>/i);
190
+ if (match) {
191
+ return match[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').trim();
192
+ }
193
+ return null;
194
+ }
195
+
196
+ function splitIntoChunks(text: string, chunkSize: number): string[] {
197
+ if (text.length <= chunkSize) return [text];
198
+
199
+ const chunks: string[] = [];
200
+ let start = 0;
201
+ while (start < text.length) {
202
+ let end = start + chunkSize;
203
+ // Try to break at a sentence boundary
204
+ if (end < text.length) {
205
+ const lastPeriod = text.lastIndexOf('. ', end);
206
+ if (lastPeriod > start + chunkSize * 0.5) {
207
+ end = lastPeriod + 2;
208
+ }
209
+ }
210
+ chunks.push(text.slice(start, end).trim());
211
+ start = end;
212
+ }
213
+ return chunks.filter((c) => c.length > 0);
214
+ }
215
+
216
+ function buildAttribution(source: IngestSource): string {
217
+ const parts = [`Source: ${source.type}`];
218
+ if (source.title) parts.push(`Title: ${source.title}`);
219
+ if (source.url) parts.push(`URL: ${source.url}`);
220
+ if (source.author) parts.push(`Author: ${source.author}`);
221
+ return parts.join(' | ');
222
+ }
223
+
224
+ function mapType(type: string): IntelligenceEntry['type'] {
225
+ if (type === 'pattern') return 'pattern';
226
+ if (type === 'anti-pattern') return 'anti-pattern';
227
+ return 'rule';
228
+ }
229
+
230
+ function mapSeverity(severity: string | undefined): IntelligenceEntry['severity'] {
231
+ if (severity === 'critical') return 'critical';
232
+ if (severity === 'warning') return 'warning';
233
+ return 'suggestion';
234
+ }
@@ -25,8 +25,45 @@ const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
25
25
  // =============================================================================
26
26
 
27
27
  function loadRoutingConfig(agentId: string): RoutingConfig {
28
+ // Default task→model routing: cheap models for routine, powerful for reasoning.
29
+ // Anthropic routes use extended thinking for quality decisions when available.
30
+ // Agents can override via ~/.{agentId}/model-routing.json.
31
+ const defaultRoutes: RouteEntry[] = [
32
+ // OpenAI routes (default — works without Anthropic key)
33
+ { caller: 'quality-gate', task: 'evaluate', model: 'gpt-4o', provider: 'openai' },
34
+ { caller: 'classifier', task: 'classify', model: 'gpt-4o-mini', provider: 'openai' },
35
+ { caller: 'knowledge-synthesizer', task: 'synthesize', model: 'gpt-4o', provider: 'openai' },
36
+ { caller: 'content-classifier', model: 'gpt-4o-mini', provider: 'openai' },
37
+ { caller: 'vault-linking', task: 'evaluate-links', model: 'gpt-4o-mini', provider: 'openai' },
38
+ // Anthropic routes (higher quality when key available — extended thinking capable)
39
+ {
40
+ caller: 'quality-gate-anthropic',
41
+ task: 'evaluate',
42
+ model: 'claude-sonnet-4-20250514',
43
+ provider: 'anthropic',
44
+ },
45
+ {
46
+ caller: 'contradiction-evaluator',
47
+ task: 'evaluate',
48
+ model: 'claude-sonnet-4-20250514',
49
+ provider: 'anthropic',
50
+ },
51
+ {
52
+ caller: 'knowledge-synthesizer-anthropic',
53
+ task: 'synthesize',
54
+ model: 'claude-sonnet-4-20250514',
55
+ provider: 'anthropic',
56
+ },
57
+ {
58
+ caller: 'classifier-anthropic',
59
+ task: 'classify',
60
+ model: 'claude-haiku-4-5-20251001',
61
+ provider: 'anthropic',
62
+ },
63
+ ];
64
+
28
65
  const defaultConfig: RoutingConfig = {
29
- routes: [],
66
+ routes: defaultRoutes,
30
67
  defaultOpenAIModel: 'gpt-4o-mini',
31
68
  defaultAnthropicModel: 'claude-sonnet-4-20250514',
32
69
  };
@@ -0,0 +1,169 @@
1
+ /**
2
+ * OAuth Token Discovery — find Claude Code OAuth tokens on macOS and Linux.
3
+ *
4
+ * Priority:
5
+ * 1. ANTHROPIC_API_KEY env var (explicit, highest priority)
6
+ * 2. Claude Code credentials file (~/.claude/.credentials.json or similar)
7
+ * 3. macOS Keychain (security find-generic-password)
8
+ * 4. Linux GNOME Keyring (secret-tool lookup)
9
+ * 5. null (graceful fallback → use OpenAI or no LLM)
10
+ *
11
+ * Cached for 5 minutes to avoid repeated I/O.
12
+ */
13
+
14
+ import { execFileSync } from 'node:child_process';
15
+ import { readFileSync, existsSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { homedir, platform } from 'node:os';
18
+
19
+ // ─── Cache ───────────────────────────────────────────────────────────
20
+
21
+ let cachedToken: string | null = null;
22
+ let cacheTimestamp = 0;
23
+ const CACHE_TTL_MS = 5 * 60 * 1000;
24
+
25
+ // ─── Public API ──────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * Discover an Anthropic API token. Returns null if none found.
29
+ * Results cached for 5 minutes.
30
+ */
31
+ export function discoverAnthropicToken(): string | null {
32
+ if (cachedToken && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
33
+ return cachedToken;
34
+ }
35
+
36
+ const token = tryEnvVar() ?? tryCredentialsFile() ?? tryPlatformKeychain();
37
+
38
+ if (token) {
39
+ cachedToken = token;
40
+ cacheTimestamp = Date.now();
41
+ }
42
+
43
+ return token;
44
+ }
45
+
46
+ /**
47
+ * Clear the cached token (for testing or rotation).
48
+ */
49
+ export function resetTokenCache(): void {
50
+ cachedToken = null;
51
+ cacheTimestamp = 0;
52
+ }
53
+
54
+ /**
55
+ * Get discovery source info (for diagnostics).
56
+ */
57
+ export function getTokenSource(): string {
58
+ if (process.env.ANTHROPIC_API_KEY) return 'env:ANTHROPIC_API_KEY';
59
+ if (tryCredentialsFile()) return 'file:credentials';
60
+ if (tryPlatformKeychain()) return `keychain:${platform()}`;
61
+ return 'none';
62
+ }
63
+
64
+ // ─── Discovery Methods ───────────────────────────────────────────────
65
+
66
+ function tryEnvVar(): string | null {
67
+ return process.env.ANTHROPIC_API_KEY ?? null;
68
+ }
69
+
70
+ function tryCredentialsFile(): string | null {
71
+ const candidates = [
72
+ join(homedir(), '.claude', '.credentials.json'),
73
+ join(homedir(), '.claude', 'credentials.json'),
74
+ join(homedir(), '.config', 'claude', 'credentials.json'),
75
+ ];
76
+
77
+ for (const path of candidates) {
78
+ try {
79
+ if (!existsSync(path)) continue;
80
+ const raw = readFileSync(path, 'utf-8');
81
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
82
+
83
+ // Claude Code OAuth format: { claudeAiOauth: { accessToken: "..." } }
84
+ const oauth = parsed.claudeAiOauth as Record<string, unknown> | undefined;
85
+ if (oauth?.accessToken && typeof oauth.accessToken === 'string') {
86
+ return oauth.accessToken;
87
+ }
88
+
89
+ // Alternative: direct token field
90
+ if (parsed.accessToken && typeof parsed.accessToken === 'string') {
91
+ return parsed.accessToken as string;
92
+ }
93
+
94
+ // Alternative: API key field
95
+ if (parsed.apiKey && typeof parsed.apiKey === 'string') {
96
+ return parsed.apiKey as string;
97
+ }
98
+ } catch {
99
+ continue;
100
+ }
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ function tryPlatformKeychain(): string | null {
107
+ const os = platform();
108
+
109
+ if (os === 'darwin') return tryMacKeychain();
110
+ if (os === 'linux') return tryLinuxKeyring();
111
+
112
+ return null;
113
+ }
114
+
115
+ function tryMacKeychain(): string | null {
116
+ try {
117
+ const raw = execFileSync(
118
+ 'security',
119
+ ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],
120
+ { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
121
+ ).trim();
122
+
123
+ if (!raw) return null;
124
+
125
+ // Try JSON parse
126
+ try {
127
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
128
+ const oauth = parsed.claudeAiOauth as Record<string, unknown> | undefined;
129
+ if (oauth?.accessToken && typeof oauth.accessToken === 'string') {
130
+ return oauth.accessToken;
131
+ }
132
+ } catch {
133
+ // JSON might be truncated — try regex fallback
134
+ const match = raw.match(/"accessToken"\s*:\s*"([^"]+)"/);
135
+ if (match) return match[1];
136
+ }
137
+ } catch {
138
+ // Keychain not available or no entry
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ function tryLinuxKeyring(): string | null {
145
+ try {
146
+ // GNOME Keyring via secret-tool
147
+ const token = execFileSync(
148
+ 'secret-tool',
149
+ ['lookup', 'service', 'Claude Code', 'type', 'credentials'],
150
+ { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
151
+ ).trim();
152
+
153
+ if (token) {
154
+ // May be JSON or raw token
155
+ try {
156
+ const parsed = JSON.parse(token) as Record<string, unknown>;
157
+ const oauth = parsed.claudeAiOauth as Record<string, unknown> | undefined;
158
+ if (oauth?.accessToken) return oauth.accessToken as string;
159
+ } catch {
160
+ // Treat as raw token
161
+ if (token.length > 20) return token;
162
+ }
163
+ }
164
+ } catch {
165
+ // secret-tool not available or no entry
166
+ }
167
+
168
+ return null;
169
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Evidence Collector — cross-references plan tasks against git reality.
3
+ *
4
+ * Runs `git diff` to find what actually changed, then matches file changes
5
+ * against planned tasks to produce an evidence-based drift report.
6
+ */
7
+
8
+ import { execFileSync } from 'node:child_process';
9
+ import type { Plan, PlanTask } from './planner.js';
10
+
11
+ export interface FileChange {
12
+ path: string;
13
+ status: 'added' | 'modified' | 'deleted' | 'renamed';
14
+ }
15
+
16
+ export interface GitTaskEvidence {
17
+ taskId: string;
18
+ taskTitle: string;
19
+ plannedStatus: string;
20
+ matchedFiles: FileChange[];
21
+ verdict: 'DONE' | 'PARTIAL' | 'MISSING' | 'SKIPPED';
22
+ }
23
+
24
+ export interface UnplannedChange {
25
+ file: FileChange;
26
+ possibleReason: string;
27
+ }
28
+
29
+ export interface EvidenceReport {
30
+ planId: string;
31
+ planObjective: string;
32
+ accuracy: number;
33
+ evidenceSources: string[];
34
+ taskEvidence: GitTaskEvidence[];
35
+ unplannedChanges: UnplannedChange[];
36
+ missingWork: GitTaskEvidence[];
37
+ summary: string;
38
+ }
39
+
40
+ /**
41
+ * Collect git diff evidence for a plan.
42
+ *
43
+ * @param plan - The plan to verify
44
+ * @param projectPath - Project root (must be a git repo)
45
+ * @param baseBranch - Compare against this branch (default: 'main')
46
+ */
47
+ export function collectGitEvidence(
48
+ plan: Plan,
49
+ projectPath: string,
50
+ baseBranch: string = 'main',
51
+ ): EvidenceReport {
52
+ const fileChanges = getGitDiff(projectPath, baseBranch);
53
+ const taskEvidence: GitTaskEvidence[] = [];
54
+ const matchedFiles = new Set<string>();
55
+
56
+ for (const task of plan.tasks) {
57
+ const matches = findMatchingFiles(task, fileChanges);
58
+ for (const m of matches) matchedFiles.add(m.path);
59
+
60
+ const verdict = determineVerdict(task, matches);
61
+ taskEvidence.push({
62
+ taskId: task.id,
63
+ taskTitle: task.title,
64
+ plannedStatus: task.status,
65
+ matchedFiles: matches,
66
+ verdict,
67
+ });
68
+ }
69
+
70
+ const unplannedChanges: UnplannedChange[] = fileChanges
71
+ .filter((f) => !matchedFiles.has(f.path))
72
+ .map((f) => ({
73
+ file: f,
74
+ possibleReason: inferReason(f),
75
+ }));
76
+
77
+ const missingWork = taskEvidence.filter((te) => te.verdict === 'MISSING');
78
+
79
+ const totalTasks = taskEvidence.length;
80
+ const doneTasks = taskEvidence.filter((te) => te.verdict === 'DONE').length;
81
+ const partialTasks = taskEvidence.filter((te) => te.verdict === 'PARTIAL').length;
82
+ const skippedTasks = taskEvidence.filter((te) => te.verdict === 'SKIPPED').length;
83
+ const accuracy =
84
+ totalTasks > 0
85
+ ? Math.round(((doneTasks + partialTasks * 0.5 + skippedTasks * 0.25) / totalTasks) * 100)
86
+ : 100;
87
+
88
+ const summary = buildSummary(
89
+ totalTasks,
90
+ doneTasks,
91
+ partialTasks,
92
+ missingWork.length,
93
+ unplannedChanges.length,
94
+ );
95
+
96
+ return {
97
+ planId: plan.id,
98
+ planObjective: plan.objective,
99
+ accuracy,
100
+ evidenceSources: ['git'],
101
+ taskEvidence,
102
+ unplannedChanges,
103
+ missingWork,
104
+ summary,
105
+ };
106
+ }
107
+
108
+ function getGitDiff(projectPath: string, baseBranch: string): FileChange[] {
109
+ try {
110
+ const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
111
+ cwd: projectPath,
112
+ encoding: 'utf-8',
113
+ timeout: 5000,
114
+ }).trim();
115
+
116
+ const diffTarget = currentBranch === baseBranch ? 'HEAD~10' : baseBranch;
117
+
118
+ let output: string;
119
+ try {
120
+ output = execFileSync('git', ['diff', '--name-status', `${diffTarget}...HEAD`], {
121
+ cwd: projectPath,
122
+ encoding: 'utf-8',
123
+ timeout: 10000,
124
+ });
125
+ } catch {
126
+ output = execFileSync('git', ['diff', '--name-status', 'HEAD~5'], {
127
+ cwd: projectPath,
128
+ encoding: 'utf-8',
129
+ timeout: 10000,
130
+ });
131
+ }
132
+
133
+ return output
134
+ .trim()
135
+ .split('\n')
136
+ .filter((line) => line.length > 0)
137
+ .map(parseGitDiffLine)
138
+ .filter((f): f is FileChange => f !== null);
139
+ } catch {
140
+ return [];
141
+ }
142
+ }
143
+
144
+ function parseGitDiffLine(line: string): FileChange | null {
145
+ const match = line.match(/^([AMDRC])\d*\t(.+?)(?:\t(.+))?$/);
146
+ if (!match) return null;
147
+
148
+ const statusChar = match[1];
149
+ const path = match[3] ?? match[2];
150
+
151
+ const statusMap: Record<string, FileChange['status']> = {
152
+ A: 'added',
153
+ M: 'modified',
154
+ D: 'deleted',
155
+ R: 'renamed',
156
+ C: 'added',
157
+ };
158
+
159
+ return { path, status: statusMap[statusChar] ?? 'modified' };
160
+ }
161
+
162
+ function findMatchingFiles(task: PlanTask, files: FileChange[]): FileChange[] {
163
+ const keywords = extractKeywords(task.title + ' ' + task.description);
164
+ if (keywords.length === 0) return [];
165
+
166
+ return files.filter((f) => {
167
+ const pathLower = f.path.toLowerCase();
168
+ return keywords.some((kw) => pathLower.includes(kw));
169
+ });
170
+ }
171
+
172
+ function extractKeywords(text: string): string[] {
173
+ const stopWords = new Set([
174
+ 'the',
175
+ 'and',
176
+ 'for',
177
+ 'with',
178
+ 'that',
179
+ 'this',
180
+ 'from',
181
+ 'into',
182
+ 'add',
183
+ 'create',
184
+ 'implement',
185
+ 'update',
186
+ 'fix',
187
+ 'remove',
188
+ 'delete',
189
+ 'new',
190
+ 'use',
191
+ 'should',
192
+ 'must',
193
+ 'will',
194
+ 'can',
195
+ 'all',
196
+ 'each',
197
+ 'when',
198
+ 'not',
199
+ 'are',
200
+ 'has',
201
+ 'have',
202
+ 'been',
203
+ 'was',
204
+ ]);
205
+
206
+ const words = text
207
+ .toLowerCase()
208
+ .replace(/[^a-z0-9\s\-_/.]/g, ' ')
209
+ .split(/[\s\-_/]+/)
210
+ .filter((w) => w.length >= 3 && !stopWords.has(w));
211
+
212
+ return [...new Set(words)];
213
+ }
214
+
215
+ function determineVerdict(task: PlanTask, matches: FileChange[]): GitTaskEvidence['verdict'] {
216
+ if (task.status === 'skipped') return 'SKIPPED';
217
+ if (matches.length === 0) return 'MISSING';
218
+ if (task.status === 'completed') return 'DONE';
219
+ if (matches.length > 0) return 'PARTIAL';
220
+ return 'MISSING';
221
+ }
222
+
223
+ function inferReason(file: FileChange): string {
224
+ const path = file.path.toLowerCase();
225
+ if (path.includes('index.') || path.includes('barrel')) return 'likely re-export update';
226
+ if (path.includes('config') || path.includes('.env')) return 'configuration change';
227
+ if (path.includes('test') || path.includes('spec')) return 'test file';
228
+ if (path.includes('package.json') || path.includes('lock')) return 'dependency update';
229
+ if (path.includes('readme') || path.includes('.md')) return 'documentation';
230
+ if (path.includes('types') || path.includes('.d.ts')) return 'type definition update';
231
+ return 'unplanned scope';
232
+ }
233
+
234
+ function buildSummary(
235
+ total: number,
236
+ done: number,
237
+ partial: number,
238
+ missing: number,
239
+ unplanned: number,
240
+ ): string {
241
+ const parts: string[] = [];
242
+ parts.push(`${done}/${total} tasks verified by git evidence`);
243
+ if (partial > 0) parts.push(`${partial} partially done`);
244
+ if (missing > 0) parts.push(`${missing} with no file evidence`);
245
+ if (unplanned > 0) parts.push(`${unplanned} unplanned file changes`);
246
+ return parts.join(', ');
247
+ }