@synergenius/flow-weaver-pack-weaver 0.9.0 → 0.9.3
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/ai-client.d.ts +22 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +168 -20
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/assistant-core.d.ts +25 -0
- package/dist/bot/assistant-core.d.ts.map +1 -0
- package/dist/bot/assistant-core.js +265 -0
- package/dist/bot/assistant-core.js.map +1 -0
- package/dist/bot/assistant-tools.d.ts +9 -0
- package/dist/bot/assistant-tools.d.ts.map +1 -0
- package/dist/bot/assistant-tools.js +602 -0
- package/dist/bot/assistant-tools.js.map +1 -0
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +9 -5
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +49 -0
- package/dist/bot/bot-manager.d.ts.map +1 -0
- package/dist/bot/bot-manager.js +279 -0
- package/dist/bot/bot-manager.js.map +1 -0
- package/dist/bot/child-process-tracker.d.ts +6 -0
- package/dist/bot/child-process-tracker.d.ts.map +1 -0
- package/dist/bot/child-process-tracker.js +35 -0
- package/dist/bot/child-process-tracker.js.map +1 -0
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +13 -8
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +40 -0
- package/dist/bot/conversation-store.d.ts.map +1 -0
- package/dist/bot/conversation-store.js +182 -0
- package/dist/bot/conversation-store.js.map +1 -0
- package/dist/bot/error-guide.d.ts +10 -0
- package/dist/bot/error-guide.d.ts.map +1 -0
- package/dist/bot/error-guide.js +34 -0
- package/dist/bot/error-guide.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +17 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -0
- package/dist/bot/knowledge-store.js +53 -0
- package/dist/bot/knowledge-store.js.map +1 -0
- package/dist/bot/retry-utils.d.ts +19 -0
- package/dist/bot/retry-utils.d.ts.map +1 -0
- package/dist/bot/retry-utils.js +64 -0
- package/dist/bot/retry-utils.js.map +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +12 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +3 -1
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/steering.js +1 -1
- package/dist/bot/steering.js.map +1 -1
- package/dist/bot/step-executor.d.ts +10 -5
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +252 -3
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +69 -43
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-decomposer.d.ts +24 -0
- package/dist/bot/task-decomposer.d.ts.map +1 -0
- package/dist/bot/task-decomposer.js +75 -0
- package/dist/bot/task-decomposer.js.map +1 -0
- package/dist/bot/task-queue.d.ts +17 -4
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +95 -4
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/terminal-renderer.d.ts +60 -0
- package/dist/bot/terminal-renderer.d.ts.map +1 -0
- package/dist/bot/terminal-renderer.js +205 -0
- package/dist/bot/terminal-renderer.js.map +1 -0
- package/dist/bot/types.d.ts +7 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +18 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -0
- package/dist/bot/weaver-tools.js +215 -0
- package/dist/bot/weaver-tools.js.map +1 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +5 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +13 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +616 -48
- package/dist/cli-handlers.js.map +1 -1
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/abort-task.d.ts.map +1 -1
- package/dist/node-types/abort-task.js +4 -3
- package/dist/node-types/abort-task.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +38 -0
- package/dist/node-types/agent-execute.d.ts.map +1 -0
- package/dist/node-types/agent-execute.js +256 -0
- package/dist/node-types/agent-execute.js.map +1 -0
- package/dist/node-types/bot-report.d.ts +5 -3
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +39 -7
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts +3 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +108 -24
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/detect-provider.d.ts +2 -2
- package/dist/node-types/detect-provider.d.ts.map +1 -1
- package/dist/node-types/detect-provider.js +3 -1
- package/dist/node-types/detect-provider.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +43 -6
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/execute-plan.d.ts.map +1 -1
- package/dist/node-types/execute-plan.js +31 -8
- package/dist/node-types/execute-plan.js.map +1 -1
- package/dist/node-types/execute-target.d.ts.map +1 -1
- package/dist/node-types/execute-target.js +3 -1
- package/dist/node-types/execute-target.js.map +1 -1
- package/dist/node-types/fix-errors.d.ts.map +1 -1
- package/dist/node-types/fix-errors.js +21 -5
- package/dist/node-types/fix-errors.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +3 -1
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-report.js +4 -1
- package/dist/node-types/genesis-report.js.map +1 -1
- package/dist/node-types/git-ops.d.ts.map +1 -1
- package/dist/node-types/git-ops.js +98 -4
- package/dist/node-types/git-ops.js.map +1 -1
- package/dist/node-types/index.d.ts +2 -0
- package/dist/node-types/index.d.ts.map +1 -1
- package/dist/node-types/index.js +2 -0
- package/dist/node-types/index.js.map +1 -1
- package/dist/node-types/load-config.d.ts +2 -2
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +14 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/read-workflow.js +8 -2
- package/dist/node-types/read-workflow.js.map +1 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +35 -26
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/send-notify.js +2 -1
- package/dist/node-types/send-notify.js.map +1 -1
- package/dist/node-types/validate-gate.d.ts +18 -0
- package/dist/node-types/validate-gate.d.ts.map +1 -0
- package/dist/node-types/validate-gate.js +96 -0
- package/dist/node-types/validate-gate.js.map +1 -0
- package/dist/workflows/genesis-task.d.ts +20 -12
- package/dist/workflows/genesis-task.d.ts.map +1 -1
- package/dist/workflows/genesis-task.js +20 -12
- package/dist/workflows/genesis-task.js.map +1 -1
- package/dist/workflows/weaver-agent.d.ts +35 -0
- package/dist/workflows/weaver-agent.d.ts.map +1 -0
- package/dist/workflows/weaver-agent.js +777 -0
- package/dist/workflows/weaver-agent.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.d.ts +19 -26
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +1043 -27
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +21 -35
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +1119 -36
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +21 -1
- package/package.json +5 -2
- package/src/bot/ai-client.ts +180 -19
- package/src/bot/assistant-core.ts +306 -0
- package/src/bot/assistant-tools.ts +605 -0
- package/src/bot/audit-logger.ts +6 -5
- package/src/bot/bot-manager.ts +293 -0
- package/src/bot/child-process-tracker.ts +40 -0
- package/src/bot/cli-provider.ts +13 -8
- package/src/bot/conversation-store.ts +222 -0
- package/src/bot/error-guide.ts +34 -0
- package/src/bot/knowledge-store.ts +59 -0
- package/src/bot/retry-utils.ts +76 -0
- package/src/bot/runner.ts +12 -1
- package/src/bot/session-state.ts +2 -1
- package/src/bot/steering.ts +1 -1
- package/src/bot/step-executor.ts +313 -5
- package/src/bot/system-prompt.ts +70 -47
- package/src/bot/task-decomposer.ts +100 -0
- package/src/bot/task-queue.ts +113 -7
- package/src/bot/terminal-renderer.ts +241 -0
- package/src/bot/types.ts +8 -0
- package/src/bot/weaver-tools.ts +225 -0
- package/src/cli-bridge.ts +7 -1
- package/src/cli-handlers.ts +625 -48
- package/src/mcp-tools.ts +2 -2
- package/src/node-types/abort-task.ts +5 -4
- package/src/node-types/agent-execute.ts +306 -0
- package/src/node-types/bot-report.ts +40 -9
- package/src/node-types/build-context.ts +112 -25
- package/src/node-types/detect-provider.ts +4 -3
- package/src/node-types/exec-validate-retry.ts +47 -8
- package/src/node-types/execute-plan.ts +32 -8
- package/src/node-types/execute-target.ts +2 -1
- package/src/node-types/fix-errors.ts +20 -5
- package/src/node-types/genesis-observe.ts +2 -1
- package/src/node-types/genesis-report.ts +1 -1
- package/src/node-types/git-ops.ts +93 -4
- package/src/node-types/index.ts +2 -0
- package/src/node-types/load-config.ts +3 -3
- package/src/node-types/plan-task.ts +15 -3
- package/src/node-types/read-workflow.ts +2 -2
- package/src/node-types/receive-task.ts +31 -26
- package/src/node-types/send-notify.ts +1 -1
- package/src/node-types/validate-gate.ts +112 -0
- package/src/workflows/genesis-task.ts +20 -12
- package/src/workflows/weaver-agent.ts +799 -0
- package/src/workflows/weaver-bot-batch.ts +1049 -27
- package/src/workflows/weaver-bot.ts +1123 -36
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot Manager — spawns and manages multiple weaver bot sessions
|
|
3
|
+
* as separate processes. Each bot has its own queue, steering file,
|
|
4
|
+
* and output log under ~/.weaver/bots/{name}/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, execFileSync, type ChildProcess } from 'node:child_process';
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import { TaskQueue } from './task-queue.js';
|
|
12
|
+
import { SteeringController } from './steering.js';
|
|
13
|
+
|
|
14
|
+
export interface ManagedBot {
|
|
15
|
+
name: string;
|
|
16
|
+
pid: number;
|
|
17
|
+
projectDir: string;
|
|
18
|
+
botDir: string;
|
|
19
|
+
startedAt: number;
|
|
20
|
+
status: 'running' | 'paused' | 'stopped';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SpawnOpts {
|
|
24
|
+
projectDir: string;
|
|
25
|
+
parallel?: number;
|
|
26
|
+
deadline?: string;
|
|
27
|
+
autoApprove?: boolean;
|
|
28
|
+
/** Git branch for commits (creates if needed). Keeps main clean for overnight runs. */
|
|
29
|
+
branch?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
|
|
33
|
+
|
|
34
|
+
export class BotManager {
|
|
35
|
+
private bots = new Map<string, { meta: ManagedBot; process: ChildProcess }>();
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
// Ensure base dir exists
|
|
39
|
+
fs.mkdirSync(BOTS_DIR, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// Clean up on process exit
|
|
42
|
+
const cleanup = () => this.cleanup();
|
|
43
|
+
process.on('exit', cleanup);
|
|
44
|
+
process.on('SIGTERM', cleanup);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
spawn(name: string, opts: SpawnOpts): ManagedBot {
|
|
48
|
+
if (this.bots.has(name)) {
|
|
49
|
+
throw new Error(`Bot "${name}" already exists. Stop it first or use a different name.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const botDir = path.join(BOTS_DIR, name);
|
|
53
|
+
fs.mkdirSync(botDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
// Create git branch if specified (keeps main clean for overnight runs)
|
|
56
|
+
if (opts.branch) {
|
|
57
|
+
try {
|
|
58
|
+
execFileSync('git', ['checkout', '-B', opts.branch], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
59
|
+
} catch { /* branch may already exist */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const logPath = path.join(botDir, 'output.log');
|
|
63
|
+
// Touch the file synchronously so it exists immediately after spawn() returns
|
|
64
|
+
fs.writeFileSync(logPath, '', { flag: 'a' });
|
|
65
|
+
const logStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
66
|
+
|
|
67
|
+
const sessionArgs = [
|
|
68
|
+
'flow-weaver', 'weaver', 'session',
|
|
69
|
+
'--continuous',
|
|
70
|
+
'--project-dir', opts.projectDir,
|
|
71
|
+
];
|
|
72
|
+
if (opts.autoApprove !== false) sessionArgs.push('--auto-approve');
|
|
73
|
+
if (opts.parallel && opts.parallel > 1) sessionArgs.push('--parallel', String(opts.parallel));
|
|
74
|
+
if (opts.deadline) sessionArgs.push('--until', opts.deadline);
|
|
75
|
+
|
|
76
|
+
// Prevent system sleep during long runs (cross-platform)
|
|
77
|
+
const { cmd, args } = wrapWithSleepInhibitor('npx', sessionArgs);
|
|
78
|
+
|
|
79
|
+
// Set queue/steering to bot-specific paths
|
|
80
|
+
const env = {
|
|
81
|
+
...process.env,
|
|
82
|
+
WEAVER_QUEUE_DIR: botDir,
|
|
83
|
+
WEAVER_STEERING_DIR: botDir,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const child = spawn(cmd, args, {
|
|
87
|
+
cwd: opts.projectDir,
|
|
88
|
+
env,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
detached: false,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Capture output to log file
|
|
94
|
+
child.stdout?.pipe(logStream);
|
|
95
|
+
child.stderr?.pipe(logStream);
|
|
96
|
+
|
|
97
|
+
const meta: ManagedBot = {
|
|
98
|
+
name,
|
|
99
|
+
pid: child.pid ?? 0,
|
|
100
|
+
projectDir: opts.projectDir,
|
|
101
|
+
botDir,
|
|
102
|
+
startedAt: Date.now(),
|
|
103
|
+
status: 'running',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Write metadata for persistence
|
|
107
|
+
fs.writeFileSync(path.join(botDir, 'meta.json'), JSON.stringify(meta, null, 2));
|
|
108
|
+
|
|
109
|
+
child.on('exit', (code) => {
|
|
110
|
+
const bot = this.bots.get(name);
|
|
111
|
+
if (bot) {
|
|
112
|
+
bot.meta.status = 'stopped';
|
|
113
|
+
fs.writeFileSync(path.join(botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
|
|
114
|
+
}
|
|
115
|
+
logStream.write(`\n[bot-manager] Process exited with code ${code}\n`);
|
|
116
|
+
logStream.end();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.bots.set(name, { meta, process: child });
|
|
120
|
+
return meta;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
list(): ManagedBot[] {
|
|
124
|
+
this.discoverExistingBots();
|
|
125
|
+
// Health check: update status of bots that died
|
|
126
|
+
for (const [, bot] of this.bots) {
|
|
127
|
+
if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
|
|
128
|
+
bot.meta.status = 'stopped';
|
|
129
|
+
try {
|
|
130
|
+
fs.writeFileSync(path.join(bot.meta.botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
|
|
131
|
+
} catch { /* non-fatal */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...this.bots.values()].map(b => b.meta);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get(name: string): ManagedBot | null {
|
|
138
|
+
const bot = this.bots.get(name);
|
|
139
|
+
if (!bot) return null;
|
|
140
|
+
// Health check on access
|
|
141
|
+
if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
|
|
142
|
+
bot.meta.status = 'stopped';
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(path.join(bot.meta.botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
|
|
145
|
+
} catch { /* non-fatal */ }
|
|
146
|
+
}
|
|
147
|
+
return bot.meta;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Check if a bot process is still alive. */
|
|
151
|
+
private isAlive(bot: ManagedBot): boolean {
|
|
152
|
+
if (!bot.pid || bot.pid === 0) return false;
|
|
153
|
+
try { process.kill(bot.pid, 0); return true; } catch { return false; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
getQueue(name: string): TaskQueue {
|
|
157
|
+
const bot = this.bots.get(name);
|
|
158
|
+
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
159
|
+
return new TaskQueue(bot.meta.botDir);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSteering(name: string): SteeringController {
|
|
163
|
+
const bot = this.bots.get(name);
|
|
164
|
+
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
165
|
+
return new SteeringController(bot.meta.botDir);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async steer(name: string, command: 'pause' | 'resume' | 'cancel'): Promise<void> {
|
|
169
|
+
const steering = this.getSteering(name);
|
|
170
|
+
await steering.write({ command, timestamp: Date.now() });
|
|
171
|
+
if (command === 'pause') {
|
|
172
|
+
const bot = this.bots.get(name);
|
|
173
|
+
if (bot) bot.meta.status = 'paused';
|
|
174
|
+
} else if (command === 'resume') {
|
|
175
|
+
const bot = this.bots.get(name);
|
|
176
|
+
if (bot) bot.meta.status = 'running';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
stop(name: string): void {
|
|
181
|
+
const bot = this.bots.get(name);
|
|
182
|
+
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
183
|
+
// Send SIGTERM for graceful shutdown
|
|
184
|
+
if (bot.process.pid && !bot.process.killed) {
|
|
185
|
+
bot.process.kill('SIGTERM');
|
|
186
|
+
}
|
|
187
|
+
bot.meta.status = 'stopped';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
kill(name: string): void {
|
|
191
|
+
const bot = this.bots.get(name);
|
|
192
|
+
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
193
|
+
if (bot.process.pid && !bot.process.killed) {
|
|
194
|
+
bot.process.kill('SIGKILL');
|
|
195
|
+
}
|
|
196
|
+
bot.meta.status = 'stopped';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
logs(name: string, lines = 50): string {
|
|
200
|
+
const bot = this.bots.get(name);
|
|
201
|
+
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
202
|
+
const logPath = path.join(bot.meta.botDir, 'output.log');
|
|
203
|
+
if (!fs.existsSync(logPath)) return '(no logs yet)';
|
|
204
|
+
const content = fs.readFileSync(logPath, 'utf-8');
|
|
205
|
+
const allLines = content.split('\n');
|
|
206
|
+
return allLines.slice(-lines).join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cleanup(): void {
|
|
210
|
+
for (const [, bot] of this.bots) {
|
|
211
|
+
if (bot.process.pid && !bot.process.killed) {
|
|
212
|
+
try { bot.process.kill('SIGTERM'); } catch (err) {
|
|
213
|
+
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] SIGTERM failed for bot: ${err}\n`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Discover bots from disk that were spawned by a previous assistant session. */
|
|
220
|
+
private discoverExistingBots(): void {
|
|
221
|
+
if (!fs.existsSync(BOTS_DIR)) return;
|
|
222
|
+
for (const name of fs.readdirSync(BOTS_DIR)) {
|
|
223
|
+
if (this.bots.has(name)) continue;
|
|
224
|
+
const metaPath = path.join(BOTS_DIR, name, 'meta.json');
|
|
225
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
226
|
+
try {
|
|
227
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) as ManagedBot;
|
|
228
|
+
// Check if process is still running
|
|
229
|
+
if (meta.pid > 0) {
|
|
230
|
+
try {
|
|
231
|
+
process.kill(meta.pid, 0); // test if process exists
|
|
232
|
+
meta.status = 'running';
|
|
233
|
+
} catch {
|
|
234
|
+
meta.status = 'stopped';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Store without a process handle (can only steer via file, not kill directly)
|
|
238
|
+
this.bots.set(name, { meta, process: null as unknown as ChildProcess });
|
|
239
|
+
} catch { /* corrupt meta */ }
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Cross-platform sleep inhibitor. Wraps a command to prevent the OS from sleeping.
|
|
246
|
+
* - macOS: caffeinate -i -s
|
|
247
|
+
* - Linux: systemd-inhibit (if available)
|
|
248
|
+
* - Windows/other: no wrapper (runs command directly)
|
|
249
|
+
*/
|
|
250
|
+
function wrapWithSleepInhibitor(command: string, args: string[]): { cmd: string; args: string[] } {
|
|
251
|
+
switch (process.platform) {
|
|
252
|
+
case 'darwin':
|
|
253
|
+
return { cmd: 'caffeinate', args: ['-i', '-s', command, ...args] };
|
|
254
|
+
case 'linux': {
|
|
255
|
+
// Check if systemd-inhibit is available
|
|
256
|
+
try {
|
|
257
|
+
execFileSync('which', ['systemd-inhibit'], { stdio: 'pipe' });
|
|
258
|
+
return {
|
|
259
|
+
cmd: 'systemd-inhibit',
|
|
260
|
+
args: ['--what=idle:sleep', '--who=weaver', '--why=Bot session running', command, ...args],
|
|
261
|
+
};
|
|
262
|
+
} catch {
|
|
263
|
+
return { cmd: command, args };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
return { cmd: command, args };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Cross-platform desktop notification.
|
|
273
|
+
* - macOS: osascript
|
|
274
|
+
* - Linux: notify-send (if available)
|
|
275
|
+
* - Windows: PowerShell toast (if available)
|
|
276
|
+
*/
|
|
277
|
+
export function sendDesktopNotification(title: string, message: string): void {
|
|
278
|
+
try {
|
|
279
|
+
switch (process.platform) {
|
|
280
|
+
case 'darwin':
|
|
281
|
+
execFileSync('osascript', ['-e', `display notification "${message}" with title "${title}"`], { stdio: 'ignore' });
|
|
282
|
+
break;
|
|
283
|
+
case 'linux':
|
|
284
|
+
execFileSync('notify-send', [title, message], { stdio: 'ignore' });
|
|
285
|
+
break;
|
|
286
|
+
case 'win32':
|
|
287
|
+
execFileSync('powershell', ['-Command', `[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}','${title}')`], { stdio: 'ignore' });
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
// Non-fatal — notification is best-effort
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks spawned child processes and kills them on SIGINT/SIGTERM.
|
|
5
|
+
* Prevents zombie `claude` processes when user hits Ctrl+C.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const activeChildren = new Set<ChildProcess>();
|
|
9
|
+
let signalHandlersInstalled = false;
|
|
10
|
+
|
|
11
|
+
function installSignalHandlers(): void {
|
|
12
|
+
if (signalHandlersInstalled) return;
|
|
13
|
+
signalHandlersInstalled = true;
|
|
14
|
+
|
|
15
|
+
const cleanup = () => {
|
|
16
|
+
for (const child of activeChildren) {
|
|
17
|
+
try {
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
} catch { /* already dead */ }
|
|
20
|
+
}
|
|
21
|
+
activeChildren.clear();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
process.on('SIGINT', cleanup);
|
|
25
|
+
process.on('SIGTERM', cleanup);
|
|
26
|
+
process.on('exit', cleanup);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Register a child process for cleanup on signal. */
|
|
30
|
+
export function trackChild(child: ChildProcess): void {
|
|
31
|
+
installSignalHandlers();
|
|
32
|
+
activeChildren.add(child);
|
|
33
|
+
child.on('exit', () => activeChildren.delete(child));
|
|
34
|
+
child.on('error', () => activeChildren.delete(child));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Get the count of active tracked children. */
|
|
38
|
+
export function activeChildCount(): number {
|
|
39
|
+
return activeChildren.size;
|
|
40
|
+
}
|
package/src/bot/cli-provider.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
2
2
|
import type { BotAgentProvider, OnUsageCallback, StreamChunk, ToolDefinition, ToolUseResult } from './types.js';
|
|
3
3
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
4
4
|
import { parseStreamLine, extractTextFromChunks } from './cli-stream-parser.js';
|
|
5
|
+
import { trackChild } from './child-process-tracker.js';
|
|
5
6
|
|
|
6
7
|
// Strip CLAUDECODE from child env so nested claude CLI invocations work.
|
|
7
8
|
const childEnv = { ...process.env };
|
|
@@ -29,20 +30,20 @@ export class CliAgentProvider implements BotAgentProvider {
|
|
|
29
30
|
? request.context
|
|
30
31
|
: JSON.stringify(request.context, null, 2);
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const userPrompt = `Context:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
|
|
33
34
|
|
|
34
35
|
if (this.cli === 'claude-cli') {
|
|
35
36
|
const chunks: StreamChunk[] = [];
|
|
36
|
-
for await (const chunk of this.streamRaw(
|
|
37
|
+
for await (const chunk of this.streamRaw(userPrompt, systemPrompt)) {
|
|
37
38
|
chunks.push(chunk);
|
|
38
39
|
}
|
|
39
40
|
const raw = extractTextFromChunks(chunks);
|
|
40
41
|
return this.parseJson(raw);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// copilot-cli: no
|
|
44
|
+
// copilot-cli: no --system-prompt support, keep concatenated
|
|
44
45
|
const raw = execSync('copilot -p --silent --allow-all-tools', {
|
|
45
|
-
input:
|
|
46
|
+
input: systemPrompt + '\n\n' + userPrompt,
|
|
46
47
|
encoding: 'utf-8',
|
|
47
48
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
49
|
timeout: 120_000,
|
|
@@ -64,10 +65,10 @@ export class CliAgentProvider implements BotAgentProvider {
|
|
|
64
65
|
? request.context
|
|
65
66
|
: JSON.stringify(request.context, null, 2);
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
+
const userPrompt = `Context:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
|
|
68
69
|
|
|
69
70
|
if (this.cli === 'claude-cli') {
|
|
70
|
-
yield* this.streamRaw(
|
|
71
|
+
yield* this.streamRaw(userPrompt, systemPrompt);
|
|
71
72
|
} else {
|
|
72
73
|
const result = await this.decide(request);
|
|
73
74
|
yield { type: 'text', text: JSON.stringify(result) };
|
|
@@ -85,18 +86,22 @@ export class CliAgentProvider implements BotAgentProvider {
|
|
|
85
86
|
return { result };
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
private async *streamRaw(
|
|
89
|
+
private async *streamRaw(userPrompt: string, systemPrompt?: string): AsyncGenerator<StreamChunk> {
|
|
89
90
|
const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
90
91
|
if (this.model) {
|
|
91
92
|
args.push('--model', this.model);
|
|
92
93
|
}
|
|
94
|
+
if (systemPrompt) {
|
|
95
|
+
args.push('--system-prompt', systemPrompt);
|
|
96
|
+
}
|
|
93
97
|
|
|
94
98
|
const child = spawn('claude', args, {
|
|
95
99
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
96
100
|
env: childEnv,
|
|
97
101
|
});
|
|
102
|
+
trackChild(child);
|
|
98
103
|
|
|
99
|
-
child.stdin.write(
|
|
104
|
+
child.stdin.write(userPrompt);
|
|
100
105
|
child.stdin.end();
|
|
101
106
|
|
|
102
107
|
const timeout = setTimeout(() => {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Store — persistent conversation history for the assistant.
|
|
3
|
+
* Mirrors the platform's aiConversations/aiChatMessages tables
|
|
4
|
+
* using file-based storage for CLI use.
|
|
5
|
+
*
|
|
6
|
+
* Layout:
|
|
7
|
+
* ~/.weaver/conversations/
|
|
8
|
+
* index.json # ConversationRecord[]
|
|
9
|
+
* {id}/messages.ndjson # StoredMessage[] (append-only)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
16
|
+
import type { AgentMessage } from '@synergenius/flow-weaver/agent';
|
|
17
|
+
import { withFileLock } from './file-lock.js';
|
|
18
|
+
|
|
19
|
+
export interface ConversationRecord {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
projectDir: string;
|
|
23
|
+
messageCount: number;
|
|
24
|
+
totalTokens: number;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
lastMessageAt: number;
|
|
27
|
+
botIds: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StoredMessage {
|
|
31
|
+
role: 'user' | 'assistant' | 'tool';
|
|
32
|
+
content: string;
|
|
33
|
+
toolCalls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
|
|
34
|
+
toolCallId?: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
tokens?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MAX_INDEX_SIZE = 20;
|
|
40
|
+
const CHARS_PER_TOKEN = 4;
|
|
41
|
+
|
|
42
|
+
export class ConversationStore {
|
|
43
|
+
private baseDir: string;
|
|
44
|
+
private indexPath: string;
|
|
45
|
+
|
|
46
|
+
constructor(baseDir?: string) {
|
|
47
|
+
this.baseDir = baseDir ?? path.join(os.homedir(), '.weaver', 'conversations');
|
|
48
|
+
this.indexPath = path.join(this.baseDir, 'index.json');
|
|
49
|
+
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
create(projectDir: string): ConversationRecord {
|
|
53
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const record: ConversationRecord = {
|
|
56
|
+
id,
|
|
57
|
+
title: '',
|
|
58
|
+
projectDir,
|
|
59
|
+
messageCount: 0,
|
|
60
|
+
totalTokens: 0,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
lastMessageAt: now,
|
|
63
|
+
botIds: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Create conversation directory
|
|
67
|
+
const convDir = path.join(this.baseDir, id);
|
|
68
|
+
fs.mkdirSync(convDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
// Add to index (sync write — no lock needed for create, index is new or we're the only writer)
|
|
71
|
+
const index = this.readIndex();
|
|
72
|
+
index.unshift(record);
|
|
73
|
+
this.writeIndexUnsafe(index);
|
|
74
|
+
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
list(): ConversationRecord[] {
|
|
79
|
+
return this.readIndex();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get(id: string): ConversationRecord | null {
|
|
83
|
+
const index = this.readIndex();
|
|
84
|
+
return index.find(c => c.id === id) ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getMostRecent(): ConversationRecord | null {
|
|
88
|
+
const index = this.readIndex();
|
|
89
|
+
return index.length > 0 ? index[0] : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
delete(id: string): void {
|
|
93
|
+
// Remove from index (sync — delete doesn't need lock)
|
|
94
|
+
const index = this.readIndex();
|
|
95
|
+
const filtered = index.filter(c => c.id !== id);
|
|
96
|
+
this.writeIndexUnsafe(filtered);
|
|
97
|
+
|
|
98
|
+
// Remove files
|
|
99
|
+
const convDir = path.join(this.baseDir, id);
|
|
100
|
+
if (fs.existsSync(convDir)) {
|
|
101
|
+
fs.rmSync(convDir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
loadMessages(id: string): AgentMessage[] {
|
|
106
|
+
const msgPath = path.join(this.baseDir, id, 'messages.ndjson');
|
|
107
|
+
if (!fs.existsSync(msgPath)) return [];
|
|
108
|
+
|
|
109
|
+
const content = fs.readFileSync(msgPath, 'utf-8');
|
|
110
|
+
const messages: AgentMessage[] = [];
|
|
111
|
+
|
|
112
|
+
for (const line of content.split('\n')) {
|
|
113
|
+
if (!line.trim()) continue;
|
|
114
|
+
try {
|
|
115
|
+
const stored = JSON.parse(line) as StoredMessage;
|
|
116
|
+
const msg: AgentMessage = {
|
|
117
|
+
role: stored.role,
|
|
118
|
+
content: stored.content,
|
|
119
|
+
};
|
|
120
|
+
if (stored.toolCalls) msg.toolCalls = stored.toolCalls;
|
|
121
|
+
if (stored.toolCallId) msg.toolCallId = stored.toolCallId;
|
|
122
|
+
messages.push(msg);
|
|
123
|
+
} catch {
|
|
124
|
+
// Skip corrupt lines
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return messages;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
appendMessages(id: string, messages: AgentMessage[]): void {
|
|
132
|
+
if (messages.length === 0) return;
|
|
133
|
+
|
|
134
|
+
const convDir = path.join(this.baseDir, id);
|
|
135
|
+
fs.mkdirSync(convDir, { recursive: true });
|
|
136
|
+
const msgPath = path.join(convDir, 'messages.ndjson');
|
|
137
|
+
|
|
138
|
+
const lines = messages.map(m => {
|
|
139
|
+
const stored: StoredMessage = {
|
|
140
|
+
role: m.role,
|
|
141
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
tokens: Math.ceil((typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content).length) / CHARS_PER_TOKEN),
|
|
144
|
+
};
|
|
145
|
+
if (m.toolCalls) stored.toolCalls = m.toolCalls;
|
|
146
|
+
if (m.toolCallId) stored.toolCallId = m.toolCallId;
|
|
147
|
+
return JSON.stringify(stored);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
fs.appendFileSync(msgPath, lines.join('\n') + '\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async updateAfterTurn(id: string, newMessages: AgentMessage[], tokensUsed: number): Promise<void> {
|
|
154
|
+
await withFileLock(this.indexPath, () => {
|
|
155
|
+
const index = this.readIndex();
|
|
156
|
+
const record = index.find(c => c.id === id);
|
|
157
|
+
if (!record) return;
|
|
158
|
+
|
|
159
|
+
record.messageCount += newMessages.length;
|
|
160
|
+
record.totalTokens += tokensUsed;
|
|
161
|
+
record.lastMessageAt = Date.now();
|
|
162
|
+
|
|
163
|
+
// Move to front (most recent)
|
|
164
|
+
const idx = index.indexOf(record);
|
|
165
|
+
if (idx > 0) {
|
|
166
|
+
index.splice(idx, 1);
|
|
167
|
+
index.unshift(record);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Cap index size
|
|
171
|
+
if (index.length > MAX_INDEX_SIZE) {
|
|
172
|
+
index.splice(MAX_INDEX_SIZE);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.writeIndexUnsafe(index);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setTitle(id: string, title: string): Promise<void> {
|
|
180
|
+
await withFileLock(this.indexPath, () => {
|
|
181
|
+
const index = this.readIndex();
|
|
182
|
+
const record = index.find(c => c.id === id);
|
|
183
|
+
if (record) {
|
|
184
|
+
record.title = title.slice(0, 80).replace(/\n/g, ' ').trim();
|
|
185
|
+
this.writeIndexUnsafe(index);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async addBotId(id: string, botId: string): Promise<void> {
|
|
191
|
+
await withFileLock(this.indexPath, () => {
|
|
192
|
+
const index = this.readIndex();
|
|
193
|
+
const record = index.find(c => c.id === id);
|
|
194
|
+
if (record && !record.botIds.includes(botId)) {
|
|
195
|
+
record.botIds.push(botId);
|
|
196
|
+
this.writeIndexUnsafe(index);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Private ---
|
|
202
|
+
|
|
203
|
+
private readIndex(): ConversationRecord[] {
|
|
204
|
+
if (!fs.existsSync(this.indexPath)) return [];
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation index parse failed: ${err}\n`);
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async writeIndex(index: ConversationRecord[]): Promise<void> {
|
|
214
|
+
await withFileLock(this.indexPath, () => {
|
|
215
|
+
this.writeIndexUnsafe(index);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private writeIndexUnsafe(index: ConversationRecord[]): void {
|
|
220
|
+
fs.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actionable error guidance — maps cryptic error messages
|
|
3
|
+
* to human-readable explanations with fix suggestions.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const GUIDES: Array<{ pattern: RegExp; guidance: string }> = [
|
|
7
|
+
{ pattern: /ETIMEDOUT/i, guidance: 'Network timeout. Check internet connection or increase timeout with --timeout.' },
|
|
8
|
+
{ pattern: /ECONNRESET/i, guidance: 'Connection reset. The server closed the connection. Retry in a few seconds.' },
|
|
9
|
+
{ pattern: /ECONNREFUSED/i, guidance: 'Connection refused. Is the service running? Check the provider URL.' },
|
|
10
|
+
{ pattern: /401|authentication|invalid.*key/i, guidance: 'Authentication failed. Check ANTHROPIC_API_KEY or run "weaver init" to reconfigure.' },
|
|
11
|
+
{ pattern: /403|forbidden/i, guidance: 'Access denied. Your API key may not have permission for this model.' },
|
|
12
|
+
{ pattern: /429|rate.?limit|too many requests/i, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.' },
|
|
13
|
+
{ pattern: /502|bad gateway/i, guidance: 'Server error (502). The API is temporarily unavailable. Will auto-retry.' },
|
|
14
|
+
{ pattern: /503|service unavailable|overloaded/i, guidance: 'Service overloaded. Will auto-retry with backoff.' },
|
|
15
|
+
{ pattern: /exit code 143/i, guidance: 'Process was killed (SIGTERM). Likely our timeout or Ctrl+C.' },
|
|
16
|
+
{ pattern: /exit code 137/i, guidance: 'Process was killed (OOM or SIGKILL). System may be low on memory.' },
|
|
17
|
+
{ pattern: /ENOMEM/i, guidance: 'Out of memory. Close other applications or increase available RAM.' },
|
|
18
|
+
{ pattern: /ENOSPC/i, guidance: 'Disk full. Free up disk space.' },
|
|
19
|
+
{ pattern: /lock.*retries|failed to acquire.*lock/i, guidance: 'File lock contention. Another weaver process may be running. Check with "ps aux | grep weaver".' },
|
|
20
|
+
{ pattern: /not a workflow|No @flowWeaver/i, guidance: 'File is not a Flow Weaver workflow. Ensure it has @flowWeaver annotations.' },
|
|
21
|
+
{ pattern: /parse.*json|unexpected token/i, guidance: 'JSON parse error. The AI may have returned malformed output. Retry the task.' },
|
|
22
|
+
{ pattern: /Queue full/i, guidance: 'Too many pending tasks (200 max). Process or clear existing tasks first.' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get actionable guidance for an error message.
|
|
27
|
+
* Returns null if no guidance is available.
|
|
28
|
+
*/
|
|
29
|
+
export function getErrorGuidance(msg: string): string | null {
|
|
30
|
+
for (const { pattern, guidance } of GUIDES) {
|
|
31
|
+
if (pattern.test(msg)) return guidance;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|