@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.
- package/dist/bot/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/agent-provider.d.ts.map +1 -1
- package/dist/bot/agent-provider.js +3 -2
- package/dist/bot/agent-provider.js.map +1 -1
- package/dist/bot/approvals.js +17 -8
- package/dist/bot/approvals.js.map +1 -1
- package/dist/bot/assistant-core.d.ts +17 -0
- package/dist/bot/assistant-core.d.ts.map +1 -1
- package/dist/bot/assistant-core.js +418 -60
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/assistant-tools.d.ts +1 -1
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +283 -9
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/bot-agent-channel.d.ts.map +1 -1
- package/dist/bot/bot-agent-channel.js +2 -0
- package/dist/bot/bot-agent-channel.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +4 -0
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +72 -27
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +6 -5
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js +98 -42
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/cost-store.d.ts +3 -0
- package/dist/bot/cost-store.d.ts.map +1 -1
- package/dist/bot/cost-store.js +21 -10
- package/dist/bot/cost-store.js.map +1 -1
- package/dist/bot/cost-tracker.d.ts.map +1 -1
- package/dist/bot/cost-tracker.js +14 -1
- package/dist/bot/cost-tracker.js.map +1 -1
- package/dist/bot/cron-parser.d.ts.map +1 -1
- package/dist/bot/cron-parser.js +2 -0
- package/dist/bot/cron-parser.js.map +1 -1
- package/dist/bot/cron-scheduler.d.ts.map +1 -1
- package/dist/bot/cron-scheduler.js +1 -0
- package/dist/bot/cron-scheduler.js.map +1 -1
- package/dist/bot/device-connection.d.ts +13 -0
- package/dist/bot/device-connection.d.ts.map +1 -0
- package/dist/bot/device-connection.js +102 -0
- package/dist/bot/device-connection.js.map +1 -0
- package/dist/bot/error-classifier.d.ts.map +1 -1
- package/dist/bot/error-classifier.js +5 -0
- package/dist/bot/error-classifier.js.map +1 -1
- package/dist/bot/file-lock.d.ts.map +1 -1
- package/dist/bot/file-lock.js +13 -3
- package/dist/bot/file-lock.js.map +1 -1
- package/dist/bot/file-watcher.d.ts.map +1 -1
- package/dist/bot/file-watcher.js +1 -0
- package/dist/bot/file-watcher.js.map +1 -1
- package/dist/bot/genesis-prompt-context.d.ts +5 -0
- package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
- package/dist/bot/genesis-prompt-context.js +55 -0
- package/dist/bot/genesis-prompt-context.js.map +1 -1
- package/dist/bot/genesis-store.d.ts +4 -0
- package/dist/bot/genesis-store.d.ts.map +1 -1
- package/dist/bot/genesis-store.js +79 -12
- package/dist/bot/genesis-store.js.map +1 -1
- package/dist/bot/improve-loop.d.ts +46 -0
- package/dist/bot/improve-loop.d.ts.map +1 -0
- package/dist/bot/improve-loop.js +592 -0
- package/dist/bot/improve-loop.js.map +1 -0
- package/dist/bot/insight-engine.d.ts +12 -0
- package/dist/bot/insight-engine.d.ts.map +1 -0
- package/dist/bot/insight-engine.js +256 -0
- package/dist/bot/insight-engine.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +4 -1
- package/dist/bot/knowledge-store.js.map +1 -1
- package/dist/bot/pipeline-runner.d.ts.map +1 -1
- package/dist/bot/pipeline-runner.js +12 -4
- package/dist/bot/pipeline-runner.js.map +1 -1
- package/dist/bot/project-model.d.ts +25 -0
- package/dist/bot/project-model.d.ts.map +1 -0
- package/dist/bot/project-model.js +372 -0
- package/dist/bot/project-model.js.map +1 -0
- package/dist/bot/response-formatter.js +2 -3
- package/dist/bot/response-formatter.js.map +1 -1
- package/dist/bot/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +10 -2
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/safe-path.d.ts +1 -1
- package/dist/bot/safe-path.d.ts.map +1 -1
- package/dist/bot/safe-path.js +20 -1
- package/dist/bot/safe-path.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -2
- package/dist/bot/safety.d.ts.map +1 -1
- package/dist/bot/safety.js +45 -2
- package/dist/bot/safety.js.map +1 -1
- package/dist/bot/session-state.d.ts +4 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +52 -9
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/slash-commands.d.ts.map +1 -1
- package/dist/bot/slash-commands.js +109 -3
- package/dist/bot/slash-commands.js.map +1 -1
- package/dist/bot/steering-engine.d.ts +67 -0
- package/dist/bot/steering-engine.d.ts.map +1 -0
- package/dist/bot/steering-engine.js +198 -0
- package/dist/bot/steering-engine.js.map +1 -0
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +62 -25
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +5 -2
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-queue.d.ts +6 -1
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +43 -4
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/tool-registry.d.ts +1 -1
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +65 -4
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/trust-calculator.d.ts +34 -0
- package/dist/bot/trust-calculator.d.ts.map +1 -0
- package/dist/bot/trust-calculator.js +67 -0
- package/dist/bot/trust-calculator.js.map +1 -0
- package/dist/bot/types.d.ts +97 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/update-checker.d.ts +21 -0
- package/dist/bot/update-checker.d.ts.map +1 -0
- package/dist/bot/update-checker.js +129 -0
- package/dist/bot/update-checker.js.map +1 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +11 -4
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/cli-bridge.d.ts +2 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +3 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +10 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +141 -24
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/weaver-config.md +15 -9
- package/dist/handlers/on-execution-completed.d.ts +11 -0
- package/dist/handlers/on-execution-completed.d.ts.map +1 -0
- package/dist/handlers/on-execution-completed.js +25 -0
- package/dist/handlers/on-execution-completed.js.map +1 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +33 -0
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/genesis-approve.d.ts.map +1 -1
- package/dist/node-types/genesis-approve.js +28 -3
- package/dist/node-types/genesis-approve.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +23 -13
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-propose.d.ts.map +1 -1
- package/dist/node-types/genesis-propose.js +8 -0
- package/dist/node-types/genesis-propose.js.map +1 -1
- package/dist/node-types/genesis-update-history.d.ts.map +1 -1
- package/dist/node-types/genesis-update-history.js +13 -0
- package/dist/node-types/genesis-update-history.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +28 -1
- package/package.json +6 -3
- package/src/bot/agent-provider.ts +3 -2
- package/src/bot/approvals.ts +16 -8
- package/src/bot/assistant-core.ts +420 -63
- package/src/bot/assistant-tools.ts +291 -9
- package/src/bot/bot-agent-channel.ts +2 -0
- package/src/bot/bot-manager.ts +70 -29
- package/src/bot/conversation-store.ts +87 -42
- package/src/bot/cost-store.ts +20 -9
- package/src/bot/cost-tracker.ts +13 -1
- package/src/bot/cron-parser.ts +1 -0
- package/src/bot/cron-scheduler.ts +1 -0
- package/src/bot/device-connection.ts +102 -0
- package/src/bot/error-classifier.ts +5 -0
- package/src/bot/file-lock.ts +12 -2
- package/src/bot/file-watcher.ts +1 -0
- package/src/bot/genesis-prompt-context.ts +61 -0
- package/src/bot/genesis-store.ts +68 -16
- package/src/bot/improve-loop.ts +651 -0
- package/src/bot/insight-engine.ts +273 -0
- package/src/bot/knowledge-store.ts +4 -1
- package/src/bot/pipeline-runner.ts +11 -6
- package/src/bot/project-model.ts +404 -0
- package/src/bot/response-formatter.ts +2 -3
- package/src/bot/run-store.ts +5 -2
- package/src/bot/safe-path.ts +20 -1
- package/src/bot/safety.ts +57 -3
- package/src/bot/session-state.ts +47 -7
- package/src/bot/slash-commands.ts +111 -3
- package/src/bot/steering-engine.ts +233 -0
- package/src/bot/step-executor.ts +66 -26
- package/src/bot/system-prompt.ts +5 -2
- package/src/bot/task-queue.ts +40 -4
- package/src/bot/tool-registry.ts +67 -5
- package/src/bot/trust-calculator.ts +87 -0
- package/src/bot/types.ts +104 -0
- package/src/bot/update-checker.ts +138 -0
- package/src/bot/weaver-tools.ts +10 -4
- package/src/cli-bridge.ts +4 -1
- package/src/cli-handlers.ts +150 -29
- package/src/handlers/on-execution-completed.ts +30 -0
- package/src/mcp-tools.ts +38 -0
- package/src/node-types/genesis-approve.ts +28 -3
- package/src/node-types/genesis-observe.ts +23 -12
- package/src/node-types/genesis-propose.ts +8 -0
- package/src/node-types/genesis-update-history.ts +12 -0
- package/src/ui/evolution-panel.tsx +96 -0
- 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 };
|