@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,651 @@
1
+ /**
2
+ * Improve Loop — autonomous codebase improvement via git worktree.
3
+ *
4
+ * Creates an isolated worktree, runs the insight engine to find issues,
5
+ * generates tasks, executes them through the assistant, verifies with tests,
6
+ * and commits or rolls back. The user's working directory is never touched.
7
+ *
8
+ * Usage: `weaver improve [--max-cycles 20] [--max-failures 3] [--protected "*.config.*"]`
9
+ *
10
+ * General-purpose: works on any project. We dogfood it on ourselves.
11
+ */
12
+
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import * as os from 'node:os';
16
+ import { execFileSync } from 'node:child_process';
17
+ import { c } from './ansi.js';
18
+
19
+ export interface ImproveConfig {
20
+ maxCycles: number; // 0 = unlimited, runs until stopped or nothing left to improve
21
+ maxConsecutiveFailures: number;
22
+ protectedPatterns: string[];
23
+ testCommand: string;
24
+ buildCommand?: string;
25
+ /** Optional device connection for streaming events to Studio */
26
+ deviceConnection?: import('@synergenius/flow-weaver/agent').DeviceConnection;
27
+ projectDir: string;
28
+ }
29
+
30
+ export interface ImproveCycleResult {
31
+ cycle: number;
32
+ outcome: 'success' | 'failure' | 'skip' | 'blocked';
33
+ description: string;
34
+ filesChanged: string[];
35
+ commitHash?: string;
36
+ error?: string;
37
+ }
38
+
39
+ export interface ImproveResult {
40
+ totalCycles: number;
41
+ successes: number;
42
+ failures: number;
43
+ skips: number;
44
+ blocked: number;
45
+ cycles: ImproveCycleResult[];
46
+ startedAt: string;
47
+ finishedAt: string;
48
+ branch: string;
49
+ worktreePath: string;
50
+ reason: 'complete' | 'max-cycles' | 'max-failures' | 'nothing-to-improve';
51
+ }
52
+
53
+ const DEFAULT_PROTECTED = [
54
+ 'package.json',
55
+ 'package-lock.json',
56
+ 'tsconfig.json',
57
+ '*.config.*',
58
+ '.weaver.json',
59
+ '.genesis/**',
60
+ ];
61
+
62
+ export async function runImproveLoop(config: ImproveConfig): Promise<ImproveResult> {
63
+ const { maxCycles, maxConsecutiveFailures, protectedPatterns, testCommand, buildCommand, projectDir, deviceConnection } = config;
64
+ const out = (s: string) => process.stderr.write(s);
65
+ const emitEvent = (type: string, data: Record<string, unknown> = {}) => {
66
+ deviceConnection?.emit({ type, data, timestamp: Date.now() });
67
+ };
68
+ const cycles: ImproveCycleResult[] = [];
69
+ let consecutiveFailures = 0;
70
+ const completedWork: string[] = []; // track what's been done so it doesn't repeat
71
+ const startedAt = new Date().toISOString();
72
+ const branchName = `weaver/improve-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}`;
73
+ const worktreeDir = path.join(projectDir, '.weaver-improve', branchName.replace(/\//g, '-'));
74
+
75
+ // Prevent sleep on macOS
76
+ let caffeinate: import('node:child_process').ChildProcess | null = null;
77
+ try {
78
+ if (process.platform === 'darwin') {
79
+ const { spawn } = await import('node:child_process');
80
+ caffeinate = spawn('caffeinate', ['-i', '-s'], { stdio: 'ignore', detached: true });
81
+ caffeinate.unref();
82
+ }
83
+ } catch { /* caffeinate not available */ }
84
+
85
+ out(`\n ${c.bold('weaver improve')}\n`);
86
+ out(` ${c.dim(`Project: ${path.basename(projectDir)}`)}\n`);
87
+ out(` ${c.dim(`Branch: ${branchName}`)}\n`);
88
+ out(` ${c.dim(`Worktree: ${path.relative(projectDir, worktreeDir)}`)}\n`);
89
+ out(` ${c.dim(`Max cycles: ${maxCycles === 0 ? 'unlimited' : maxCycles}, stop after ${maxConsecutiveFailures} consecutive failures`)}\n`);
90
+ out(` ${c.dim(`Test: ${testCommand}`)}\n`);
91
+ if (caffeinate) out(` ${c.dim('Sleep inhibited (caffeinate)')}\n`);
92
+ out('\n');
93
+
94
+ // Verify this is a git repo (worktree requires it)
95
+ try {
96
+ execFileSync('git', ['rev-parse', '--git-dir'], { cwd: projectDir, stdio: 'pipe' });
97
+ } catch {
98
+ out(` ${c.red('✗')} Not a git repository.\n`);
99
+ return emptyResult(startedAt, branchName, worktreeDir, 'complete');
100
+ }
101
+
102
+ // Create worktree (clean up stale ones first)
103
+ out(` ${c.dim('Creating worktree...')}\n`);
104
+ try {
105
+ // Remove stale worktree/branch from previous runs
106
+ if (fs.existsSync(worktreeDir)) {
107
+ try { execFileSync('git', ['worktree', 'remove', worktreeDir, '--force'], { cwd: projectDir, stdio: 'pipe' }); } catch { /* best effort */ }
108
+ if (fs.existsSync(worktreeDir)) fs.rmSync(worktreeDir, { recursive: true, force: true });
109
+ }
110
+ try { execFileSync('git', ['branch', '-D', branchName], { cwd: projectDir, stdio: 'pipe' }); } catch { /* branch may not exist */ }
111
+ execFileSync('git', ['worktree', 'prune'], { cwd: projectDir, stdio: 'pipe' });
112
+
113
+ fs.mkdirSync(path.dirname(worktreeDir), { recursive: true });
114
+ execFileSync('git', ['worktree', 'add', worktreeDir, '-b', branchName], { cwd: projectDir, stdio: 'pipe' });
115
+ } catch {
116
+ // Branch might already exist — try without -b
117
+ try {
118
+ execFileSync('git', ['worktree', 'add', worktreeDir, branchName], { cwd: projectDir, stdio: 'pipe' });
119
+ } catch (err) {
120
+ out(` ${c.red('✗')} Failed to create worktree: ${err instanceof Error ? err.message : err}\n`);
121
+ return emptyResult(startedAt, branchName, worktreeDir, 'complete');
122
+ }
123
+ }
124
+
125
+ // Ensure node_modules is gitignored in worktree (symlink shouldn't be committed)
126
+ try {
127
+ const wtGitignore = path.join(worktreeDir, '.gitignore');
128
+ const existing = fs.existsSync(wtGitignore) ? fs.readFileSync(wtGitignore, 'utf-8') : '';
129
+ if (!existing.includes('node_modules')) {
130
+ fs.appendFileSync(wtGitignore, '\nnode_modules/\n');
131
+ }
132
+ } catch { /* non-fatal */ }
133
+
134
+ // Install deps in worktree if needed
135
+ const worktreeNodeModules = path.join(worktreeDir, 'node_modules');
136
+ if (!fs.existsSync(worktreeNodeModules)) {
137
+ // Symlink node_modules from main tree for speed
138
+ try {
139
+ const mainNodeModules = path.join(projectDir, 'node_modules');
140
+ if (fs.existsSync(mainNodeModules)) {
141
+ fs.symlinkSync(mainNodeModules, worktreeNodeModules);
142
+ out(` ${c.dim('Linked node_modules from main tree')}\n`);
143
+ }
144
+ } catch { /* will need npm install */ }
145
+ }
146
+
147
+ // Build in worktree if needed
148
+ if (buildCommand) {
149
+ out(` ${c.dim('Building in worktree...')}\n`);
150
+ try {
151
+ execFileSync('sh', ['-c', buildCommand], { cwd: worktreeDir, stdio: 'pipe', timeout: 120_000 });
152
+ } catch {
153
+ out(` ${c.red('✗')} Build failed in worktree — aborting.\n`);
154
+ cleanup(projectDir, worktreeDir);
155
+ return emptyResult(startedAt, branchName, worktreeDir, 'complete');
156
+ }
157
+ }
158
+
159
+ // Baseline test — run in worktree to establish the failure count
160
+ // Some projects have flaky tests or env-dependent failures in worktrees
161
+ out(` ${c.dim('Running baseline tests...')}\n`);
162
+ let baselineFailCount = 0;
163
+ try {
164
+ execFileSync('sh', ['-c', testCommand], { cwd: worktreeDir, stdio: 'pipe', timeout: 300_000 });
165
+ out(` ${c.green('✓')} Baseline tests pass\n\n`);
166
+ } catch (err) {
167
+ // Count failures and extract names from vitest output
168
+ const output = (err as { stderr?: Buffer; stdout?: Buffer }).stdout?.toString() ?? '';
169
+ const stderrOutput = (err as { stderr?: Buffer }).stderr?.toString() ?? '';
170
+ const failMatch = output.match(/(\d+) failed/);
171
+ baselineFailCount = failMatch ? parseInt(failMatch[1]!, 10) : 0;
172
+
173
+ // Extract failing test file names for diagnostics
174
+ const failedFiles = (output + stderrOutput).match(/FAIL\s+\S+/g)?.slice(0, 10) ?? [];
175
+
176
+ if (baselineFailCount <= 5) {
177
+ out(` ${c.yellow('⚠')} Baseline: ${baselineFailCount} pre-existing failure(s) — will tolerate these\n`);
178
+ if (failedFiles.length > 0) {
179
+ for (const f of failedFiles) out(` ${c.dim(f)}\n`);
180
+ }
181
+ out('\n');
182
+ } else {
183
+ out(` ${c.red('✗')} Baseline: ${baselineFailCount} failures — too many, fix them first.\n`);
184
+ if (failedFiles.length > 0) {
185
+ for (const f of failedFiles) out(` ${c.dim(f)}\n`);
186
+ }
187
+ cleanup(projectDir, worktreeDir);
188
+ return emptyResult(startedAt, branchName, worktreeDir, 'complete');
189
+ }
190
+ }
191
+
192
+ // Load steering configuration
193
+ const { SteeringEngine, loadSteers, IMPROVE_STEERS } = await import('./steering-engine.js');
194
+ const steers = loadSteers(projectDir, IMPROVE_STEERS);
195
+
196
+ // Main loop
197
+ for (let cycle = 1; maxCycles === 0 || cycle <= maxCycles; cycle++) {
198
+ if (consecutiveFailures >= maxConsecutiveFailures) {
199
+ out(` ${c.yellow('⚠')} Stopping: ${maxConsecutiveFailures} consecutive failures.\n`);
200
+ break;
201
+ }
202
+
203
+ out(` ${c.bold(`--- Cycle ${cycle}/${maxCycles === 0 ? '∞' : maxCycles} ---`)}\n`);
204
+ emitEvent('improve:cycle_start', { cycle, maxCycles });
205
+
206
+ const cycleEngine = new SteeringEngine(steers);
207
+
208
+ // Record HEAD at cycle start to detect if assistant commits during its turn
209
+ let headAtCycleStart = '';
210
+ try {
211
+ headAtCycleStart = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
212
+ } catch { /* not a git repo? */ }
213
+
214
+ // Build context: read the plan and extract top priority
215
+ let planContext = '';
216
+ try {
217
+ const planPath = path.join(worktreeDir, '.weaver-plan.md');
218
+ if (fs.existsSync(planPath)) {
219
+ const planContent = fs.readFileSync(planPath, 'utf-8');
220
+ // Extract the first priority section (### 0. or ### 1.)
221
+ const priorityMatch = planContent.match(/### 0\.[^\n]*\n([\s\S]*?)(?=\n### \d|$)/);
222
+ if (priorityMatch) {
223
+ planContext = `\n\nTOP PRIORITY FROM PROJECT PLAN — you MUST work on this:\n${priorityMatch[0].trim()}\n\nDo NOT work on anything else until the top priority is complete.`;
224
+ } else {
225
+ // Fall back to the full priorities section
226
+ const prioritiesMatch = planContent.match(/## Current Priorities[\s\S]*?(?=\n## [A-Z]|$)/);
227
+ if (prioritiesMatch) {
228
+ planContext = `\n\nPROJECT PLAN PRIORITIES:\n${prioritiesMatch[0].slice(0, 1500)}`;
229
+ }
230
+ }
231
+ }
232
+ } catch { /* no plan */ }
233
+
234
+ const workLog = completedWork.length > 0
235
+ ? `\nALREADY DONE (do NOT repeat): ${completedWork.join('; ')}`
236
+ : '';
237
+
238
+ // Step 1: Find and fix in one turn
239
+ const improveMsg = `You are working in: ${worktreeDir}
240
+
241
+ Work on the TOP PRIORITY from the project plan. If no specific priority, find ONE small improvement. Steps:
242
+ 1. Recall what you know: knowledge_search "project"
243
+ 2. Read .weaver-plan.md to understand the top priority
244
+ 3. Do exactly what the top priority says — one handler migration, one fix, one concrete step
245
+ 4. Write a failing test first, then implement
246
+ 5. Run tests to verify
247
+ 4. Run tests with run_tests to verify
248
+ 5. Store any insights with learn()
249
+
250
+ Keep changes to 1-3 files. All paths relative to ${worktreeDir}.${planContext}${workLog}`;
251
+
252
+ let conversationId = '';
253
+ let discovery = '';
254
+ try {
255
+ const raw = await runAssistantInDir(worktreeDir, improveMsg, '', cycleEngine);
256
+ const parsed = JSON.parse(raw);
257
+ conversationId = String(parsed.conversationId ?? '');
258
+ discovery = String(parsed.response ?? '');
259
+ } catch (err) {
260
+ const msg = err instanceof Error ? err.message : '';
261
+ out(` ${c.dim(`Skip: ${msg.includes('timeout') ? 'timed out' : 'no response'}`)}\n`);
262
+ rollback(worktreeDir);
263
+ cycles.push({ cycle, outcome: 'skip', description: msg.includes('timeout') ? 'Timed out' : 'No response', filesChanged: [] });
264
+ continue;
265
+ }
266
+
267
+ // Stage all work immediately so it's recoverable even if the cycle doesn't complete
268
+ try { execFileSync('git', ['add', '--all'], { cwd: worktreeDir, stdio: 'pipe' }); } catch { /* non-fatal */ }
269
+
270
+ out(` ${c.dim('Done:')} ${discovery.split('\n')[0]?.slice(0, 80)}\n`);
271
+
272
+ // Check steering engine after initial assistant call
273
+ {
274
+ const steerMsg = cycleEngine.check();
275
+ if (steerMsg) {
276
+ out(` ${c.dim(steerMsg.replace(/\[.*?\]\s*/g, ''))}\n`);
277
+ }
278
+ if (cycleEngine.hasHardStop()) {
279
+ cycles.push({ cycle, outcome: 'skip', description: 'Hard stop from steering engine', filesChanged: [] });
280
+ break;
281
+ }
282
+ }
283
+
284
+ if (/^(nothing to improve|no issues found|all good|clean bill of health|i can.t find any|couldn.t find any|no improvements needed)/im.test(discovery)) {
285
+ out(` ${c.green('✓')} Nothing more to improve.\n`);
286
+ cycles.push({ cycle, outcome: 'skip', description: 'Nothing to improve', filesChanged: [] });
287
+ break;
288
+ }
289
+
290
+ // Step 2: Iterative test-fix loop (like a developer would)
291
+ // Steering engine controls cycle duration — hard stop is the safety valve
292
+ const cycleStart = Date.now();
293
+ let testsPassing = false;
294
+ let attempt = 0;
295
+
296
+ while (!cycleEngine.hasHardStop()) {
297
+ attempt++;
298
+ const changedFiles = getChangedFiles(worktreeDir);
299
+ if (changedFiles.length === 0) {
300
+ out(` ${c.dim('Skip: no files changed')}\n`);
301
+ break;
302
+ }
303
+
304
+ // Check protected files
305
+ const blocked = changedFiles.find(f => isProtected(f, protectedPatterns));
306
+ if (blocked) {
307
+ out(` ${c.yellow('⚠')} Blocked: modified protected file ${blocked}\n`);
308
+ break;
309
+ }
310
+
311
+ // Build if needed
312
+ if (buildCommand) {
313
+ try {
314
+ execFileSync('sh', ['-c', buildCommand], { cwd: worktreeDir, stdio: 'pipe', timeout: 120_000 });
315
+ } catch {
316
+ out(` ${c.red('✗')} Build failed (attempt ${attempt})\n`);
317
+ if (cycleEngine.hasHardStop()) break;
318
+ // Ask assistant to fix build errors
319
+ try {
320
+ const fixRaw = await runAssistantInDir(worktreeDir, `Build failed. Fix the build errors. You are working in: ${worktreeDir}`, conversationId, cycleEngine);
321
+ const fixParsed = JSON.parse(fixRaw);
322
+ conversationId = String(fixParsed.conversationId ?? conversationId);
323
+ } catch { break; }
324
+ continue;
325
+ }
326
+ }
327
+
328
+ // Run tests
329
+ out(` ${c.dim(`Testing (attempt ${attempt})...`)}\n`);
330
+ try {
331
+ execFileSync('sh', ['-c', testCommand], { cwd: worktreeDir, stdio: 'pipe', timeout: 300_000 });
332
+ cycleEngine.recordEvent('test_pass');
333
+ testsPassing = true;
334
+ break;
335
+ } catch (testErr) {
336
+ cycleEngine.recordEvent('test_fail');
337
+ const testOutput = ((testErr as { stdout?: Buffer }).stdout?.toString() ?? '');
338
+ const failMatch = testOutput.match(/(\d+) failed/);
339
+ const newFailCount = failMatch ? parseInt(failMatch[1]!, 10) : 999;
340
+
341
+ if (newFailCount <= baselineFailCount) {
342
+ out(` ${c.yellow('⚠')} Same pre-existing failures (${newFailCount}) — accepting\n`);
343
+ testsPassing = true;
344
+ break;
345
+ }
346
+
347
+ out(` ${c.red('✗')} ${newFailCount} failures (${newFailCount - baselineFailCount} new)\n`);
348
+
349
+ // Check steering engine for nudges
350
+ {
351
+ const steerMsg = cycleEngine.check();
352
+ if (steerMsg) {
353
+ out(` ${c.dim(steerMsg.replace(/\[.*?\]\s*/g, ''))}\n`);
354
+ }
355
+ }
356
+
357
+ const elapsed = Math.round((Date.now() - cycleStart) / 1000);
358
+ if (cycleEngine.hasHardStop()) {
359
+ out(` ${c.red('✗')} Steering engine hard stop (${elapsed}s) — rollback\n`);
360
+ break;
361
+ }
362
+
363
+ // Extract failing test names for the assistant
364
+ const failedTests = testOutput.match(/FAIL .+/g)?.slice(0, 5).join('\n') ?? 'unknown failures';
365
+
366
+ // Ask assistant to fix the test failures — same conversation, it has context
367
+ out(` ${c.dim(`Fixing failures (attempt ${attempt + 1})...`)}\n`);
368
+ try {
369
+ const fixMsg = `Tests failed with ${newFailCount - baselineFailCount} new failures. Fix them. You are working in: ${worktreeDir}
370
+
371
+ Failing tests:
372
+ ${failedTests}
373
+
374
+ Fix the failures without reverting your improvement. If you can't fix them, revert only the parts that broke tests.`;
375
+ const fixRaw = await runAssistantInDir(worktreeDir, fixMsg, conversationId, cycleEngine);
376
+ const fixParsed = JSON.parse(fixRaw);
377
+ conversationId = String(fixParsed.conversationId ?? conversationId);
378
+ // Stage work immediately after each fix attempt
379
+ try { execFileSync('git', ['add', '--all'], { cwd: worktreeDir, stdio: 'pipe' }); } catch { /* non-fatal */ }
380
+ } catch {
381
+ out(` ${c.dim('Fix attempt timed out')}\n`);
382
+ break;
383
+ }
384
+ }
385
+ }
386
+
387
+ // Check if the assistant already committed (the test_pass steer tells it to commit immediately)
388
+ try {
389
+ const headNow = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
390
+ if (headAtCycleStart && headNow !== headAtCycleStart) {
391
+ // HEAD moved — assistant committed during its turn
392
+ const newCommits = execFileSync('git', ['log', '--oneline', `${headAtCycleStart}..HEAD`], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
393
+ const commitHash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
394
+ const commitMsg = execFileSync('git', ['log', '-1', '--format=%s'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
395
+ const commitCount = newCommits.split('\n').filter(Boolean).length;
396
+ out(` ${c.green('✓')} ${c.dim(commitHash)} (${commitCount} commit${commitCount > 1 ? 's' : ''} by assistant)\n\n`);
397
+ emitEvent('improve:commit', { cycle, commitHash, description: commitMsg.slice(0, 70) });
398
+ cycles.push({ cycle, outcome: 'success', description: commitMsg.slice(0, 70), filesChanged: [], commitHash });
399
+ completedWork.push(commitMsg.slice(0, 80));
400
+ consecutiveFailures = 0;
401
+ continue;
402
+ }
403
+ } catch { /* git check failed — fall through to normal flow */ }
404
+
405
+ if (!testsPassing) {
406
+ // Record what was attempted so next cycle can learn from it
407
+ const failSummary = discovery.split('\n').filter(l => l.trim()).slice(0, 2).join(' ').slice(0, 100);
408
+ completedWork.push(`FAILED: ${failSummary} (tests broke, rolled back — try a different approach)`);
409
+ rollback(worktreeDir);
410
+ const elapsed = Math.round((Date.now() - cycleStart) / 1000);
411
+ cycles.push({ cycle, outcome: 'failure', description: `Tests failed after ${attempt} attempts (${elapsed}s)`, filesChanged: getChangedFiles(worktreeDir) });
412
+ consecutiveFailures++;
413
+ continue;
414
+ }
415
+
416
+ // Step 7: Commit in worktree (exclude symlinks and node_modules)
417
+ try {
418
+ // Stage all changes
419
+ execFileSync('git', ['add', '--all'], { cwd: worktreeDir, stdio: 'pipe' });
420
+
421
+ // Check if there's actually anything staged
422
+ const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
423
+ if (!staged) {
424
+ out(` ${c.dim('Skip: assistant made no file changes')}\n\n`);
425
+ cycles.push({ cycle, outcome: 'skip', description: 'No staged changes after fix', filesChanged: [] });
426
+ continue;
427
+ }
428
+
429
+ // Build commit message from staged files + discovery text
430
+ const stagedFiles = staged.split('\n').filter(Boolean);
431
+ const srcFiles = stagedFiles.filter(f => f.startsWith('src/')).map(f => path.basename(f, '.ts'));
432
+ const testFiles = stagedFiles.filter(f => f.startsWith('tests/') || f.includes('.test.'));
433
+ const fileNames = srcFiles.length > 0 ? srcFiles.join(', ') : stagedFiles.map(f => path.basename(f, '.ts')).join(', ');
434
+
435
+ const descLine = discovery
436
+ .split('\n')
437
+ .map(l => l.trim())
438
+ .filter(l => l.length > 15)
439
+ .filter(l => !/^(here|let me|i found|i'll|looking|checking|reading|good$|now |you are|step \d|important|first|use )/i.test(l))
440
+ .map(l => l.replace(/^\*\*|^\d+\.\s*|\*\*$|^[-•]\s*/g, '').trim())
441
+ .find(l => /test|cover|fix|add|miss|error|handl|improv|bug|reliab|word.bound/i.test(l)) ?? '';
442
+
443
+ const commitDescription = descLine
444
+ ? descLine.slice(0, 60)
445
+ : testFiles.length > 0
446
+ ? `add tests for ${fileNames}`.slice(0, 60)
447
+ : `improve ${fileNames}`.slice(0, 60);
448
+ const commitMsg = `[improve] ${commitDescription}`;
449
+
450
+ execFileSync('git', ['commit', '-m', `${commitMsg}\n\nCo-authored-by: Weaver Assistant <weaver@synergenius.dev>`], { cwd: worktreeDir, stdio: 'pipe' });
451
+ const hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
452
+ out(` ${c.green('✓')} ${c.dim(hash)} ${commitMsg}\n\n`);
453
+ emitEvent('improve:commit', { cycle, commitHash: hash, description: commitDescription });
454
+ cycles.push({ cycle, outcome: 'success', description: commitDescription, filesChanged: stagedFiles, commitHash: hash });
455
+ completedWork.push(`${commitDescription} (${stagedFiles.join(', ')})`);
456
+ consecutiveFailures = 0;
457
+ } catch (err) {
458
+ out(` ${c.red('✗')} Commit failed: ${err instanceof Error ? err.message.split('\n')[0] : 'unknown'}\n`);
459
+ rollback(worktreeDir);
460
+ cycles.push({ cycle, outcome: 'failure', description: 'Commit failed', filesChanged: getChangedFiles(worktreeDir), error: String(err) });
461
+ consecutiveFailures++;
462
+ }
463
+ }
464
+
465
+ const result: ImproveResult = {
466
+ totalCycles: cycles.length,
467
+ successes: cycles.filter(cy => cy.outcome === 'success').length,
468
+ failures: cycles.filter(cy => cy.outcome === 'failure').length,
469
+ skips: cycles.filter(cy => cy.outcome === 'skip').length,
470
+ blocked: cycles.filter(cy => cy.outcome === 'blocked').length,
471
+ cycles,
472
+ startedAt,
473
+ finishedAt: new Date().toISOString(),
474
+ branch: branchName,
475
+ worktreePath: worktreeDir,
476
+ reason: consecutiveFailures >= maxConsecutiveFailures ? 'max-failures'
477
+ : cycles.some(cy => cy.description === 'Nothing to improve') ? 'nothing-to-improve'
478
+ : 'max-cycles',
479
+ };
480
+
481
+ // Release sleep inhibitor
482
+ if (caffeinate) {
483
+ try { caffeinate.kill(); } catch { /* already dead */ }
484
+ }
485
+
486
+ // Emit completion event
487
+ emitEvent('improve:complete', { successes: cycles.filter(cy => cy.outcome === 'success').length, failures: cycles.filter(cy => cy.outcome === 'failure').length, totalCycles: cycles.length });
488
+
489
+ // Summary
490
+ out(`\n ${c.bold('=== Improve Complete ===')}\n`);
491
+ out(` ${result.successes} committed, ${result.failures} rolled back, ${result.skips} skipped, ${result.blocked} blocked\n`);
492
+ out(` Branch: ${c.cyan(branchName)}\n`);
493
+ if (result.successes > 0) {
494
+ out(`\n ${c.bold('Commits:')}\n`);
495
+ for (const cy of cycles.filter(cy => cy.outcome === 'success')) {
496
+ out(` ${c.green(cy.commitHash!)} ${cy.description}\n`);
497
+ }
498
+ out(`\n Review: ${c.cyan(`git log main..${branchName}`)}\n`);
499
+ out(` Merge: ${c.cyan(`git merge ${branchName}`)}\n`);
500
+ }
501
+ if (result.successes === 0) {
502
+ out(`\n No changes made. Cleaning up worktree.\n`);
503
+ cleanup(projectDir, worktreeDir);
504
+ } else {
505
+ out(`\n ${c.dim(`Worktree kept at: ${path.relative(projectDir, worktreeDir)}`)}\n`);
506
+ out(` ${c.dim(`Clean up: git worktree remove ${path.relative(projectDir, worktreeDir)}`)}\n`);
507
+ }
508
+ out('\n');
509
+
510
+ // Persist summary
511
+ try {
512
+ const summaryDir = path.join(os.homedir(), '.weaver', 'improve');
513
+ fs.mkdirSync(summaryDir, { recursive: true });
514
+ fs.writeFileSync(
515
+ path.join(summaryDir, `run-${new Date().toISOString().replace(/[:.]/g, '-')}.json`),
516
+ JSON.stringify(result, null, 2), 'utf-8',
517
+ );
518
+ } catch { /* non-fatal */ }
519
+
520
+ return result;
521
+ }
522
+
523
+ function emptyResult(startedAt: string, branch: string, worktreePath: string, reason: ImproveResult['reason']): ImproveResult {
524
+ return { totalCycles: 0, successes: 0, failures: 0, skips: 0, blocked: 0, cycles: [], startedAt, finishedAt: new Date().toISOString(), branch, worktreePath, reason };
525
+ }
526
+
527
+ // --- Helpers ---
528
+
529
+ let cachedProvider: unknown = null;
530
+ let cachedTools: unknown[] = [];
531
+ let cachedExecutor: unknown = null;
532
+
533
+ async function runAssistantInDir(worktreeDir: string, message: string, _conversationId: string, steeringEngine?: import('./steering-engine.js').SteeringEngine): Promise<string> {
534
+ const originalCwd = process.cwd();
535
+ process.chdir(worktreeDir);
536
+
537
+ try {
538
+ // Reuse provider across cycles (keeps Claude CLI subprocess alive)
539
+ if (!cachedProvider) {
540
+ const agentMod = await import('@synergenius/flow-weaver/agent');
541
+ const { ASSISTANT_TOOLS, createAssistantExecutor } = await import('./assistant-tools.js');
542
+ cachedTools = ASSISTANT_TOOLS;
543
+ cachedExecutor = createAssistantExecutor(worktreeDir, steeringEngine);
544
+
545
+ if (process.env.ANTHROPIC_API_KEY) {
546
+ cachedProvider = agentMod.createAnthropicProvider({
547
+ apiKey: process.env.ANTHROPIC_API_KEY,
548
+ });
549
+ } else {
550
+ // Disable Claude CLI's built-in file tools so it uses our MCP-bridged
551
+ // tools which respect projectDir (critical for worktree isolation)
552
+ cachedProvider = agentMod.createClaudeCliProvider({
553
+ cwd: worktreeDir,
554
+ disallowedTools: ['Read', 'Edit', 'Write', 'MultiEdit'],
555
+ });
556
+ }
557
+ }
558
+
559
+ const { runAgentLoop } = await import('@synergenius/flow-weaver/agent');
560
+
561
+ let responseText = '';
562
+ const toolCalls: Array<{ name: string; isError: boolean }> = [];
563
+
564
+ const result = await runAgentLoop(
565
+ cachedProvider as any,
566
+ cachedTools as any,
567
+ cachedExecutor as any,
568
+ [{ role: 'user' as const, content: message }],
569
+ {
570
+ maxIterations: 20,
571
+ onStreamEvent: (e: any) => {
572
+ if (e.type === 'text_delta') {
573
+ responseText += e.text;
574
+ process.stderr.write(e.text);
575
+ }
576
+ },
577
+ onToolEvent: (e: any) => {
578
+ if (e.type === 'tool_call_start') {
579
+ process.stderr.write(`\n ${e.name} `);
580
+ }
581
+ if (e.type === 'tool_call_result') {
582
+ toolCalls.push({ name: e.name ?? '', isError: !!e.isError });
583
+ process.stderr.write(e.isError ? '✗ ' : '✓ ');
584
+ }
585
+ },
586
+ },
587
+ );
588
+
589
+ return JSON.stringify({
590
+ response: responseText,
591
+ toolCalls,
592
+ tokensUsed: result.usage.promptTokens + result.usage.completionTokens,
593
+ });
594
+ } finally {
595
+ process.chdir(originalCwd);
596
+ }
597
+ }
598
+
599
+ function getChangedFiles(dir: string): string[] {
600
+ try {
601
+ const modified = execFileSync('git', ['diff', '--name-only'], { cwd: dir, encoding: 'utf-8' }).trim();
602
+ const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd: dir, encoding: 'utf-8' }).trim();
603
+ return [...modified.split('\n'), ...untracked.split('\n')]
604
+ .filter(Boolean)
605
+ .filter(f => !f.startsWith('node_modules') && !f.includes('/node_modules/'));
606
+ } catch { return []; }
607
+ }
608
+
609
+ function rollback(dir: string): void {
610
+ try {
611
+ execFileSync('git', ['checkout', '.'], { cwd: dir, stdio: 'pipe' });
612
+ execFileSync('git', ['clean', '-fd'], { cwd: dir, stdio: 'pipe' });
613
+ } catch { /* best effort */ }
614
+ }
615
+
616
+ function cleanup(projectDir: string, worktreeDir: string): void {
617
+ // Safety: stash any staged work before removing worktree so blobs survive in git
618
+ try {
619
+ const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: worktreeDir, encoding: 'utf-8' }).trim();
620
+ if (staged) {
621
+ execFileSync('git', ['stash', 'push', '-m', `weaver-improve: work-in-progress (${staged.split('\n').length} files)`], { cwd: worktreeDir, stdio: 'pipe' });
622
+ }
623
+ } catch { /* stash failed — work may be lost but at least blobs are in git objects if staged */ }
624
+
625
+ try {
626
+ execFileSync('git', ['worktree', 'remove', worktreeDir, '--force'], { cwd: projectDir, stdio: 'pipe' });
627
+ } catch { /* best effort */ }
628
+ try {
629
+ const parent = path.dirname(worktreeDir);
630
+ if (fs.existsSync(parent) && fs.readdirSync(parent).length === 0) {
631
+ fs.rmdirSync(parent);
632
+ }
633
+ } catch { /* best effort */ }
634
+ }
635
+
636
+ function isProtected(file: string, patterns: string[]): boolean {
637
+ for (const pattern of patterns) {
638
+ const regex = new RegExp(
639
+ '^' + pattern
640
+ .replace(/\./g, '\\.')
641
+ .replace(/\*\*/g, '{{DS}}')
642
+ .replace(/\*/g, '[^/]*')
643
+ .replace(/\{\{DS\}\}/g, '.*')
644
+ + '$',
645
+ );
646
+ if (regex.test(file)) return true;
647
+ }
648
+ return false;
649
+ }
650
+
651
+ export { DEFAULT_PROTECTED };