@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9

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 (224) hide show
  1. package/dist/bot/agent-loop.d.ts +20 -0
  2. package/dist/bot/agent-loop.d.ts.map +1 -0
  3. package/dist/bot/agent-loop.js +331 -0
  4. package/dist/bot/agent-loop.js.map +1 -0
  5. package/dist/bot/agent-provider.d.ts.map +1 -1
  6. package/dist/bot/agent-provider.js +3 -2
  7. package/dist/bot/agent-provider.js.map +1 -1
  8. package/dist/bot/approvals.js +17 -8
  9. package/dist/bot/approvals.js.map +1 -1
  10. package/dist/bot/assistant-core.d.ts +17 -0
  11. package/dist/bot/assistant-core.d.ts.map +1 -1
  12. package/dist/bot/assistant-core.js +418 -60
  13. package/dist/bot/assistant-core.js.map +1 -1
  14. package/dist/bot/assistant-tools.d.ts +1 -1
  15. package/dist/bot/assistant-tools.d.ts.map +1 -1
  16. package/dist/bot/assistant-tools.js +283 -9
  17. package/dist/bot/assistant-tools.js.map +1 -1
  18. package/dist/bot/bot-agent-channel.d.ts.map +1 -1
  19. package/dist/bot/bot-agent-channel.js +2 -0
  20. package/dist/bot/bot-agent-channel.js.map +1 -1
  21. package/dist/bot/bot-manager.d.ts +4 -0
  22. package/dist/bot/bot-manager.d.ts.map +1 -1
  23. package/dist/bot/bot-manager.js +72 -27
  24. package/dist/bot/bot-manager.js.map +1 -1
  25. package/dist/bot/conversation-store.d.ts +6 -5
  26. package/dist/bot/conversation-store.d.ts.map +1 -1
  27. package/dist/bot/conversation-store.js +98 -42
  28. package/dist/bot/conversation-store.js.map +1 -1
  29. package/dist/bot/cost-store.d.ts +3 -0
  30. package/dist/bot/cost-store.d.ts.map +1 -1
  31. package/dist/bot/cost-store.js +21 -10
  32. package/dist/bot/cost-store.js.map +1 -1
  33. package/dist/bot/cost-tracker.d.ts.map +1 -1
  34. package/dist/bot/cost-tracker.js +14 -1
  35. package/dist/bot/cost-tracker.js.map +1 -1
  36. package/dist/bot/cron-parser.d.ts.map +1 -1
  37. package/dist/bot/cron-parser.js +2 -0
  38. package/dist/bot/cron-parser.js.map +1 -1
  39. package/dist/bot/cron-scheduler.d.ts.map +1 -1
  40. package/dist/bot/cron-scheduler.js +1 -0
  41. package/dist/bot/cron-scheduler.js.map +1 -1
  42. package/dist/bot/device-connection.d.ts +13 -0
  43. package/dist/bot/device-connection.d.ts.map +1 -0
  44. package/dist/bot/device-connection.js +102 -0
  45. package/dist/bot/device-connection.js.map +1 -0
  46. package/dist/bot/error-classifier.d.ts.map +1 -1
  47. package/dist/bot/error-classifier.js +5 -0
  48. package/dist/bot/error-classifier.js.map +1 -1
  49. package/dist/bot/file-lock.d.ts.map +1 -1
  50. package/dist/bot/file-lock.js +13 -3
  51. package/dist/bot/file-lock.js.map +1 -1
  52. package/dist/bot/file-watcher.d.ts.map +1 -1
  53. package/dist/bot/file-watcher.js +1 -0
  54. package/dist/bot/file-watcher.js.map +1 -1
  55. package/dist/bot/genesis-prompt-context.d.ts +5 -0
  56. package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
  57. package/dist/bot/genesis-prompt-context.js +55 -0
  58. package/dist/bot/genesis-prompt-context.js.map +1 -1
  59. package/dist/bot/genesis-store.d.ts +4 -0
  60. package/dist/bot/genesis-store.d.ts.map +1 -1
  61. package/dist/bot/genesis-store.js +79 -12
  62. package/dist/bot/genesis-store.js.map +1 -1
  63. package/dist/bot/improve-loop.d.ts +46 -0
  64. package/dist/bot/improve-loop.d.ts.map +1 -0
  65. package/dist/bot/improve-loop.js +592 -0
  66. package/dist/bot/improve-loop.js.map +1 -0
  67. package/dist/bot/insight-engine.d.ts +12 -0
  68. package/dist/bot/insight-engine.d.ts.map +1 -0
  69. package/dist/bot/insight-engine.js +256 -0
  70. package/dist/bot/insight-engine.js.map +1 -0
  71. package/dist/bot/knowledge-store.d.ts.map +1 -1
  72. package/dist/bot/knowledge-store.js +4 -1
  73. package/dist/bot/knowledge-store.js.map +1 -1
  74. package/dist/bot/pipeline-runner.d.ts.map +1 -1
  75. package/dist/bot/pipeline-runner.js +12 -4
  76. package/dist/bot/pipeline-runner.js.map +1 -1
  77. package/dist/bot/project-model.d.ts +25 -0
  78. package/dist/bot/project-model.d.ts.map +1 -0
  79. package/dist/bot/project-model.js +372 -0
  80. package/dist/bot/project-model.js.map +1 -0
  81. package/dist/bot/response-formatter.js +2 -3
  82. package/dist/bot/response-formatter.js.map +1 -1
  83. package/dist/bot/run-store.d.ts.map +1 -1
  84. package/dist/bot/run-store.js +10 -2
  85. package/dist/bot/run-store.js.map +1 -1
  86. package/dist/bot/safe-path.d.ts +1 -1
  87. package/dist/bot/safe-path.d.ts.map +1 -1
  88. package/dist/bot/safe-path.js +20 -1
  89. package/dist/bot/safe-path.js.map +1 -1
  90. package/dist/bot/safety.d.ts +10 -2
  91. package/dist/bot/safety.d.ts.map +1 -1
  92. package/dist/bot/safety.js +45 -2
  93. package/dist/bot/safety.js.map +1 -1
  94. package/dist/bot/session-state.d.ts +4 -0
  95. package/dist/bot/session-state.d.ts.map +1 -1
  96. package/dist/bot/session-state.js +52 -9
  97. package/dist/bot/session-state.js.map +1 -1
  98. package/dist/bot/slash-commands.d.ts.map +1 -1
  99. package/dist/bot/slash-commands.js +109 -3
  100. package/dist/bot/slash-commands.js.map +1 -1
  101. package/dist/bot/steering-engine.d.ts +67 -0
  102. package/dist/bot/steering-engine.d.ts.map +1 -0
  103. package/dist/bot/steering-engine.js +198 -0
  104. package/dist/bot/steering-engine.js.map +1 -0
  105. package/dist/bot/step-executor.d.ts.map +1 -1
  106. package/dist/bot/step-executor.js +62 -25
  107. package/dist/bot/step-executor.js.map +1 -1
  108. package/dist/bot/system-prompt.d.ts.map +1 -1
  109. package/dist/bot/system-prompt.js +5 -2
  110. package/dist/bot/system-prompt.js.map +1 -1
  111. package/dist/bot/task-queue.d.ts +6 -1
  112. package/dist/bot/task-queue.d.ts.map +1 -1
  113. package/dist/bot/task-queue.js +43 -4
  114. package/dist/bot/task-queue.js.map +1 -1
  115. package/dist/bot/tool-registry.d.ts +1 -1
  116. package/dist/bot/tool-registry.d.ts.map +1 -1
  117. package/dist/bot/tool-registry.js +65 -4
  118. package/dist/bot/tool-registry.js.map +1 -1
  119. package/dist/bot/trust-calculator.d.ts +34 -0
  120. package/dist/bot/trust-calculator.d.ts.map +1 -0
  121. package/dist/bot/trust-calculator.js +67 -0
  122. package/dist/bot/trust-calculator.js.map +1 -0
  123. package/dist/bot/types.d.ts +97 -0
  124. package/dist/bot/types.d.ts.map +1 -1
  125. package/dist/bot/update-checker.d.ts +21 -0
  126. package/dist/bot/update-checker.d.ts.map +1 -0
  127. package/dist/bot/update-checker.js +129 -0
  128. package/dist/bot/update-checker.js.map +1 -0
  129. package/dist/bot/weaver-tools.d.ts.map +1 -1
  130. package/dist/bot/weaver-tools.js +11 -4
  131. package/dist/bot/weaver-tools.js.map +1 -1
  132. package/dist/cli-bridge.d.ts +2 -0
  133. package/dist/cli-bridge.d.ts.map +1 -1
  134. package/dist/cli-bridge.js +3 -1
  135. package/dist/cli-bridge.js.map +1 -1
  136. package/dist/cli-handlers.d.ts +10 -1
  137. package/dist/cli-handlers.d.ts.map +1 -1
  138. package/dist/cli-handlers.js +141 -24
  139. package/dist/cli-handlers.js.map +1 -1
  140. package/dist/cli.d.ts +3 -0
  141. package/dist/cli.d.ts.map +1 -0
  142. package/dist/cli.js +749 -0
  143. package/dist/cli.js.map +1 -0
  144. package/dist/docs/weaver-config.md +15 -9
  145. package/dist/handlers/on-execution-completed.d.ts +11 -0
  146. package/dist/handlers/on-execution-completed.d.ts.map +1 -0
  147. package/dist/handlers/on-execution-completed.js +25 -0
  148. package/dist/handlers/on-execution-completed.js.map +1 -0
  149. package/dist/mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp-tools.js +33 -0
  151. package/dist/mcp-tools.js.map +1 -1
  152. package/dist/node-types/genesis-approve.d.ts.map +1 -1
  153. package/dist/node-types/genesis-approve.js +28 -3
  154. package/dist/node-types/genesis-approve.js.map +1 -1
  155. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  156. package/dist/node-types/genesis-observe.js +23 -13
  157. package/dist/node-types/genesis-observe.js.map +1 -1
  158. package/dist/node-types/genesis-propose.d.ts.map +1 -1
  159. package/dist/node-types/genesis-propose.js +8 -0
  160. package/dist/node-types/genesis-propose.js.map +1 -1
  161. package/dist/node-types/genesis-update-history.d.ts.map +1 -1
  162. package/dist/node-types/genesis-update-history.js +13 -0
  163. package/dist/node-types/genesis-update-history.js.map +1 -1
  164. package/dist/templates/weaver-template.d.ts +11 -0
  165. package/dist/templates/weaver-template.d.ts.map +1 -0
  166. package/dist/templates/weaver-template.js +53 -0
  167. package/dist/templates/weaver-template.js.map +1 -0
  168. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  169. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  170. package/dist/workflows/weaver-bot-session.js +68 -0
  171. package/dist/workflows/weaver-bot-session.js.map +1 -0
  172. package/dist/workflows/weaver.d.ts +24 -0
  173. package/dist/workflows/weaver.d.ts.map +1 -0
  174. package/dist/workflows/weaver.js +28 -0
  175. package/dist/workflows/weaver.js.map +1 -0
  176. package/flowweaver.manifest.json +28 -1
  177. package/package.json +6 -3
  178. package/src/bot/agent-provider.ts +3 -2
  179. package/src/bot/approvals.ts +16 -8
  180. package/src/bot/assistant-core.ts +420 -63
  181. package/src/bot/assistant-tools.ts +291 -9
  182. package/src/bot/bot-agent-channel.ts +2 -0
  183. package/src/bot/bot-manager.ts +70 -29
  184. package/src/bot/conversation-store.ts +87 -42
  185. package/src/bot/cost-store.ts +20 -9
  186. package/src/bot/cost-tracker.ts +13 -1
  187. package/src/bot/cron-parser.ts +1 -0
  188. package/src/bot/cron-scheduler.ts +1 -0
  189. package/src/bot/device-connection.ts +102 -0
  190. package/src/bot/error-classifier.ts +5 -0
  191. package/src/bot/file-lock.ts +12 -2
  192. package/src/bot/file-watcher.ts +1 -0
  193. package/src/bot/genesis-prompt-context.ts +61 -0
  194. package/src/bot/genesis-store.ts +68 -16
  195. package/src/bot/improve-loop.ts +651 -0
  196. package/src/bot/insight-engine.ts +273 -0
  197. package/src/bot/knowledge-store.ts +4 -1
  198. package/src/bot/pipeline-runner.ts +11 -6
  199. package/src/bot/project-model.ts +404 -0
  200. package/src/bot/response-formatter.ts +2 -3
  201. package/src/bot/run-store.ts +5 -2
  202. package/src/bot/safe-path.ts +20 -1
  203. package/src/bot/safety.ts +57 -3
  204. package/src/bot/session-state.ts +47 -7
  205. package/src/bot/slash-commands.ts +111 -3
  206. package/src/bot/steering-engine.ts +233 -0
  207. package/src/bot/step-executor.ts +66 -26
  208. package/src/bot/system-prompt.ts +5 -2
  209. package/src/bot/task-queue.ts +40 -4
  210. package/src/bot/tool-registry.ts +67 -5
  211. package/src/bot/trust-calculator.ts +87 -0
  212. package/src/bot/types.ts +104 -0
  213. package/src/bot/update-checker.ts +138 -0
  214. package/src/bot/weaver-tools.ts +10 -4
  215. package/src/cli-bridge.ts +4 -1
  216. package/src/cli-handlers.ts +150 -29
  217. package/src/handlers/on-execution-completed.ts +30 -0
  218. package/src/mcp-tools.ts +38 -0
  219. package/src/node-types/genesis-approve.ts +28 -3
  220. package/src/node-types/genesis-observe.ts +23 -12
  221. package/src/node-types/genesis-propose.ts +8 -0
  222. package/src/node-types/genesis-update-history.ts +12 -0
  223. package/src/ui/evolution-panel.tsx +96 -0
  224. package/src/ui/insights-widget.tsx +77 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Project Model Store — aggregation layer that pulls from all existing data stores
3
+ * and computes a unified project model ("the brain").
4
+ *
5
+ * Storage: ~/.weaver/projects/{hash8}/model.json (cached with 5-minute TTL)
6
+ */
7
+
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import * as crypto from 'node:crypto';
12
+ import type {
13
+ ProjectModel, WorkflowHealth, FailurePattern, BotProfile,
14
+ ApprovalDecision, OperationEffectiveness,
15
+ GenesisCycleRecord,
16
+ } from './types.js';
17
+ import { computeTrustLevel } from './trust-calculator.js';
18
+
19
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
20
+ const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
21
+
22
+ export class ProjectModelStore {
23
+ private readonly hash8: string;
24
+ private readonly modelPath: string;
25
+
26
+ constructor(private readonly projectDir: string) {
27
+ this.hash8 = crypto.createHash('sha256').update(projectDir).digest('hex').slice(0, 8);
28
+ const modelDir = path.join(os.homedir(), '.weaver', 'projects', this.hash8);
29
+ fs.mkdirSync(modelDir, { recursive: true });
30
+ this.modelPath = path.join(modelDir, 'model.json');
31
+ }
32
+
33
+ async getOrBuild(): Promise<ProjectModel> {
34
+ try {
35
+ if (fs.existsSync(this.modelPath)) {
36
+ const raw = fs.readFileSync(this.modelPath, 'utf-8');
37
+ const cached = JSON.parse(raw) as ProjectModel;
38
+ if (Date.now() - cached.builtAt < CACHE_TTL_MS) {
39
+ return cached;
40
+ }
41
+ }
42
+ } catch { /* cache corrupt — rebuild */ }
43
+
44
+ return this.build();
45
+ }
46
+
47
+ async build(): Promise<ProjectModel> {
48
+ // 1. Load stores with dynamic imports (avoid circular deps)
49
+ let runs: Array<{ workflowFile: string; startedAt: string; finishedAt: string; durationMs: number; success: boolean; outcome: string; summary: string }> = [];
50
+ try {
51
+ const { RunStore } = await import('./run-store.js');
52
+ const allRuns = new RunStore().list({ limit: 10_000 });
53
+ // Filter to runs within this project directory, excluding node_modules
54
+ runs = allRuns.filter(r =>
55
+ r.workflowFile.startsWith(this.projectDir) &&
56
+ !r.workflowFile.includes('/node_modules/')
57
+ );
58
+ } catch { /* unavailable */ }
59
+
60
+ let auditEvents: Array<{ type: string; timestamp: string; runId: string; data?: Record<string, unknown> }> = [];
61
+ try {
62
+ const { AuditStore } = await import('./audit-store.js');
63
+ auditEvents = new AuditStore().queryRecent(500);
64
+ } catch { /* unavailable */ }
65
+
66
+ let costRecords: Array<{ runId?: string; model?: string; promptTokens?: number; completionTokens?: number; estimatedCost?: number; cost?: number; timestamp: number }> = [];
67
+ try {
68
+ const { CostStore } = await import('./cost-store.js');
69
+ costRecords = new CostStore().query();
70
+ } catch { /* unavailable */ }
71
+
72
+ let genesisCycles: GenesisCycleRecord[] = [];
73
+ try {
74
+ if (fs.existsSync(path.join(this.projectDir, '.genesis'))) {
75
+ const { GenesisStore } = await import('./genesis-store.js');
76
+ genesisCycles = new GenesisStore(this.projectDir).loadHistory().cycles;
77
+ }
78
+ } catch { /* unavailable */ }
79
+
80
+ let conversationCount = 0;
81
+ try {
82
+ const { ConversationStore } = await import('./conversation-store.js');
83
+ conversationCount = new ConversationStore().list().length;
84
+ } catch { /* unavailable */ }
85
+
86
+ // 2. Build sections
87
+ const workflows = this.buildWorkflowHealth(runs);
88
+ const overall = workflows.length > 0
89
+ ? Math.round(workflows.reduce((s, w) => s + w.score * w.totalRuns, 0) / Math.max(1, workflows.reduce((s, w) => s + w.totalRuns, 0)))
90
+ : 0;
91
+
92
+ const bots = this.buildBotProfiles(runs);
93
+
94
+ let classifyError: ((msg: string) => { isTransient: boolean; category: string }) | null = null;
95
+ try {
96
+ const mod = await import('./error-classifier.js');
97
+ classifyError = (msg: string) => mod.classifyError(msg);
98
+ } catch { /* unavailable */ }
99
+
100
+ const failurePatterns = this.buildFailurePatterns(auditEvents, classifyError);
101
+ const userPreferences = this.buildUserPreferences(genesisCycles);
102
+ const evolution = this.buildEvolution(genesisCycles);
103
+ const cost = this.buildCost(costRecords, runs);
104
+
105
+ // 3. Assemble partial model, compute trust
106
+ const partial = {
107
+ projectDir: this.projectDir,
108
+ builtAt: Date.now(),
109
+ health: { overall, workflows },
110
+ bots,
111
+ failurePatterns,
112
+ userPreferences,
113
+ evolution,
114
+ cost,
115
+ _conversationCount: conversationCount,
116
+ };
117
+
118
+ const trust = computeTrustLevel(partial);
119
+ const model: ProjectModel = {
120
+ projectDir: partial.projectDir,
121
+ builtAt: partial.builtAt,
122
+ health: partial.health,
123
+ bots: partial.bots,
124
+ failurePatterns: partial.failurePatterns,
125
+ userPreferences: partial.userPreferences,
126
+ evolution: partial.evolution,
127
+ cost: partial.cost,
128
+ trust,
129
+ };
130
+
131
+ // 4. Cache
132
+ try {
133
+ fs.writeFileSync(this.modelPath, JSON.stringify(model, null, 2), 'utf-8');
134
+ } catch { /* non-fatal */ }
135
+
136
+ return model;
137
+ }
138
+
139
+ invalidate(): void {
140
+ try {
141
+ if (fs.existsSync(this.modelPath)) fs.unlinkSync(this.modelPath);
142
+ } catch { /* non-fatal */ }
143
+ }
144
+
145
+ formatSummary(model: ProjectModel): string {
146
+ const { health, bots, failurePatterns, cost, trust, evolution } = model;
147
+ const healthyCount = health.workflows.filter(w => w.trend !== 'degrading').length;
148
+ const degradingCount = health.workflows.filter(w => w.trend === 'degrading').length;
149
+ const ejectedCount = bots.filter(b => b.ejected).length;
150
+ const criticalPatterns = failurePatterns.filter(f => f.occurrences >= 5 && !f.transient).length;
151
+
152
+ const phaseNames: Record<number, string> = {
153
+ 1: 'insights + suggestions',
154
+ 2: 'proposals with explanation',
155
+ 3: 'proposals with visual diff',
156
+ 4: 'auto-apply COSMETIC',
157
+ };
158
+
159
+ return [
160
+ `Health: ${health.overall}/100 (${health.workflows.length} workflows, ${healthyCount} healthy, ${degradingCount} degrading)`,
161
+ `Bots: ${bots.length} registered (${ejectedCount} ejected)`,
162
+ `Failures: ${failurePatterns.length} recurring patterns (${criticalPatterns} critical)`,
163
+ `Cost: $${cost.last7Days.toFixed(2)} last 7 days (${cost.trend})`,
164
+ `Trust: Phase ${trust.phase} (${phaseNames[trust.phase]})`,
165
+ `Evolution: ${evolution.totalCycles} cycles (${evolution.totalCycles > 0 ? Math.round(evolution.successRate * 100) : 0}% success)`,
166
+ ].join('\n');
167
+ }
168
+
169
+ formatSessionGreeting(model: ProjectModel): string {
170
+ const insightCount = model.failurePatterns.filter(f => f.occurrences >= 3).length
171
+ + model.health.workflows.filter(w => w.trend === 'degrading').length;
172
+ return `Health ${model.health.overall}/100 \u00b7 ${model.bots.length} bots \u00b7 ${insightCount} insights \u00b7 $${model.cost.last7Days.toFixed(2)}/7d`;
173
+ }
174
+
175
+ // ---- Private builders ----
176
+
177
+ private buildWorkflowHealth(runs: typeof ProjectModelStore.prototype.build extends (...args: any[]) => any ? any : never): WorkflowHealth[] {
178
+ const byWorkflow = new Map<string, Array<{ startedAt: string; durationMs: number; success: boolean }>>();
179
+ for (const run of runs) {
180
+ if (!byWorkflow.has(run.workflowFile)) byWorkflow.set(run.workflowFile, []);
181
+ byWorkflow.get(run.workflowFile)!.push(run);
182
+ }
183
+
184
+ const now = Date.now();
185
+ const d7 = now - 7 * 86_400_000;
186
+ const d14 = now - 14 * 86_400_000;
187
+ const result: WorkflowHealth[] = [];
188
+
189
+ for (const [absFile, wfRuns] of byWorkflow) {
190
+ // Convert to relative path for display
191
+ const file = absFile.startsWith(this.projectDir)
192
+ ? path.relative(this.projectDir, absFile)
193
+ : absFile;
194
+
195
+ const totalRuns = wfRuns.length;
196
+ const successRate = totalRuns > 0 ? wfRuns.filter(r => r.success).length / totalRuns : 0;
197
+ const avgDurationMs = totalRuns > 0 ? Math.round(wfRuns.reduce((s, r) => s + (r.durationMs ?? 0), 0) / totalRuns) : 0;
198
+ const sorted = [...wfRuns].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
199
+ const lastRun = sorted[0]?.startedAt ?? null;
200
+
201
+ const recent = wfRuns.filter(r => new Date(r.startedAt).getTime() >= d7);
202
+ const prev = wfRuns.filter(r => { const t = new Date(r.startedAt).getTime(); return t >= d14 && t < d7; });
203
+ const recentRate = recent.length > 0 ? recent.filter(r => r.success).length / recent.length : successRate;
204
+ const prevRate = prev.length > 0 ? prev.filter(r => r.success).length / prev.length : successRate;
205
+ const diff = recentRate - prevRate;
206
+ const trend: WorkflowHealth['trend'] = diff > 0.05 ? 'improving' : diff < -0.05 ? 'degrading' : 'stable';
207
+
208
+ result.push({ file, score: Math.round(successRate * 100), totalRuns, successRate, avgDurationMs, lastRun, trend });
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ private buildBotProfiles(runs: Array<{ workflowFile: string; success: boolean; durationMs: number }>): BotProfile[] {
215
+ const profiles: BotProfile[] = [];
216
+ try {
217
+ if (!fs.existsSync(BOTS_DIR)) return profiles;
218
+ for (const name of fs.readdirSync(BOTS_DIR)) {
219
+ const metaPath = path.join(BOTS_DIR, name, 'meta.json');
220
+ if (!fs.existsSync(metaPath)) continue;
221
+ try {
222
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
223
+ if (meta.projectDir !== this.projectDir) continue;
224
+
225
+ const ejectedPath = path.join(this.projectDir, '.fw', 'bots', name);
226
+ const ejected = fs.existsSync(ejectedPath);
227
+
228
+ // Compute bot task stats from runs associated with this bot
229
+ const botRuns = runs.filter(r => r.workflowFile.includes(name) || r.workflowFile.includes('weaver-bot'));
230
+ const totalTasksRun = botRuns.length;
231
+ const successRate = totalTasksRun > 0 ? botRuns.filter(r => r.success).length / totalTasksRun : 0;
232
+ const avgTaskDurationMs = totalTasksRun > 0 ? Math.round(botRuns.reduce((s, r) => s + (r.durationMs ?? 0), 0) / totalTasksRun) : 0;
233
+
234
+ profiles.push({
235
+ name: meta.name ?? name,
236
+ workflowFile: ejected ? path.join(ejectedPath, 'weaver-bot.ts') : 'node_modules/@synergenius/flow-weaver-pack-weaver/src/workflows/weaver-bot.ts',
237
+ ejected,
238
+ totalTasksRun,
239
+ successRate,
240
+ avgTaskDurationMs,
241
+ topFailurePatterns: [],
242
+ });
243
+ } catch { /* corrupt meta */ }
244
+ }
245
+ } catch { /* bots dir read failed */ }
246
+ return profiles;
247
+ }
248
+
249
+ private buildFailurePatterns(
250
+ auditEvents: Array<{ type: string; timestamp: string; data?: Record<string, unknown> }>,
251
+ classifyError: ((msg: string) => { isTransient: boolean; category: string }) | null,
252
+ ): FailurePattern[] {
253
+ const errorEvents = auditEvents.filter(e =>
254
+ e.type.includes('error') || e.type.includes('fail') || e.data?.error !== undefined
255
+ );
256
+
257
+ const groups = new Map<string, { message: string; count: number; lastSeen: string; workflows: Set<string> }>();
258
+ for (const event of errorEvents) {
259
+ const rawMsg = String(event.data?.error ?? event.data?.message ?? event.type);
260
+ const key = rawMsg.slice(0, 50);
261
+ const existing = groups.get(key);
262
+ const wf = String(event.data?.workflowFile ?? event.data?.file ?? '');
263
+ if (existing) {
264
+ existing.count++;
265
+ if (event.timestamp > existing.lastSeen) existing.lastSeen = event.timestamp;
266
+ if (wf) existing.workflows.add(wf);
267
+ } else {
268
+ const wfSet = new Set<string>();
269
+ if (wf) wfSet.add(wf);
270
+ groups.set(key, { message: rawMsg, count: 1, lastSeen: event.timestamp, workflows: wfSet });
271
+ }
272
+ }
273
+
274
+ const patterns: FailurePattern[] = [];
275
+ for (const [, g] of groups) {
276
+ let transient = false;
277
+ let category = 'unknown';
278
+ if (classifyError) {
279
+ try {
280
+ const c = classifyError(g.message);
281
+ transient = c.isTransient;
282
+ category = c.category;
283
+ } catch { /* classification failed */ }
284
+ }
285
+ patterns.push({
286
+ pattern: g.message.slice(0, 120),
287
+ category,
288
+ occurrences: g.count,
289
+ lastSeen: g.lastSeen,
290
+ workflows: [...g.workflows],
291
+ transient,
292
+ });
293
+ }
294
+
295
+ return patterns.sort((a, b) => b.occurrences - a.occurrences);
296
+ }
297
+
298
+ private buildUserPreferences(genesisCycles: GenesisCycleRecord[]): ProjectModel['userPreferences'] {
299
+ const approvalHistory: ApprovalDecision[] = [];
300
+
301
+ for (const cycle of genesisCycles) {
302
+ if (cycle.approved === null) continue;
303
+ approvalHistory.push({
304
+ timestamp: cycle.timestamp,
305
+ proposalSummary: cycle.proposal?.summary ?? '',
306
+ impactLevel: cycle.proposal?.impactLevel ?? 'MINOR',
307
+ approved: cycle.approved,
308
+ reason: cycle.rejectionReason,
309
+ });
310
+ }
311
+
312
+ // Auto-approve: 5+ consecutive approvals for a given impact level
313
+ const autoApprovePatterns: string[] = [];
314
+ const neverApprovePatterns: string[] = [];
315
+ const byImpact = new Map<string, boolean[]>();
316
+ for (const d of approvalHistory) {
317
+ if (!byImpact.has(d.impactLevel)) byImpact.set(d.impactLevel, []);
318
+ byImpact.get(d.impactLevel)!.push(d.approved);
319
+ }
320
+ for (const [level, decisions] of byImpact) {
321
+ let consec = 0;
322
+ for (let i = decisions.length - 1; i >= 0; i--) {
323
+ if (decisions[i]) consec++;
324
+ else break;
325
+ }
326
+ if (consec >= 5) autoApprovePatterns.push(level);
327
+
328
+ let consecRejects = 0;
329
+ for (let i = decisions.length - 1; i >= 0; i--) {
330
+ if (!decisions[i]) consecRejects++;
331
+ else break;
332
+ }
333
+ if (consecRejects >= 3) neverApprovePatterns.push(level);
334
+ }
335
+
336
+ return { approvalHistory, autoApprovePatterns, neverApprovePatterns };
337
+ }
338
+
339
+ private buildEvolution(genesisCycles: GenesisCycleRecord[]): ProjectModel['evolution'] {
340
+ const totalCycles = genesisCycles.length;
341
+ const applied = genesisCycles.filter(c => c.outcome === 'applied').length;
342
+ const successRate = totalCycles > 0 ? applied / totalCycles : 0;
343
+
344
+ const byOp = new Map<string, { proposed: number; applied: number; rolledBack: number }>();
345
+ for (const cycle of genesisCycles) {
346
+ if (!cycle.proposal) continue;
347
+ for (const op of cycle.proposal.operations) {
348
+ if (!byOp.has(op.type)) byOp.set(op.type, { proposed: 0, applied: 0, rolledBack: 0 });
349
+ const e = byOp.get(op.type)!;
350
+ e.proposed++;
351
+ if (cycle.outcome === 'applied') e.applied++;
352
+ else if (cycle.outcome === 'rolled-back') e.rolledBack++;
353
+ }
354
+ }
355
+
356
+ const byOperationType: Record<string, OperationEffectiveness> = {};
357
+ for (const [type, stats] of byOp) {
358
+ byOperationType[type] = {
359
+ proposed: stats.proposed,
360
+ applied: stats.applied,
361
+ rolledBack: stats.rolledBack,
362
+ effectiveness: stats.proposed > 0 ? stats.applied / stats.proposed : 0,
363
+ };
364
+ }
365
+
366
+ return {
367
+ totalCycles,
368
+ successRate,
369
+ byOperationType,
370
+ recentCycles: genesisCycles.slice(-10),
371
+ };
372
+ }
373
+
374
+ private buildCost(
375
+ costRecords: Array<{ estimatedCost?: number; cost?: number; timestamp: number; model?: string }>,
376
+ runs: Array<{ success: boolean }>,
377
+ ): ProjectModel['cost'] {
378
+ const now = Date.now();
379
+ const d7 = now - 7 * 86_400_000;
380
+ const d14 = now - 14 * 86_400_000;
381
+ const d30 = now - 30 * 86_400_000;
382
+
383
+ const getCost = (r: { estimatedCost?: number; cost?: number }) => r.estimatedCost ?? r.cost ?? 0;
384
+ const totalSpent = costRecords.reduce((s, r) => s + getCost(r), 0);
385
+ const last7Days = costRecords.filter(r => r.timestamp >= d7).reduce((s, r) => s + getCost(r), 0);
386
+ const prev7Days = costRecords.filter(r => r.timestamp >= d14 && r.timestamp < d7).reduce((s, r) => s + getCost(r), 0);
387
+ const last30Days = costRecords.filter(r => r.timestamp >= d30).reduce((s, r) => s + getCost(r), 0);
388
+
389
+ let trend: ProjectModel['cost']['trend'] = 'stable';
390
+ if (prev7Days > 0) {
391
+ const ratio = last7Days / prev7Days;
392
+ if (ratio > 1.15) trend = 'increasing';
393
+ else if (ratio < 0.85) trend = 'decreasing';
394
+ }
395
+
396
+ const successfulRuns = runs.filter(r => r.success).length;
397
+ const costPerSuccessfulRun = successfulRuns > 0 ? totalSpent / successfulRuns : 0;
398
+
399
+ // High cost workflows — from cost records grouped by model (simplified)
400
+ const highCostWorkflows: Array<{ workflow: string; avgCost: number }> = [];
401
+
402
+ return { totalSpent, last7Days, last30Days, trend, costPerSuccessfulRun, highCostWorkflows };
403
+ }
404
+ }
@@ -36,7 +36,6 @@ export function formatResponse(text: string, cwd: string): string {
36
36
 
37
37
  function supportsHyperlinks(): boolean {
38
38
  const term = process.env.TERM_PROGRAM ?? '';
39
- // Known terminals that support OSC 8
40
- return ['iTerm.app', 'WezTerm', 'vscode', 'Hyper'].includes(term)
41
- || !!process.env.TERM_PROGRAM_VERSION; // Most modern terminals
39
+ // Known terminals that support OSC 8 hyperlinks
40
+ return ['iTerm.app', 'WezTerm', 'vscode', 'Hyper'].includes(term);
42
41
  }
@@ -110,9 +110,12 @@ export class RunStore {
110
110
  });
111
111
  fs.unlinkSync(path.join(this.dir, file));
112
112
  }
113
- } catch { /* skip corrupt marker */ }
113
+ } catch (err) {
114
+ console.warn(`[weaver] checkOrphans: removing corrupt marker file ${file}:`, err);
115
+ try { fs.unlinkSync(path.join(this.dir, file)); } catch { /* already gone */ }
116
+ }
114
117
  }
115
- } catch { /* dir read failed */ }
118
+ } catch (err) { console.warn(`[weaver] checkOrphans: failed to read directory ${this.dir}:`, err); }
116
119
  return orphans;
117
120
  }
118
121
 
@@ -6,12 +6,13 @@
6
6
  * user input, AI output, or external configuration.
7
7
  */
8
8
 
9
+ import * as fs from 'node:fs';
9
10
  import * as path from 'node:path';
10
11
 
11
12
  /**
12
13
  * Validate that a relative path does not escape the base directory.
13
14
  * Returns the resolved absolute path if safe, or null if the path
14
- * attempts traversal.
15
+ * attempts traversal. Also resolves symlinks to prevent bypass attacks.
15
16
  */
16
17
  export function safePath(baseDir: string, relativePath: string): string | null {
17
18
  const normalized = path.normalize(relativePath);
@@ -28,6 +29,24 @@ export function safePath(baseDir: string, relativePath: string): string | null {
28
29
  return null;
29
30
  }
30
31
 
32
+ // Symlink protection: walk up to find deepest existing segment and
33
+ // verify its real path stays within the base directory.
34
+ let checkPath = resolved;
35
+ while (!fs.existsSync(checkPath) && checkPath !== resolvedBase) {
36
+ checkPath = path.dirname(checkPath);
37
+ }
38
+ if (fs.existsSync(checkPath)) {
39
+ try {
40
+ const realPath = fs.realpathSync(checkPath);
41
+ const realBase = fs.realpathSync(resolvedBase);
42
+ if (!realPath.startsWith(realBase + path.sep) && realPath !== realBase) {
43
+ return null;
44
+ }
45
+ } catch {
46
+ return null; // Can't verify safety, reject
47
+ }
48
+ }
49
+
31
50
  return resolved;
32
51
  }
33
52
 
package/src/bot/safety.ts CHANGED
@@ -1,14 +1,68 @@
1
1
  /**
2
2
  * Shared safety constants and checks — single source of truth.
3
+ *
4
+ * Both step-executor.ts and assistant-tools.ts MUST use these patterns.
5
+ * Do not duplicate blocklists elsewhere.
3
6
  */
4
7
 
5
- export const BLOCKED_COMMANDS = ['rm -rf', 'git push', 'npm publish', 'sudo', 'curl|sh', 'wget|sh'];
8
+ /** Shell commands that are NEVER allowed (destructive or evasive operations). */
9
+ export const BLOCKED_SHELL_PATTERNS: RegExp[] = [
10
+ // Destructive file operations
11
+ /\brm\s+(-[a-z]*r|-[a-z]*f)[a-z]*\s/i, // rm with -r or -f flags
12
+
13
+ // Git remote/destructive operations
14
+ /\bgit\s+push\b/i, // git push (no remote ops)
15
+ /\bgit\s+reset\s+--hard\b/i, // git reset --hard
16
+
17
+ // Package publishing
18
+ /\bnpm\s+publish\b/i, // npm publish
19
+
20
+ // Remote code execution via download
21
+ /\bcurl\b.*\|\s*(sh|bash)\b/i, // curl | sh/bash
22
+ /\bwget\b.*\|\s*(sh|bash)\b/i, // wget | sh/bash
23
+
24
+ // Privilege escalation
25
+ /\bsudo\b/i, // sudo
26
+
27
+ // Dangerous permissions
28
+ /\bchmod\s+777\b/i, // chmod 777
29
+
30
+ // Process killing
31
+ /\bkill\s+-9\b/i, // kill -9
32
+
33
+ // Disk destruction
34
+ /\bmkfs\b/i, // format disk
35
+ /\bdd\s+if=/i, // dd (disk destroyer)
36
+ />\s*\/dev\/sd/i, // write to raw disk
37
+
38
+ // Pipe-to-interpreter: encoded payload execution
39
+ /\bbase64\b.*\|\s*(sh|bash|zsh)\b/i, // base64 | bash
40
+
41
+ // Inline code execution via interpreters
42
+ /\bnode\s+(-e|--eval)\b/i, // node -e / node --eval
43
+ /\bpython[23]?\s+-c\b/i, // python -c / python3 -c
44
+ /\bperl\s+-e\b/i, // perl -e
45
+ /\bruby\s+-e\b/i, // ruby -e
46
+
47
+ // Shell eval (arbitrary code execution)
48
+ /\beval\s+/i, // eval ...
49
+ ];
50
+
6
51
  export const BLOCKED_URL_PATTERN = /localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d|172\.(1[6-9]|2\d|3[01])\.|192\.168\./i;
7
52
  export const MAX_READ_SIZE = 1_048_576;
8
53
  export const CHARS_PER_TOKEN = 4;
9
54
 
10
- export function isBlockedCommand(cmd: string): boolean {
11
- return BLOCKED_COMMANDS.some(b => cmd.includes(b));
55
+ /**
56
+ * Check if a shell command matches any blocked pattern.
57
+ * Returns the matching pattern source string, or false if safe.
58
+ */
59
+ export function isBlockedCommand(cmd: string): string | false {
60
+ for (const pattern of BLOCKED_SHELL_PATTERNS) {
61
+ if (pattern.test(cmd)) {
62
+ return pattern.source;
63
+ }
64
+ }
65
+ return false;
12
66
  }
13
67
 
14
68
  export function isBlockedUrl(url: string): boolean {
@@ -42,16 +42,30 @@ export class SessionStore {
42
42
  return JSON.parse(fs.readFileSync(this.filePath, 'utf-8')) as SessionState;
43
43
  } catch (err) {
44
44
  if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] session state load failed: ${err}\n`);
45
+ // Try backup recovery
46
+ return this.loadBackup();
47
+ }
48
+ }
49
+
50
+ /** Recover session from backup file when primary is corrupt. */
51
+ private loadBackup(): SessionState | null {
52
+ const backupPath = this.filePath + '.bak';
53
+ if (!fs.existsSync(backupPath)) return null;
54
+ try {
55
+ const data = JSON.parse(fs.readFileSync(backupPath, 'utf-8')) as SessionState;
56
+ // Restore backup to primary
57
+ try { this.writeAtomic(data); } catch { /* best effort */ }
58
+ return data;
59
+ } catch (err) {
60
+ if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] session backup load failed: ${err}\n`);
45
61
  return null;
46
62
  }
47
63
  }
48
64
 
49
65
  async save(state: SessionState): Promise<void> {
50
66
  return withFileLock(this.filePath, () => {
51
- const dir = path.dirname(this.filePath);
52
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
53
67
  state.lastActivity = Date.now();
54
- fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), 'utf-8');
68
+ this.writeAtomic(state);
55
69
  });
56
70
  }
57
71
 
@@ -60,15 +74,41 @@ export class SessionStore {
60
74
  const state = this.load();
61
75
  if (!state) return null;
62
76
  Object.assign(state, patch);
63
- const dir = path.dirname(this.filePath);
64
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
65
77
  state.lastActivity = Date.now();
66
- fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), 'utf-8');
78
+ this.writeAtomic(state);
67
79
  return state;
68
80
  });
69
81
  }
70
82
 
71
83
  clear(): void {
72
- try { fs.unlinkSync(this.filePath); } catch { /* ignore */ }
84
+ try {
85
+ fs.unlinkSync(this.filePath);
86
+ } catch (err) {
87
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
88
+ }
89
+ }
90
+
91
+ /** Atomic write: write to temp file, backup existing, rename into place. */
92
+ private writeAtomic(state: SessionState): void {
93
+ const dir = path.dirname(this.filePath);
94
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
95
+
96
+ const tmpPath = this.filePath + `.tmp.${process.pid}`;
97
+ const backupPath = this.filePath + '.bak';
98
+ const content = JSON.stringify(state, null, 2);
99
+
100
+ // Write to temp file first
101
+ fs.writeFileSync(tmpPath, content, 'utf-8');
102
+
103
+ // Backup current file if it exists
104
+ if (fs.existsSync(this.filePath)) {
105
+ try { fs.copyFileSync(this.filePath, backupPath); } catch { /* best effort */ }
106
+ }
107
+
108
+ // Atomic rename
109
+ fs.renameSync(tmpPath, this.filePath);
110
+
111
+ // Update backup after successful write
112
+ try { fs.copyFileSync(this.filePath, backupPath); } catch { /* best effort */ }
73
113
  }
74
114
  }