create-walle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-walle.js +134 -0
- package/package.json +18 -0
- package/template/.env.example +40 -0
- package/template/CLAUDE.md +12 -0
- package/template/LICENSE +21 -0
- package/template/README.md +167 -0
- package/template/bin/setup.js +100 -0
- package/template/claude-code-skill.md +60 -0
- package/template/claude-task-manager/api-prompts.js +1841 -0
- package/template/claude-task-manager/api-reviews.js +275 -0
- package/template/claude-task-manager/approval-agent.js +454 -0
- package/template/claude-task-manager/bin/restart-ctm.sh +16 -0
- package/template/claude-task-manager/db.js +1721 -0
- package/template/claude-task-manager/docs/PROMPT-MANAGEMENT-DESIGN.md +631 -0
- package/template/claude-task-manager/git-utils.js +214 -0
- package/template/claude-task-manager/package-lock.json +1607 -0
- package/template/claude-task-manager/package.json +31 -0
- package/template/claude-task-manager/prompt-harvest.js +1148 -0
- package/template/claude-task-manager/public/css/prompts.css +880 -0
- package/template/claude-task-manager/public/css/reviews.css +430 -0
- package/template/claude-task-manager/public/css/walle.css +732 -0
- package/template/claude-task-manager/public/favicon.ico +0 -0
- package/template/claude-task-manager/public/icon.svg +37 -0
- package/template/claude-task-manager/public/index.html +8346 -0
- package/template/claude-task-manager/public/js/prompts.js +3159 -0
- package/template/claude-task-manager/public/js/reviews.js +1292 -0
- package/template/claude-task-manager/public/js/walle.js +3081 -0
- package/template/claude-task-manager/public/manifest.json +13 -0
- package/template/claude-task-manager/public/prompts.html +4353 -0
- package/template/claude-task-manager/public/setup.html +216 -0
- package/template/claude-task-manager/queue-engine.js +404 -0
- package/template/claude-task-manager/server-state.js +5 -0
- package/template/claude-task-manager/server.js +2254 -0
- package/template/claude-task-manager/session-utils.js +124 -0
- package/template/claude-task-manager/start.sh +17 -0
- package/template/claude-task-manager/tests/test-ai-search.js +61 -0
- package/template/claude-task-manager/tests/test-editor-ux.js +76 -0
- package/template/claude-task-manager/tests/test-editor-ux2.js +51 -0
- package/template/claude-task-manager/tests/test-features-v2.js +127 -0
- package/template/claude-task-manager/tests/test-insights-cached.js +78 -0
- package/template/claude-task-manager/tests/test-insights.js +124 -0
- package/template/claude-task-manager/tests/test-permissions-v2.js +127 -0
- package/template/claude-task-manager/tests/test-permissions.js +122 -0
- package/template/claude-task-manager/tests/test-pin.js +51 -0
- package/template/claude-task-manager/tests/test-prompts.js +164 -0
- package/template/claude-task-manager/tests/test-recent-sessions.js +96 -0
- package/template/claude-task-manager/tests/test-review.js +104 -0
- package/template/claude-task-manager/tests/test-send-dropdown.js +76 -0
- package/template/claude-task-manager/tests/test-send-final.js +30 -0
- package/template/claude-task-manager/tests/test-send-fixes.js +76 -0
- package/template/claude-task-manager/tests/test-send-integration.js +107 -0
- package/template/claude-task-manager/tests/test-send-visual.js +34 -0
- package/template/claude-task-manager/tests/test-session-create.js +147 -0
- package/template/claude-task-manager/tests/test-sidebar-ux.js +83 -0
- package/template/claude-task-manager/tests/test-url-hash.js +68 -0
- package/template/claude-task-manager/tests/test-ux-crop.js +34 -0
- package/template/claude-task-manager/tests/test-ux-review.js +130 -0
- package/template/claude-task-manager/tests/test-zoom-card.js +76 -0
- package/template/claude-task-manager/tests/test-zoom.js +92 -0
- package/template/claude-task-manager/tests/test-zoom2.js +67 -0
- package/template/docs/site/api/README.md +187 -0
- package/template/docs/site/guides/claude-code.md +58 -0
- package/template/docs/site/guides/configuration.md +96 -0
- package/template/docs/site/guides/quickstart.md +158 -0
- package/template/docs/site/index.md +14 -0
- package/template/docs/site/skills/README.md +135 -0
- package/template/wall-e/.dockerignore +11 -0
- package/template/wall-e/Dockerfile +25 -0
- package/template/wall-e/adapters/adapter-base.js +37 -0
- package/template/wall-e/adapters/ctm.js +193 -0
- package/template/wall-e/adapters/slack.js +56 -0
- package/template/wall-e/agent.js +319 -0
- package/template/wall-e/api-walle.js +1073 -0
- package/template/wall-e/brain.js +1235 -0
- package/template/wall-e/channels/agent-api.js +172 -0
- package/template/wall-e/channels/channel-base.js +14 -0
- package/template/wall-e/channels/imessage-channel.js +113 -0
- package/template/wall-e/channels/slack-channel.js +118 -0
- package/template/wall-e/chat.js +778 -0
- package/template/wall-e/decision/confidence.js +93 -0
- package/template/wall-e/deploy.sh +35 -0
- package/template/wall-e/docs/specs/2026-04-01-publish-plan.md +112 -0
- package/template/wall-e/docs/specs/SKILL-FORMAT.md +326 -0
- package/template/wall-e/extraction/contradiction.js +168 -0
- package/template/wall-e/extraction/knowledge-extractor.js +190 -0
- package/template/wall-e/fly.toml +24 -0
- package/template/wall-e/loops/ingest.js +34 -0
- package/template/wall-e/loops/reflect.js +63 -0
- package/template/wall-e/loops/tasks.js +487 -0
- package/template/wall-e/loops/think.js +125 -0
- package/template/wall-e/package-lock.json +533 -0
- package/template/wall-e/package.json +18 -0
- package/template/wall-e/scripts/ingest-slack-search.js +85 -0
- package/template/wall-e/scripts/pull-slack-via-claude.js +98 -0
- package/template/wall-e/scripts/slack-backfill.js +295 -0
- package/template/wall-e/scripts/slack-channel-history.js +454 -0
- package/template/wall-e/server.js +93 -0
- package/template/wall-e/skills/_bundled/email-digest/SKILL.md +95 -0
- package/template/wall-e/skills/_bundled/email-sync/SKILL.md +65 -0
- package/template/wall-e/skills/_bundled/email-sync/mail-reader.jxa +104 -0
- package/template/wall-e/skills/_bundled/email-sync/run.js +213 -0
- package/template/wall-e/skills/_bundled/google-calendar/SKILL.md +73 -0
- package/template/wall-e/skills/_bundled/google-calendar/cal-reader.swift +81 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +181 -0
- package/template/wall-e/skills/_bundled/memory-search/SKILL.md +92 -0
- package/template/wall-e/skills/_bundled/morning-briefing/SKILL.md +131 -0
- package/template/wall-e/skills/_bundled/morning-briefing/run.js +264 -0
- package/template/wall-e/skills/_bundled/slack-backfill/SKILL.md +60 -0
- package/template/wall-e/skills/_bundled/slack-sync/SKILL.md +55 -0
- package/template/wall-e/skills/claude-code-reader.js +144 -0
- package/template/wall-e/skills/mcp-client.js +407 -0
- package/template/wall-e/skills/skill-executor.js +163 -0
- package/template/wall-e/skills/skill-loader.js +410 -0
- package/template/wall-e/skills/skill-planner.js +88 -0
- package/template/wall-e/skills/slack-ingest.js +329 -0
- package/template/wall-e/skills/slack-pull-live.js +270 -0
- package/template/wall-e/skills/tool-executor.js +188 -0
- package/template/wall-e/tests/adapter-base.test.js +20 -0
- package/template/wall-e/tests/adapter-ctm.test.js +122 -0
- package/template/wall-e/tests/adapter-slack.test.js +98 -0
- package/template/wall-e/tests/agent-api.test.js +256 -0
- package/template/wall-e/tests/api-walle.test.js +222 -0
- package/template/wall-e/tests/brain.test.js +602 -0
- package/template/wall-e/tests/channels.test.js +104 -0
- package/template/wall-e/tests/chat.test.js +103 -0
- package/template/wall-e/tests/confidence.test.js +134 -0
- package/template/wall-e/tests/contradiction.test.js +217 -0
- package/template/wall-e/tests/ingest.test.js +113 -0
- package/template/wall-e/tests/mcp-client.test.js +71 -0
- package/template/wall-e/tests/reflect.test.js +103 -0
- package/template/wall-e/tests/server.test.js +111 -0
- package/template/wall-e/tests/skills.test.js +198 -0
- package/template/wall-e/tests/slack-ingest.test.js +103 -0
- package/template/wall-e/tests/think.test.js +435 -0
- package/template/wall-e/tools/local-tools.js +697 -0
- package/template/wall-e/tools/slack-mcp.js +290 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const brain = require('../brain');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { findSkill } = require('../skills/skill-loader');
|
|
6
|
+
|
|
7
|
+
const WALL_E_DIR = path.join(__dirname, '..');
|
|
8
|
+
|
|
9
|
+
// ── Live log buffer — in-memory, keyed by task ID ──
|
|
10
|
+
const taskLogs = new Map(); // task_id → { lines: string[], startedAt, status }
|
|
11
|
+
const taskProcesses = new Map(); // task_id → child process (for stopping)
|
|
12
|
+
|
|
13
|
+
function stopTask(taskId) {
|
|
14
|
+
const child = taskProcesses.get(taskId);
|
|
15
|
+
if (child) {
|
|
16
|
+
child.kill('SIGTERM');
|
|
17
|
+
taskProcesses.delete(taskId);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getTaskLogs(taskId) {
|
|
24
|
+
return taskLogs.get(taskId) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clearTaskLogs(taskId) {
|
|
28
|
+
taskLogs.delete(taskId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appendLog(taskId, line) {
|
|
32
|
+
let entry = taskLogs.get(taskId);
|
|
33
|
+
if (!entry) {
|
|
34
|
+
entry = { lines: [], startedAt: new Date().toISOString(), status: 'running' };
|
|
35
|
+
taskLogs.set(taskId, entry);
|
|
36
|
+
}
|
|
37
|
+
entry.lines.push(line);
|
|
38
|
+
// Cap at 500 lines to prevent memory bloat
|
|
39
|
+
if (entry.lines.length > 500) entry.lines.shift();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* On daemon startup, recover tasks that were interrupted (stuck in 'running').
|
|
44
|
+
* Resets them to 'pending' so they get re-picked up.
|
|
45
|
+
*/
|
|
46
|
+
function recoverInterruptedTasks() {
|
|
47
|
+
const db = brain.getDb();
|
|
48
|
+
const stuck = db.prepare("SELECT * FROM tasks WHERE status = 'running'").all();
|
|
49
|
+
if (stuck.length === 0) return 0;
|
|
50
|
+
|
|
51
|
+
for (const t of stuck) {
|
|
52
|
+
const hasCheckpoint = t.checkpoint ? ' (has checkpoint — will resume)' : '';
|
|
53
|
+
console.log(`[tasks] Recovering interrupted task: ${t.title}${hasCheckpoint}`);
|
|
54
|
+
brain.updateTask(t.id, {
|
|
55
|
+
status: 'pending',
|
|
56
|
+
started_at: null,
|
|
57
|
+
next_run_at: new Date().toISOString(), // run immediately on recovery
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return stuck.length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Task executor loop — picks up due tasks and runs them.
|
|
65
|
+
*/
|
|
66
|
+
async function runDueTasks() {
|
|
67
|
+
const tasks = brain.getDueTasks();
|
|
68
|
+
if (tasks.length === 0) return { processed: 0 };
|
|
69
|
+
|
|
70
|
+
let processed = 0;
|
|
71
|
+
for (const task of tasks) {
|
|
72
|
+
if (task.status === 'running') continue;
|
|
73
|
+
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const hasCheckpoint = task.checkpoint ? ' (resuming from checkpoint)' : '';
|
|
76
|
+
brain.updateTask(task.id, { status: 'running', started_at: now });
|
|
77
|
+
appendLog(task.id, `[${now.slice(11, 19)}] Starting: ${task.title}${hasCheckpoint}`);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
let resultText;
|
|
81
|
+
|
|
82
|
+
if (task.skill) {
|
|
83
|
+
resultText = await executeSkill(task.id, task);
|
|
84
|
+
} else if (task.execution === 'script' && task.script) {
|
|
85
|
+
resultText = await executeScript(task.id, task.script, task.checkpoint);
|
|
86
|
+
} else {
|
|
87
|
+
resultText = await executeChat(task.id, task);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const completedAt = new Date().toISOString();
|
|
91
|
+
appendLog(task.id, `[${completedAt.slice(11, 19)}] Completed`);
|
|
92
|
+
|
|
93
|
+
const logEntry = taskLogs.get(task.id);
|
|
94
|
+
if (logEntry) logEntry.status = 'completed';
|
|
95
|
+
|
|
96
|
+
// Strip BRIEFING_ITEMS block from displayed result, store items separately
|
|
97
|
+
const cleanResult = resultText.replace(/<!--\s*BRIEFING_ITEMS[\s\S]*?-->/, '').trim();
|
|
98
|
+
|
|
99
|
+
brain.updateTask(task.id, {
|
|
100
|
+
status: task.type === 'recurring' ? 'pending' : 'completed',
|
|
101
|
+
completed_at: completedAt,
|
|
102
|
+
last_run_at: completedAt,
|
|
103
|
+
run_count: (task.run_count || 0) + 1,
|
|
104
|
+
result: cleanResult.slice(0, 10000),
|
|
105
|
+
error: null,
|
|
106
|
+
checkpoint: null, // clear checkpoint on success
|
|
107
|
+
next_run_at: task.type === 'recurring' ? computeNextDue(task.schedule) : null,
|
|
108
|
+
// Keep started_at so duration can be computed as completed_at - started_at
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Consolidate briefing items if present
|
|
112
|
+
try {
|
|
113
|
+
consolidateBriefingItems(task, resultText, completedAt);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('[tasks] Briefing item consolidation failed:', e.message);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Notify (skip frequent recurring)
|
|
119
|
+
if (task.type !== 'recurring' || !task.schedule?.match(/every\s+\d+\s*m/i)) {
|
|
120
|
+
try {
|
|
121
|
+
const { sendNotification } = require('../tools/local-tools');
|
|
122
|
+
await sendNotification('WALL-E Task', task.title + ' — done');
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
processed++;
|
|
127
|
+
console.log(`[tasks] Completed: ${task.title} (${task.execution || 'chat'})`);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('[tasks] Failed:', task.title, err.message);
|
|
130
|
+
appendLog(task.id, `[ERROR] ${err.message}`);
|
|
131
|
+
|
|
132
|
+
const logEntry = taskLogs.get(task.id);
|
|
133
|
+
if (logEntry) logEntry.status = 'failed';
|
|
134
|
+
|
|
135
|
+
// Detect actionable errors and create pending questions
|
|
136
|
+
const errMsg = err.message || '';
|
|
137
|
+
const isSlackAuth = errMsg.includes('Slack token expired') || errMsg.includes('invalid_auth') || errMsg.includes('401') || errMsg.includes('not authenticated');
|
|
138
|
+
if (isSlackAuth) {
|
|
139
|
+
try {
|
|
140
|
+
// Pause all Slack-related tasks to prevent repeated failures
|
|
141
|
+
const allTasks = brain.listTasks({});
|
|
142
|
+
for (const t of allTasks) {
|
|
143
|
+
if (t.script && t.script.includes('slack') && (t.status === 'pending' || t.status === 'running')) {
|
|
144
|
+
brain.updateTask(t.id, { status: 'paused' });
|
|
145
|
+
console.log(`[tasks] Auto-paused: ${t.title} (Slack auth expired)`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Create a pending question for the user (dedup: skip if one already exists)
|
|
149
|
+
const existingQ = brain.listQuestions({ status: 'pending', question_type: 'action_required' })
|
|
150
|
+
.find(q => q.question.includes('Slack token expired'));
|
|
151
|
+
if (existingQ) {
|
|
152
|
+
console.log('[tasks] Slack re-auth question already pending, skipping duplicate');
|
|
153
|
+
} else {
|
|
154
|
+
brain.insertQuestion({
|
|
155
|
+
question_type: 'action_required',
|
|
156
|
+
question: 'Slack token expired — WALL-E cannot access Slack until you re-authenticate.',
|
|
157
|
+
context: JSON.stringify({
|
|
158
|
+
action: 'slack_reauth',
|
|
159
|
+
failed_task: task.title,
|
|
160
|
+
error: errMsg,
|
|
161
|
+
instructions: 'Click "Reconnect Slack" in the Tasks tab, or run: node tools/slack-mcp.js auth',
|
|
162
|
+
}),
|
|
163
|
+
priority: 'high',
|
|
164
|
+
});
|
|
165
|
+
console.log('[tasks] Created pending question for Slack re-auth');
|
|
166
|
+
try {
|
|
167
|
+
const { sendNotification } = require('../tools/local-tools');
|
|
168
|
+
await sendNotification('WALL-E: Slack Disconnected', 'Re-authenticate to resume Slack tasks');
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.error('[tasks] Failed to create auth question:', e.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
brain.updateTask(task.id, {
|
|
177
|
+
status: task.type === 'recurring' ? 'pending' : 'failed',
|
|
178
|
+
error: isSlackAuth ? 'Slack token expired. Reconnect Slack to retry.' : err.message,
|
|
179
|
+
completed_at: new Date().toISOString(),
|
|
180
|
+
last_run_at: new Date().toISOString(),
|
|
181
|
+
run_count: (task.run_count || 0) + 1,
|
|
182
|
+
next_run_at: task.type === 'recurring' ? computeNextDue(task.schedule) : null,
|
|
183
|
+
// Keep started_at so duration can be computed
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// For Slack auth errors, override recurring status to paused
|
|
187
|
+
if (isSlackAuth && task.type === 'recurring') {
|
|
188
|
+
brain.updateTask(task.id, { status: 'paused' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { processed };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Skill execution ──
|
|
197
|
+
|
|
198
|
+
async function executeSkill(taskId, task) {
|
|
199
|
+
const skill = findSkill(task.skill);
|
|
200
|
+
if (!skill) throw new Error(`Skill "${task.skill}" not found`);
|
|
201
|
+
|
|
202
|
+
appendLog(taskId, `Skill: ${skill.name} v${skill.version} (${skill.execution})`);
|
|
203
|
+
|
|
204
|
+
if (skill.execution === 'script') {
|
|
205
|
+
// Resolve entry point relative to the skill directory
|
|
206
|
+
const entryPath = path.resolve(skill.dir, skill.entry || 'run.js');
|
|
207
|
+
if (!require('fs').existsSync(entryPath)) {
|
|
208
|
+
throw new Error(`Skill entry point not found: ${entryPath}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build the shell command: node <entry> <args...>
|
|
212
|
+
const args = [...(skill.args || [])];
|
|
213
|
+
|
|
214
|
+
// Merge task-level skill_config with skill defaults
|
|
215
|
+
const skillConfig = {};
|
|
216
|
+
if (skill.config) {
|
|
217
|
+
for (const [k, v] of Object.entries(skill.config)) {
|
|
218
|
+
if (v && typeof v === 'object' && v.default !== undefined) {
|
|
219
|
+
skillConfig[k] = v.default;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Task overrides
|
|
224
|
+
let taskConfig = {};
|
|
225
|
+
if (task.skill_config) {
|
|
226
|
+
try {
|
|
227
|
+
taskConfig = typeof task.skill_config === 'string'
|
|
228
|
+
? JSON.parse(task.skill_config)
|
|
229
|
+
: task.skill_config;
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
Object.assign(skillConfig, taskConfig);
|
|
233
|
+
|
|
234
|
+
// For slack-backfill: pass mode/month as CLI args if configured
|
|
235
|
+
if (skillConfig.mode && !args.includes(skillConfig.mode)) {
|
|
236
|
+
// Only add mode as arg if the script expects it as argv[2]
|
|
237
|
+
// (slack-backfill uses process.argv[2] as mode)
|
|
238
|
+
if (skill.name === 'slack-backfill') {
|
|
239
|
+
args.push(skillConfig.mode);
|
|
240
|
+
if (skillConfig.month) args.push(skillConfig.month);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const scriptCmd = `node ${entryPath} ${args.join(' ')}`.trim();
|
|
245
|
+
appendLog(taskId, `Running: ${scriptCmd}`);
|
|
246
|
+
|
|
247
|
+
// Pass config as env var + checkpoint support
|
|
248
|
+
return executeScript(taskId, scriptCmd, task.checkpoint, {
|
|
249
|
+
WALL_E_SKILL_CONFIG: JSON.stringify(skillConfig),
|
|
250
|
+
WALL_E_SKILL_DIR: skill.dir,
|
|
251
|
+
WALL_E_SRC_DIR: WALL_E_DIR,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (skill.execution === 'agent') {
|
|
256
|
+
// Build a task-like object with skill instructions as the description
|
|
257
|
+
const agentTask = {
|
|
258
|
+
...task,
|
|
259
|
+
title: task.title || skill.name,
|
|
260
|
+
description: skill.instructions || skill.description,
|
|
261
|
+
};
|
|
262
|
+
appendLog(taskId, `Agent skill: sending instructions to Claude...`);
|
|
263
|
+
return executeChat(taskId, agentTask);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
throw new Error(`Unknown skill execution mode: ${skill.execution}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Script execution with live log streaming ──
|
|
270
|
+
|
|
271
|
+
function executeScript(taskId, script, checkpoint, extraEnv) {
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
// Pass checkpoint as env var so scripts can resume from where they left off
|
|
274
|
+
const env = { ...process.env, HOME: process.env.HOME };
|
|
275
|
+
if (checkpoint) env.WALL_E_CHECKPOINT = checkpoint;
|
|
276
|
+
if (extraEnv) Object.assign(env, extraEnv);
|
|
277
|
+
|
|
278
|
+
const child = spawn('bash', ['-c', script], {
|
|
279
|
+
cwd: WALL_E_DIR,
|
|
280
|
+
env,
|
|
281
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
282
|
+
});
|
|
283
|
+
taskProcesses.set(taskId, child);
|
|
284
|
+
|
|
285
|
+
let stdout = '';
|
|
286
|
+
let stderr = '';
|
|
287
|
+
// No timeout — tasks run until completion. User can stop via UI.
|
|
288
|
+
|
|
289
|
+
child.stdout.on('data', (chunk) => {
|
|
290
|
+
const text = chunk.toString();
|
|
291
|
+
stdout += text;
|
|
292
|
+
text.split('\n').filter(Boolean).forEach(line => {
|
|
293
|
+
appendLog(taskId, line);
|
|
294
|
+
// Lines starting with CHECKPOINT: are saved as resume points
|
|
295
|
+
if (line.startsWith('CHECKPOINT:')) {
|
|
296
|
+
brain.updateTask(taskId, { checkpoint: line.slice(11).trim() });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
child.stderr.on('data', (chunk) => {
|
|
302
|
+
const text = chunk.toString();
|
|
303
|
+
stderr += text;
|
|
304
|
+
text.split('\n').filter(Boolean).forEach(line => {
|
|
305
|
+
appendLog(taskId, `[stderr] ${line}`);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
child.on('close', (code) => {
|
|
310
|
+
taskProcesses.delete(taskId);
|
|
311
|
+
if (code !== 0 && code !== null) {
|
|
312
|
+
reject(new Error(`Exit code ${code}: ${stderr.slice(0, 500)}`));
|
|
313
|
+
} else {
|
|
314
|
+
let output = stdout.trim();
|
|
315
|
+
if (stderr.trim()) output += '\n[stderr] ' + stderr.trim();
|
|
316
|
+
resolve(output);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
child.on('error', (err) => {
|
|
321
|
+
reject(err);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Chat execution with log streaming ──
|
|
327
|
+
|
|
328
|
+
async function executeChat(taskId, task) {
|
|
329
|
+
const chatModule = require('../chat');
|
|
330
|
+
const prompt = buildTaskPrompt(task);
|
|
331
|
+
|
|
332
|
+
appendLog(taskId, 'Sending to Claude...');
|
|
333
|
+
|
|
334
|
+
const result = await chatModule.chat(prompt, {
|
|
335
|
+
channel: 'task',
|
|
336
|
+
session_id: `task-${task.id}`,
|
|
337
|
+
onProgress: (event) => {
|
|
338
|
+
if (event.type === 'tool_call') appendLog(taskId, event.summary);
|
|
339
|
+
else if (event.type === 'tool_done') appendLog(taskId, `Done: ${event.summary}`);
|
|
340
|
+
else if (event.type === 'thinking') appendLog(taskId, `Turn ${(event.turn || 0) + 1}...`);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return result.reply || '';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildTaskPrompt(task) {
|
|
348
|
+
let prompt = `[TASK] ${task.title}`;
|
|
349
|
+
if (task.description) prompt += `\n\n${task.description}`;
|
|
350
|
+
prompt += `\n\nThis is a background task. Complete it thoroughly using your tools. Provide a clear, actionable result.`;
|
|
351
|
+
if (task.priority === 'urgent' || task.priority === 'high') {
|
|
352
|
+
prompt += ` This is ${task.priority} priority.`;
|
|
353
|
+
}
|
|
354
|
+
return prompt;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function computeNextDue(schedule) {
|
|
358
|
+
if (!schedule) return null;
|
|
359
|
+
const now = new Date();
|
|
360
|
+
|
|
361
|
+
const dailyAtMatch = schedule.match(/^daily\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i);
|
|
362
|
+
if (dailyAtMatch) {
|
|
363
|
+
let hour = parseInt(dailyAtMatch[1]);
|
|
364
|
+
const min = parseInt(dailyAtMatch[2] || '0');
|
|
365
|
+
const ampm = dailyAtMatch[3]?.toLowerCase();
|
|
366
|
+
if (ampm === 'pm' && hour < 12) hour += 12;
|
|
367
|
+
if (ampm === 'am' && hour === 12) hour = 0;
|
|
368
|
+
const next = new Date(now);
|
|
369
|
+
next.setHours(hour, min, 0, 0);
|
|
370
|
+
if (next <= now) next.setDate(next.getDate() + 1);
|
|
371
|
+
return next.toISOString();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const weeklyMatch = schedule.match(/^weekly\s+(?:on\s+)?(\w+)(?:\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?)?$/i);
|
|
375
|
+
if (weeklyMatch) {
|
|
376
|
+
const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
377
|
+
const dayAbbrevs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
378
|
+
const dayInput = weeklyMatch[1].toLowerCase();
|
|
379
|
+
let targetDay = dayNames.indexOf(dayInput);
|
|
380
|
+
if (targetDay === -1) targetDay = dayAbbrevs.indexOf(dayInput.slice(0, 3));
|
|
381
|
+
if (targetDay === -1) targetDay = 1;
|
|
382
|
+
let hour = parseInt(weeklyMatch[2] || '9');
|
|
383
|
+
const min = parseInt(weeklyMatch[3] || '0');
|
|
384
|
+
const ampm = weeklyMatch[4]?.toLowerCase();
|
|
385
|
+
if (ampm === 'pm' && hour < 12) hour += 12;
|
|
386
|
+
const next = new Date(now);
|
|
387
|
+
next.setHours(hour, min, 0, 0);
|
|
388
|
+
const daysUntil = (targetDay - now.getDay() + 7) % 7 || 7;
|
|
389
|
+
next.setDate(next.getDate() + daysUntil);
|
|
390
|
+
return next.toISOString();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (schedule === 'hourly') return new Date(now.getTime() + 3600000).toISOString();
|
|
394
|
+
if (schedule === 'daily') return new Date(now.getTime() + 86400000).toISOString();
|
|
395
|
+
if (schedule === 'weekly') return new Date(now.getTime() + 604800000).toISOString();
|
|
396
|
+
|
|
397
|
+
const everyMatch = schedule.match(/^every\s+(\d+)\s*(m|h|d)$/i);
|
|
398
|
+
if (everyMatch) {
|
|
399
|
+
const n = parseInt(everyMatch[1]);
|
|
400
|
+
const unit = everyMatch[2].toLowerCase();
|
|
401
|
+
const ms = unit === 'h' ? n * 3600000 : unit === 'm' ? n * 60000 : n * 86400000;
|
|
402
|
+
return new Date(now.getTime() + ms).toISOString();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Immediately run a task by ID — bypasses the daemon loop timer.
|
|
410
|
+
* Sets the task to pending with immediate next_run_at, then executes runDueTasks().
|
|
411
|
+
*/
|
|
412
|
+
async function runTaskById(taskId) {
|
|
413
|
+
const task = brain.getTask(taskId);
|
|
414
|
+
if (!task) throw new Error('Task not found');
|
|
415
|
+
if (task.status === 'running') throw new Error('Task is already running');
|
|
416
|
+
brain.updateTask(taskId, { status: 'pending', next_run_at: new Date().toISOString(), error: null, result: null });
|
|
417
|
+
return runDueTasks();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ── Briefing Item Consolidation ──
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse BRIEFING_ITEMS from task result and upsert into briefing_items table.
|
|
424
|
+
* New items are inserted; existing items (matched by skill + title) get updated.
|
|
425
|
+
* Items from previous runs that don't appear in the new result are left as-is
|
|
426
|
+
* (user may have modified their status).
|
|
427
|
+
*/
|
|
428
|
+
function consolidateBriefingItems(task, resultText, timestamp) {
|
|
429
|
+
const match = resultText.match(/<!--\s*BRIEFING_ITEMS\s*\n([\s\S]*?)-->/);
|
|
430
|
+
if (!match) return;
|
|
431
|
+
|
|
432
|
+
let items;
|
|
433
|
+
try {
|
|
434
|
+
items = JSON.parse(match[1].trim());
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error('[tasks] Failed to parse BRIEFING_ITEMS JSON:', e.message);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
441
|
+
|
|
442
|
+
const skill = task.skill || 'unknown';
|
|
443
|
+
const now = timestamp || new Date().toISOString();
|
|
444
|
+
let inserted = 0, updated = 0;
|
|
445
|
+
|
|
446
|
+
for (const item of items) {
|
|
447
|
+
if (!item.title) continue;
|
|
448
|
+
|
|
449
|
+
// Try exact title match first
|
|
450
|
+
const existing = brain.findBriefingItemByTitle(skill, item.title);
|
|
451
|
+
|
|
452
|
+
if (existing) {
|
|
453
|
+
// Update existing item — bump times_seen, update last_seen, and refresh metadata
|
|
454
|
+
const updates = {
|
|
455
|
+
last_seen: now,
|
|
456
|
+
times_seen: (existing.times_seen || 1) + 1,
|
|
457
|
+
};
|
|
458
|
+
// Update urgency/owner/category if they changed (but don't downgrade user edits)
|
|
459
|
+
if (item.urgency) updates.urgency = item.urgency;
|
|
460
|
+
if (item.owner) updates.owner = item.owner;
|
|
461
|
+
if (item.category) updates.category = item.category;
|
|
462
|
+
if (item.context) updates.context = typeof item.context === 'string' ? item.context : JSON.stringify(item.context);
|
|
463
|
+
brain.updateBriefingItem(existing.id, updates);
|
|
464
|
+
updated++;
|
|
465
|
+
} else {
|
|
466
|
+
// Insert new item
|
|
467
|
+
brain.insertBriefingItem({
|
|
468
|
+
task_id: task.id,
|
|
469
|
+
skill,
|
|
470
|
+
title: item.title,
|
|
471
|
+
category: item.category || null,
|
|
472
|
+
owner: item.owner || null,
|
|
473
|
+
urgency: item.urgency || 'async',
|
|
474
|
+
context: item.context ? (typeof item.context === 'string' ? item.context : JSON.stringify(item.context)) : null,
|
|
475
|
+
first_seen: now,
|
|
476
|
+
last_seen: now,
|
|
477
|
+
});
|
|
478
|
+
inserted++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (inserted || updated) {
|
|
483
|
+
console.log(`[tasks] Briefing items consolidated: ${inserted} new, ${updated} updated (skill: ${skill})`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
module.exports = { runDueTasks, runTaskById, recoverInterruptedTasks, buildTaskPrompt, computeNextDue, getTaskLogs, clearTaskLogs, stopTask, executeSkill, consolidateBriefingItems, taskLogs };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const brain = require('../brain');
|
|
2
|
+
const { extractKnowledge } = require('../extraction/knowledge-extractor');
|
|
3
|
+
const { detectContradictions } = require('../extraction/contradiction');
|
|
4
|
+
|
|
5
|
+
const BATCH_SIZE = 20;
|
|
6
|
+
|
|
7
|
+
async function runOnce(opts = {}) {
|
|
8
|
+
const extractFn = opts.extractFn || extractKnowledge;
|
|
9
|
+
const ownerName = brain.getOwnerName();
|
|
10
|
+
|
|
11
|
+
const pending = brain.listMemories({ extractionStatus: 'pending', limit: BATCH_SIZE });
|
|
12
|
+
if (pending.length === 0) {
|
|
13
|
+
// Still run self-resolution even with no new memories
|
|
14
|
+
await selfResolveQuestions(ownerName, opts);
|
|
15
|
+
return { memoriesProcessed: 0, knowledgeExtracted: 0 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let knowledgeExtracted = 0;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Step 1: Extract knowledge (async API call)
|
|
22
|
+
const entries = await extractFn(pending, ownerName, { model: opts.model });
|
|
23
|
+
|
|
24
|
+
// Step 2: Detect contradictions (async API call, outside transaction)
|
|
25
|
+
const contradictions = await detectContradictions(entries, ownerName, { detectFn: opts.detectFn });
|
|
26
|
+
|
|
27
|
+
// Step 3: DB transaction: insert knowledge + handle contradictions + mark done
|
|
28
|
+
const db = brain.getDb();
|
|
29
|
+
const writeResults = db.transaction(() => {
|
|
30
|
+
// Insert all new knowledge entries, tracking their IDs
|
|
31
|
+
const insertedIds = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const result = brain.insertKnowledge({
|
|
34
|
+
category: entry.category,
|
|
35
|
+
subject: entry.subject,
|
|
36
|
+
predicate: entry.predicate,
|
|
37
|
+
object: entry.object,
|
|
38
|
+
confidence: entry.confidence || 0.5,
|
|
39
|
+
source_memory_ids: JSON.stringify(entry.source_memory_ids || []),
|
|
40
|
+
});
|
|
41
|
+
insertedIds.push({ ...entry, _id: result.id });
|
|
42
|
+
knowledgeExtracted++;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle contradictions
|
|
46
|
+
for (const contradiction of contradictions) {
|
|
47
|
+
// Find the ID of the newly inserted entry that matches this contradiction
|
|
48
|
+
const newInserted = insertedIds.find(e =>
|
|
49
|
+
e.subject === contradiction.new_entry.subject &&
|
|
50
|
+
e.predicate === contradiction.new_entry.predicate &&
|
|
51
|
+
e.object === contradiction.new_entry.object
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Insert pending question
|
|
55
|
+
brain.insertQuestion({
|
|
56
|
+
question_type: 'contradiction',
|
|
57
|
+
question: contradiction.explanation,
|
|
58
|
+
context: JSON.stringify({
|
|
59
|
+
old_id: contradiction.old_id,
|
|
60
|
+
new_entry: contradiction.new_entry,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Supersede old knowledge
|
|
65
|
+
if (newInserted) {
|
|
66
|
+
brain.supersedeKnowledge(contradiction.old_id, newInserted._id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const mem of pending) {
|
|
71
|
+
brain.updateMemoryExtraction(mem.id, 'done');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
writeResults();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('[think] Extraction failed:', err.message);
|
|
77
|
+
for (const mem of pending) {
|
|
78
|
+
try {
|
|
79
|
+
brain.updateMemoryExtraction(mem.id, 'failed');
|
|
80
|
+
} catch (updateErr) {
|
|
81
|
+
console.error('[think] Failed to mark memory as failed:', updateErr.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
brain.upsertCheckpoint('think', {
|
|
87
|
+
last_memory_at: pending[pending.length - 1].timestamp,
|
|
88
|
+
metadata: JSON.stringify({ memories_processed: pending.length, knowledge_extracted: knowledgeExtracted }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Task 4: Self-resolve pending questions
|
|
92
|
+
await selfResolveQuestions(ownerName, opts);
|
|
93
|
+
|
|
94
|
+
return { memoriesProcessed: pending.length, knowledgeExtracted };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function selfResolveQuestions(ownerName, opts = {}) {
|
|
98
|
+
const pendingQuestions = brain.listQuestions({ status: 'pending', limit: 10 });
|
|
99
|
+
|
|
100
|
+
for (const question of pendingQuestions) {
|
|
101
|
+
// Get memories created after the question was created
|
|
102
|
+
const newMemories = brain.listMemories({ since: question.created_at });
|
|
103
|
+
|
|
104
|
+
// Need more than 3 new relevant memories to attempt resolution
|
|
105
|
+
if (newMemories.length <= 3) continue;
|
|
106
|
+
|
|
107
|
+
const selfResolveFn = opts.selfResolveFn;
|
|
108
|
+
if (!selfResolveFn) continue;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = await selfResolveFn(question, newMemories, ownerName);
|
|
112
|
+
if (result && result.resolved) {
|
|
113
|
+
brain.answerQuestion(question.id, {
|
|
114
|
+
answer: result.answer,
|
|
115
|
+
resolution_type: 'inferred',
|
|
116
|
+
resolution_evidence: JSON.stringify(result.evidence),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('[think] Self-resolution failed for question', question.id, ':', err.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { runOnce };
|