autonomous-flow-daemon 1.1.0 → 1.6.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * Semantic Diff Engine — AST-based change classification for TypeScript/JavaScript.
3
+ *
4
+ * Instead of line-by-line text diff, parses both old and new source into AST,
5
+ * extracts top-level declarations, and classifies changes as:
6
+ * - signature-change (breaking)
7
+ * - body-change (non-breaking)
8
+ * - added / removed
9
+ * - renamed
10
+ * - comment-only
11
+ *
12
+ * Falls back to text diff for non-TS/JS files.
13
+ */
14
+
15
+ import ts from "typescript";
16
+
17
+ export type ChangeKind =
18
+ | "signature-change" // function/class signature modified (breaking)
19
+ | "body-change" // implementation changed, signature intact
20
+ | "added" // new declaration
21
+ | "removed" // declaration deleted
22
+ | "export-change" // export modifier added/removed
23
+ | "comment-only" // only comments changed
24
+ | "whitespace-only" // only formatting changed
25
+ | "type-change" // type annotation changed
26
+ | "unknown";
27
+
28
+ export interface SemanticChange {
29
+ kind: ChangeKind;
30
+ name: string; // declaration name
31
+ nodeType: string; // "function", "class", "interface", "variable", etc.
32
+ breaking: boolean; // true if this could break consumers
33
+ detail?: string; // human-readable description
34
+ }
35
+
36
+ export interface SemanticDiffResult {
37
+ changes: SemanticChange[];
38
+ hasBreakingChanges: boolean;
39
+ summary: string; // one-line summary for logging
40
+ }
41
+
42
+ interface DeclInfo {
43
+ name: string;
44
+ nodeType: string;
45
+ signature: string; // type signature (no body)
46
+ body: string; // full text including body
47
+ exported: boolean;
48
+ hasComments: boolean;
49
+ }
50
+
51
+ /** Check if a file path is TypeScript or JavaScript */
52
+ export function isAstSupported(filePath: string): boolean {
53
+ return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
54
+ }
55
+
56
+ /** Generate semantic diff between old and new source */
57
+ export function semanticDiff(filePath: string, oldSource: string, newSource: string): SemanticDiffResult {
58
+ if (!isAstSupported(filePath)) {
59
+ return textFallback(oldSource, newSource);
60
+ }
61
+
62
+ try {
63
+ const oldDecls = extractDeclarations(filePath, oldSource);
64
+ const newDecls = extractDeclarations(filePath, newSource);
65
+ return compareDeclarations(oldDecls, newDecls);
66
+ } catch {
67
+ // AST parsing failed — fall back to text diff
68
+ return textFallback(oldSource, newSource);
69
+ }
70
+ }
71
+
72
+ function extractDeclarations(filePath: string, source: string): Map<string, DeclInfo> {
73
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
74
+ const decls = new Map<string, DeclInfo>();
75
+
76
+ for (const stmt of sf.statements) {
77
+ const infos = extractDeclInfos(stmt, source);
78
+ for (const info of infos) {
79
+ decls.set(info.name, info);
80
+ }
81
+ }
82
+
83
+ return decls;
84
+ }
85
+
86
+ function extractDeclInfos(node: ts.Statement, _source: string): DeclInfo[] {
87
+ const exported = hasExportModifier(node);
88
+ const fullText = node.getFullText().trim();
89
+ const hasComments = /^\s*\/[/*]/.test(node.getFullText());
90
+
91
+ if (ts.isFunctionDeclaration(node) && node.name) {
92
+ return [{
93
+ name: node.name.text,
94
+ nodeType: "function",
95
+ signature: getFunctionSignature(node),
96
+ body: fullText,
97
+ exported,
98
+ hasComments,
99
+ }];
100
+ }
101
+
102
+ if (ts.isClassDeclaration(node) && node.name) {
103
+ return [{
104
+ name: node.name.text,
105
+ nodeType: "class",
106
+ signature: getClassSignature(node),
107
+ body: fullText,
108
+ exported,
109
+ hasComments,
110
+ }];
111
+ }
112
+
113
+ if (ts.isInterfaceDeclaration(node)) {
114
+ return [{
115
+ name: node.name.text,
116
+ nodeType: "interface",
117
+ signature: normalizeWhitespace(node.getText()),
118
+ body: fullText,
119
+ exported,
120
+ hasComments,
121
+ }];
122
+ }
123
+
124
+ if (ts.isTypeAliasDeclaration(node)) {
125
+ return [{
126
+ name: node.name.text,
127
+ nodeType: "type",
128
+ signature: normalizeWhitespace(node.getText()),
129
+ body: fullText,
130
+ exported,
131
+ hasComments,
132
+ }];
133
+ }
134
+
135
+ if (ts.isEnumDeclaration(node)) {
136
+ return [{
137
+ name: node.name.text,
138
+ nodeType: "enum",
139
+ signature: normalizeWhitespace(node.getText()),
140
+ body: fullText,
141
+ exported,
142
+ hasComments,
143
+ }];
144
+ }
145
+
146
+ if (ts.isVariableStatement(node)) {
147
+ const results: DeclInfo[] = [];
148
+ for (const decl of node.declarationList.declarations) {
149
+ if (ts.isIdentifier(decl.name)) {
150
+ const typeStr = decl.type ? `: ${decl.type.getText()}` : "";
151
+ const isArrowFn = decl.initializer && ts.isArrowFunction(decl.initializer);
152
+ results.push({
153
+ name: decl.name.text,
154
+ nodeType: isArrowFn ? "function" : "variable",
155
+ signature: `${decl.name.text}${typeStr}`,
156
+ body: fullText,
157
+ exported,
158
+ hasComments,
159
+ });
160
+ }
161
+ }
162
+ return results;
163
+ }
164
+
165
+ if (ts.isImportDeclaration(node)) {
166
+ const importText = normalizeWhitespace(node.getText());
167
+ return [{
168
+ name: `import:${importText}`,
169
+ nodeType: "import",
170
+ signature: importText,
171
+ body: fullText,
172
+ exported: false,
173
+ hasComments,
174
+ }];
175
+ }
176
+
177
+ if (ts.isExportDeclaration(node)) {
178
+ const exportText = normalizeWhitespace(node.getText());
179
+ return [{
180
+ name: `export:${exportText}`,
181
+ nodeType: "export",
182
+ signature: exportText,
183
+ body: fullText,
184
+ exported: true,
185
+ hasComments,
186
+ }];
187
+ }
188
+
189
+ return [];
190
+ }
191
+
192
+ function compareDeclarations(
193
+ oldDecls: Map<string, DeclInfo>,
194
+ newDecls: Map<string, DeclInfo>,
195
+ ): SemanticDiffResult {
196
+ const changes: SemanticChange[] = [];
197
+
198
+ // Check removed declarations
199
+ for (const [name, oldInfo] of oldDecls) {
200
+ if (!newDecls.has(name)) {
201
+ changes.push({
202
+ kind: "removed",
203
+ name,
204
+ nodeType: oldInfo.nodeType,
205
+ breaking: oldInfo.exported,
206
+ detail: oldInfo.exported ? `exported ${oldInfo.nodeType} removed` : `${oldInfo.nodeType} removed`,
207
+ });
208
+ }
209
+ }
210
+
211
+ // Check added and modified declarations
212
+ for (const [name, newInfo] of newDecls) {
213
+ const oldInfo = oldDecls.get(name);
214
+
215
+ if (!oldInfo) {
216
+ changes.push({
217
+ kind: "added",
218
+ name,
219
+ nodeType: newInfo.nodeType,
220
+ breaking: false,
221
+ detail: `new ${newInfo.nodeType}`,
222
+ });
223
+ continue;
224
+ }
225
+
226
+ // Same full text → no change
227
+ if (normalizeWhitespace(oldInfo.body) === normalizeWhitespace(newInfo.body)) {
228
+ continue;
229
+ }
230
+
231
+ // Check export modifier change
232
+ if (oldInfo.exported !== newInfo.exported) {
233
+ changes.push({
234
+ kind: "export-change",
235
+ name,
236
+ nodeType: newInfo.nodeType,
237
+ breaking: oldInfo.exported && !newInfo.exported, // removing export is breaking
238
+ detail: newInfo.exported ? "export added" : "export removed",
239
+ });
240
+ continue;
241
+ }
242
+
243
+ // Check if only comments changed
244
+ const oldNoComments = stripComments(oldInfo.body);
245
+ const newNoComments = stripComments(newInfo.body);
246
+ if (normalizeWhitespace(oldNoComments) === normalizeWhitespace(newNoComments)) {
247
+ changes.push({
248
+ kind: "comment-only",
249
+ name,
250
+ nodeType: oldInfo.nodeType,
251
+ breaking: false,
252
+ detail: "comments modified",
253
+ });
254
+ continue;
255
+ }
256
+
257
+ // Check if only whitespace changed
258
+ if (oldNoComments.replace(/\s+/g, "") === newNoComments.replace(/\s+/g, "")) {
259
+ changes.push({
260
+ kind: "whitespace-only",
261
+ name,
262
+ nodeType: oldInfo.nodeType,
263
+ breaking: false,
264
+ detail: "formatting changed",
265
+ });
266
+ continue;
267
+ }
268
+
269
+ // Signature changed?
270
+ if (oldInfo.signature !== newInfo.signature) {
271
+ // Check if it's just a type annotation change
272
+ const oldSigNoType = stripTypeAnnotations(oldInfo.signature);
273
+ const newSigNoType = stripTypeAnnotations(newInfo.signature);
274
+ if (oldSigNoType !== newSigNoType) {
275
+ changes.push({
276
+ kind: "signature-change",
277
+ name,
278
+ nodeType: oldInfo.nodeType,
279
+ breaking: oldInfo.exported,
280
+ detail: `${oldInfo.signature} → ${newInfo.signature}`,
281
+ });
282
+ } else {
283
+ changes.push({
284
+ kind: "type-change",
285
+ name,
286
+ nodeType: oldInfo.nodeType,
287
+ breaking: oldInfo.exported,
288
+ detail: "type annotation changed",
289
+ });
290
+ }
291
+ continue;
292
+ }
293
+
294
+ // Signature same, body different → body-only change
295
+ changes.push({
296
+ kind: "body-change",
297
+ name,
298
+ nodeType: oldInfo.nodeType,
299
+ breaking: false,
300
+ detail: "implementation changed",
301
+ });
302
+ }
303
+
304
+ const hasBreakingChanges = changes.some(c => c.breaking);
305
+ const summary = buildSummary(changes);
306
+
307
+ return { changes, hasBreakingChanges, summary };
308
+ }
309
+
310
+ function buildSummary(changes: SemanticChange[]): string {
311
+ if (changes.length === 0) return "no semantic changes";
312
+
313
+ const counts: Record<string, number> = {};
314
+ for (const c of changes) {
315
+ counts[c.kind] = (counts[c.kind] ?? 0) + 1;
316
+ }
317
+
318
+ const parts: string[] = [];
319
+ for (const [kind, count] of Object.entries(counts)) {
320
+ parts.push(`${count} ${kind}`);
321
+ }
322
+
323
+ const breaking = changes.filter(c => c.breaking).length;
324
+ if (breaking > 0) parts.push(`⚠️ ${breaking} breaking`);
325
+
326
+ return parts.join(", ");
327
+ }
328
+
329
+ // ── Helpers ──
330
+
331
+ function hasExportModifier(node: ts.Statement): boolean {
332
+ if (!ts.canHaveModifiers(node)) return false;
333
+ const mods = ts.getModifiers(node);
334
+ return mods?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
335
+ }
336
+
337
+ function getFunctionSignature(node: ts.FunctionDeclaration): string {
338
+ const name = node.name?.text ?? "anonymous";
339
+ const params = node.parameters.map(p => {
340
+ const pName = p.name.getText();
341
+ const pType = p.type ? `: ${p.type.getText()}` : "";
342
+ const optional = p.questionToken ? "?" : "";
343
+ return `${pName}${optional}${pType}`;
344
+ }).join(", ");
345
+ const ret = node.type ? `: ${node.type.getText()}` : "";
346
+ return `${name}(${params})${ret}`;
347
+ }
348
+
349
+ function getClassSignature(node: ts.ClassDeclaration): string {
350
+ const name = node.name?.text ?? "anonymous";
351
+ const heritage = node.heritageClauses?.map(h => h.getText()).join(" ") ?? "";
352
+ const members: string[] = [];
353
+ for (const member of node.members) {
354
+ if (ts.isPropertyDeclaration(member) || ts.isMethodDeclaration(member)) {
355
+ const mName = member.name?.getText() ?? "";
356
+ if (ts.isMethodDeclaration(member)) {
357
+ const params = member.parameters.map(p => p.getText()).join(", ");
358
+ const ret = member.type ? `: ${member.type.getText()}` : "";
359
+ members.push(`${mName}(${params})${ret}`);
360
+ } else {
361
+ const type = (member as ts.PropertyDeclaration).type?.getText() ?? "";
362
+ members.push(`${mName}: ${type}`);
363
+ }
364
+ }
365
+ }
366
+ return `class ${name} ${heritage} { ${members.join("; ")} }`;
367
+ }
368
+
369
+ /** Strip type annotations from a signature, handling nested braces/parens/generics */
370
+ function stripTypeAnnotations(sig: string): string {
371
+ let result = "";
372
+ let depth = 0;
373
+ let inType = false;
374
+ for (let i = 0; i < sig.length; i++) {
375
+ const ch = sig[i];
376
+ if (ch === ":" && depth === 0 && !inType) {
377
+ inType = true;
378
+ continue;
379
+ }
380
+ if (inType) {
381
+ if (ch === "<" || ch === "{" || ch === "(") depth++;
382
+ else if (ch === ">" || ch === "}" || ch === ")") {
383
+ if (depth > 0) { depth--; continue; }
384
+ // depth === 0 and closing paren → end of type, keep the paren
385
+ inType = false;
386
+ result += ch;
387
+ } else if (ch === "," && depth === 0) {
388
+ inType = false;
389
+ result += ch;
390
+ }
391
+ continue;
392
+ }
393
+ result += ch;
394
+ }
395
+ return result;
396
+ }
397
+
398
+ function normalizeWhitespace(s: string): string {
399
+ return s.replace(/\s+/g, " ").trim();
400
+ }
401
+
402
+ function stripComments(source: string): string {
403
+ return source
404
+ .replace(/\/\*[\s\S]*?\*\//g, "")
405
+ .replace(/\/\/.*$/gm, "")
406
+ .trim();
407
+ }
408
+
409
+ /** Fallback for non-TS/JS files */
410
+ function textFallback(oldSource: string, newSource: string): SemanticDiffResult {
411
+ const oldLines = oldSource.split("\n");
412
+ const newLines = newSource.split("\n");
413
+ let added = 0, removed = 0, changed = 0;
414
+ const maxLen = Math.max(oldLines.length, newLines.length);
415
+
416
+ for (let i = 0; i < maxLen; i++) {
417
+ if (i >= oldLines.length) { added++; continue; }
418
+ if (i >= newLines.length) { removed++; continue; }
419
+ if (oldLines[i] !== newLines[i]) changed++;
420
+ }
421
+
422
+ const changes: SemanticChange[] = [];
423
+ if (added > 0) changes.push({ kind: "added", name: `${added} lines`, nodeType: "text", breaking: false });
424
+ if (removed > 0) changes.push({ kind: "removed", name: `${removed} lines`, nodeType: "text", breaking: false });
425
+ if (changed > 0) changes.push({ kind: "body-change", name: `${changed} lines`, nodeType: "text", breaking: false });
426
+
427
+ return {
428
+ changes,
429
+ hasBreakingChanges: false,
430
+ summary: `text diff: +${added} -${removed} ~${changed} lines`,
431
+ };
432
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Lightweight telemetry helpers for CLI-side tracking.
3
+ * Uses a short-lived DB connection — no daemon required.
4
+ */
5
+
6
+ import { Database } from "bun:sqlite";
7
+ import { resolveWorkspacePaths } from "../constants";
8
+ import { existsSync } from "fs";
9
+
10
+ function openDb(): Database | null {
11
+ try {
12
+ const paths = resolveWorkspacePaths();
13
+ if (!existsSync(paths.dbFile)) return null;
14
+ const db = new Database(paths.dbFile);
15
+ db.exec("PRAGMA journal_mode = WAL");
16
+ return db;
17
+ } catch { return null; }
18
+ }
19
+
20
+ /** Track a CLI command invocation (fire-and-forget). */
21
+ export function trackCliCommand(command: string) {
22
+ const db = openDb();
23
+ if (!db) return;
24
+ try {
25
+ db.prepare("INSERT INTO telemetry (category, action, timestamp) VALUES (?, ?, ?)").run("cli", command, Date.now());
26
+ } catch { /* table may not exist yet — ignore */ }
27
+ finally { db.close(); }
28
+ }
29
+
30
+ export interface TelemetryRow {
31
+ category: string;
32
+ action: string;
33
+ detail: string | null;
34
+ duration_ms: number | null;
35
+ timestamp: number;
36
+ }
37
+
38
+ export interface TelemetrySummary {
39
+ cli: Record<string, number>;
40
+ mcp: Record<string, number>;
41
+ seam: { counts: Record<string, number>; avgDurationMs: Record<string, number> };
42
+ immune: Record<string, number>;
43
+ validator: Record<string, number>;
44
+ totalEvents: number;
45
+ periodDays: number;
46
+ }
47
+
48
+ /** Query aggregated telemetry for the last N days. */
49
+ export function queryTelemetry(days: number): TelemetrySummary {
50
+ const db = openDb();
51
+ const empty: TelemetrySummary = { cli: {}, mcp: {}, seam: { counts: {}, avgDurationMs: {} }, immune: {}, validator: {}, totalEvents: 0, periodDays: days };
52
+ if (!db) return empty;
53
+
54
+ try {
55
+ const since = Date.now() - days * 86_400_000;
56
+ const rows = db.prepare(
57
+ "SELECT category, action, duration_ms FROM telemetry WHERE timestamp >= ?"
58
+ ).all(since) as { category: string; action: string; duration_ms: number | null }[];
59
+
60
+ const result = { ...empty };
61
+ const seamDurations: Record<string, number[]> = {};
62
+
63
+ for (const row of rows) {
64
+ result.totalEvents++;
65
+ switch (row.category) {
66
+ case "cli":
67
+ result.cli[row.action] = (result.cli[row.action] ?? 0) + 1;
68
+ break;
69
+ case "mcp":
70
+ result.mcp[row.action] = (result.mcp[row.action] ?? 0) + 1;
71
+ break;
72
+ case "seam":
73
+ result.seam.counts[row.action] = (result.seam.counts[row.action] ?? 0) + 1;
74
+ if (row.duration_ms != null) {
75
+ (seamDurations[row.action] ??= []).push(row.duration_ms);
76
+ }
77
+ break;
78
+ case "immune":
79
+ result.immune[row.action] = (result.immune[row.action] ?? 0) + 1;
80
+ break;
81
+ case "validator":
82
+ result.validator[row.action] = (result.validator[row.action] ?? 0) + 1;
83
+ break;
84
+ }
85
+ }
86
+
87
+ for (const [action, durations] of Object.entries(seamDurations)) {
88
+ result.seam.avgDurationMs[action] = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length);
89
+ }
90
+
91
+ return result;
92
+ } catch { return empty; }
93
+ finally { db.close(); }
94
+ }