parallelclaw 1.1.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  Notable changes to parallelclaw (formerly memex-mvp). Older history lives in the git log.
4
4
 
5
+ ## 1.2.0 — delegate by talking + the laptop as executor
6
+
7
+ Coordination, now from natural language. Two MCP tools so any MCP client (Claude
8
+ Code, Cursor, OpenClaw) can delegate and check on work without touching the CLI:
9
+ - **`parallelclaw_delegate`** `{prompt, to, kind, content}` — hand a task to
10
+ another of your agents. Fired when you say "tell Kimi to …", "delegate this to
11
+ the VPS agent", "have X do this while I'm away". `to:"any"` broadcasts.
12
+ - **`parallelclaw_tasks`** `{filter: mine|inbox|results|all, status, limit}` —
13
+ status of what you delegated, your inbox, or completed answers.
14
+
15
+ SERVER_INSTRUCTIONS gained an "AGENT COORDINATION" recipe so the agent recognizes
16
+ the delegation intent and surfaces results from the SessionStart 📥 block.
17
+
18
+ ### Mac headless executor (Phase 2b) — the laptop runs delegated tasks too
19
+ `lib/executor.js` + `task-executor install|uninstall|status` / `task-run-once`:
20
+ a launchd timer drains the inbox while the Mac is awake, running each claimed
21
+ safe-kind task through `claude -p` and writing the result back — closing any↔any
22
+ auto-coordination both directions. Selective-auto: only SAFE_KINDS (research /
23
+ content / test / general / review / summarize / analysis) auto-run, addressed-to-me
24
+ only unless `--any`; the exec prompt forbids destructive actions and `claude -p`
25
+ runs without `--dangerously-skip-permissions`. Honest limit: only while awake.
26
+ The always-on cron-prompt flow stays the path for Linux/VPS nodes
27
+ (`docs/design/executor-prompt.md`). Hands-off loop live-proven on the Mac↔Kimi mesh.
28
+
5
29
  ## 1.1.0 — agent coordination: hardened task ledger + executor + result-routing
6
30
 
7
31
  The first release where the **coordination layer works end-to-end**: one of your
package/ingest.js CHANGED
@@ -159,6 +159,9 @@ if (subcommand && subcommand !== '--help' && subcommand.startsWith('-') === fals
159
159
  'task-claim': async () => (await import('./lib/tasks.js')).cmdTaskClaim(),
160
160
  'task-update': async () => (await import('./lib/tasks.js')).cmdTaskUpdate(),
161
161
  'task-results': async () => (await import('./lib/tasks.js')).cmdTaskResults(),
162
+ // Phase 2b — Mac headless executor (run delegated inbox tasks via `claude -p`)
163
+ 'task-run-once': async () => (await import('./lib/executor.js')).cmdTaskRunOnce(),
164
+ 'task-executor': async () => (await import('./lib/executor.js')).cmdTaskExecutor(),
162
165
  serve: cmdServe, // explicit foreground; same as no-arg
163
166
  // All scan / export modes fall through to module-level logic at EOF.
164
167
  // cmdServe is a no-op marker so the dispatch doesn't error.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Mac headless executor (Phase 2b).
3
+ *
4
+ * Lets THIS node (a laptop running Claude Code) be an executor too: while the
5
+ * Mac is awake, a launchd timer drains the ParallelClaw inbox by running each
6
+ * claimed task through `claude -p` (headless Claude Code) and writing the result
7
+ * back. The always-on nodes (Kimi/OpenClaw) use the cron-PROMPT instead
8
+ * (docs/design/executor-prompt.md); this is the code path for the laptop.
9
+ *
10
+ * Honest limit: launchd StartInterval only fires while the Mac is awake — a
11
+ * sleeping laptop simply doesn't run, and the task WAITS in the ledger. There is
12
+ * no cloud Claude Code, so "run it now while my laptop is closed" can't happen.
13
+ *
14
+ * Safety (selective-auto): only tasks whose `kind` is in SAFE_KINDS auto-run,
15
+ * and (by default) only those addressed to me — broadcasts ("any") are left for
16
+ * the always-on nodes unless --any is passed. The exec prompt forbids
17
+ * destructive/irreversible actions; `claude -p` runs WITHOUT
18
+ * --dangerously-skip-permissions, so risky tools are declined rather than
19
+ * silently performed.
20
+ */
21
+
22
+ import { homedir, platform } from 'node:os';
23
+ import { join, resolve } from 'node:path';
24
+ import { writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
25
+ import { execFile, execSync } from 'node:child_process';
26
+
27
+ import { getOrigin } from './config.js';
28
+ import { listTasks, claimTask, updateTask } from './tasks.js';
29
+
30
+ const HOME = homedir();
31
+ const MEMEX_DIR = process.env.MEMEX_DIR || join(HOME, '.memex');
32
+ const DATA = join(MEMEX_DIR, 'data');
33
+
34
+ const EXEC_LABEL = 'com.parallelclaw.memex.executor';
35
+ const EXEC_PLIST = join(HOME, 'Library', 'LaunchAgents', `${EXEC_LABEL}.plist`);
36
+ const EXEC_OUT_LOG = join(DATA, 'executor.out.log');
37
+ const EXEC_ERR_LOG = join(DATA, 'executor.err.log');
38
+
39
+ // Auto-runnable task classes. Read/produce-text work is safe to run unattended;
40
+ // anything that mutates another machine's state is intentionally absent and is
41
+ // left in the inbox for explicit handling (one-tap approval = future v1 UI).
42
+ export const SAFE_KINDS = ['research', 'content', 'test', 'general', 'review', 'summarize', 'analysis'];
43
+
44
+ const DEFAULT_TIMEOUT_MS = 240000; // 4 min — matches the always-on cron budget
45
+
46
+ function buildExecPrompt(env) {
47
+ return [
48
+ 'You are an autonomous ParallelClaw executor running headlessly via `claude -p`.',
49
+ 'A task was delegated to you by another of the user\'s agents. Do it, then print ONLY the',
50
+ 'result/output (no preamble, no "I will…").',
51
+ '',
52
+ 'SAFETY: do NOT perform destructive or irreversible actions — no deploys, no git push/',
53
+ 'force-push, no deleting or overwriting files outside a scratch directory, no spending, no',
54
+ 'outbound messages. If the task genuinely requires any of that, instead print exactly what',
55
+ 'needs human approval and stop.',
56
+ '',
57
+ `TASK (kind: ${env.kind || 'general'}, from: ${env.from || '?'}):`,
58
+ env.prompt || '',
59
+ env.content ? `\nADDITIONAL CONTEXT:\n${env.content}` : '',
60
+ ].join('\n');
61
+ }
62
+
63
+ /** Default runner: headless `claude -p`. Returns stdout; rejects on error/timeout. */
64
+ function claudeRunner(prompt, { timeoutMs = DEFAULT_TIMEOUT_MS, cwd } = {}) {
65
+ return new Promise((res, rej) => {
66
+ const bin = process.env.PARALLELCLAW_CLAUDE_BIN || 'claude';
67
+ execFile(bin, ['-p', prompt],
68
+ { timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024, encoding: 'utf8', cwd: cwd || DATA },
69
+ (err, stdout, stderr) => {
70
+ if (err) {
71
+ if (err.killed) return rej(new Error(`claude -p timed out after ${timeoutMs}ms`));
72
+ return rej(new Error(String(stderr || err.message).slice(0, 300)));
73
+ }
74
+ res(String(stdout || '').trim());
75
+ });
76
+ });
77
+ }
78
+
79
+ /**
80
+ * One drain pass over the inbox. opts:
81
+ * db? — shared connection (else opens its own writable handle)
82
+ * runner? — async (prompt, {timeoutMs}) => string (default: claude -p) — injectable for tests
83
+ * allowKinds? — auto-run allowlist (default SAFE_KINDS)
84
+ * includeBroadcast? — also take to:'any' tasks (default false — leave for always-on nodes)
85
+ * timeoutMs?, now?, log?
86
+ * Returns [{ id, status, skipped? }] for what it acted on.
87
+ */
88
+ export async function runInboxOnce({
89
+ db = null, runner = null, allowKinds = SAFE_KINDS, includeBroadcast = false,
90
+ timeoutMs = DEFAULT_TIMEOUT_MS, now = Date.now(), log = () => {},
91
+ } = {}) {
92
+ const me = getOrigin();
93
+ const exec = runner || claudeRunner;
94
+ const acted = [];
95
+ let inbox = listTasks({ inbox: true, db, limit: 50 });
96
+ inbox = inbox.filter((t) => t.to === me || (includeBroadcast && t.to === 'any'));
97
+ for (const t of inbox) {
98
+ if (!allowKinds.includes(t.kind)) {
99
+ log(`skip ${t.id}: kind '${t.kind}' not in auto-allowlist (left for one-tap)`);
100
+ acted.push({ id: t.id, status: 'skipped', reason: 'kind' });
101
+ continue;
102
+ }
103
+ const claimed = claimTask(t.id, { db, now });
104
+ if (!claimed) { log(`skip ${t.id}: not claimable (taken/raced)`); acted.push({ id: t.id, status: 'skipped', reason: 'claim' }); continue; }
105
+ try {
106
+ const out = await exec(buildExecPrompt(claimed), { timeoutMs });
107
+ const result = (out && String(out).trim()) || '(executor produced no output)';
108
+ updateTask(t.id, 'done', { result, db });
109
+ log(`done ${t.id}`);
110
+ acted.push({ id: t.id, status: 'done' });
111
+ } catch (e) {
112
+ updateTask(t.id, 'failed', { result: `executor error: ${e.message}`.slice(0, 500), db });
113
+ log(`failed ${t.id}: ${e.message}`);
114
+ acted.push({ id: t.id, status: 'failed' });
115
+ }
116
+ }
117
+ return acted;
118
+ }
119
+
120
+ // ── launchd (macOS) — run one drain pass every N min while awake ──────────────
121
+
122
+ function resolveClaudeBin() {
123
+ try { return execSync('command -v claude', { encoding: 'utf8' }).trim() || 'claude'; }
124
+ catch (_) { return 'claude'; }
125
+ }
126
+
127
+ export function buildExecutorLaunchAgentPlist({ script, mins, nodePath, claudeBin, includeBroadcast }) {
128
+ const interval = Math.max(60, Math.floor(mins * 60));
129
+ const argv = [nodePath, script, 'task-run-once'];
130
+ if (includeBroadcast) argv.push('--any');
131
+ const argXml = argv.map((a) => ` <string>${a}</string>`).join('\n');
132
+ // launchd's PATH is minimal and excludes /opt/homebrew/bin where `claude`
133
+ // usually lives — bake an explicit PATH + the resolved claude path so the
134
+ // headless run finds it. (Same class of bug as the brew `timeout` PATH trap.)
135
+ return `<?xml version="1.0" encoding="UTF-8"?>
136
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
137
+ <plist version="1.0">
138
+ <dict>
139
+ <key>Label</key>
140
+ <string>${EXEC_LABEL}</string>
141
+ <key>ProgramArguments</key>
142
+ <array>
143
+ ${argXml}
144
+ </array>
145
+ <key>EnvironmentVariables</key>
146
+ <dict>
147
+ <key>HOME</key><string>${HOME}</string>
148
+ <key>MEMEX_DIR</key><string>${MEMEX_DIR}</string>
149
+ <key>PARALLELCLAW_CLAUDE_BIN</key><string>${claudeBin}</string>
150
+ <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
151
+ </dict>
152
+ <key>RunAtLoad</key><true/>
153
+ <key>StartInterval</key><integer>${interval}</integer>
154
+ <key>ProcessType</key><string>Background</string>
155
+ <key>StandardOutPath</key><string>${EXEC_OUT_LOG}</string>
156
+ <key>StandardErrorPath</key><string>${EXEC_ERR_LOG}</string>
157
+ <key>WorkingDirectory</key><string>${resolve(script, '..')}</string>
158
+ </dict>
159
+ </plist>
160
+ `;
161
+ }
162
+
163
+ export function installExecutor({ scriptPath, everyMinutes = 10, nodePath = process.execPath, includeBroadcast = false } = {}) {
164
+ if (platform() !== 'darwin') {
165
+ throw new Error('The headless executor service is macOS-only. On an always-on Linux node, use the cron-prompt in docs/design/executor-prompt.md instead.');
166
+ }
167
+ const script = resolve(scriptPath || process.argv[1]);
168
+ if (!existsSync(script)) throw new Error(`installExecutor: script not found at ${script}`);
169
+ const mins = Math.max(1, Math.floor(Number(everyMinutes) || 10));
170
+ const claudeBin = resolveClaudeBin();
171
+ mkdirSync(DATA, { recursive: true });
172
+ mkdirSync(join(HOME, 'Library', 'LaunchAgents'), { recursive: true });
173
+ const plist = buildExecutorLaunchAgentPlist({ script, mins, nodePath, claudeBin, includeBroadcast });
174
+ try { execSync(`launchctl unload ${JSON.stringify(EXEC_PLIST)}`, { stdio: 'ignore' }); } catch (_) {}
175
+ writeFileSync(EXEC_PLIST, plist);
176
+ execSync(`launchctl load ${JSON.stringify(EXEC_PLIST)}`, { stdio: 'inherit' });
177
+ return { platform: 'darwin', unitPath: EXEC_PLIST, everyMinutes: mins, claudeBin, includeBroadcast };
178
+ }
179
+
180
+ export function uninstallExecutor() {
181
+ if (platform() !== 'darwin') return { platform: platform(), unitPath: null };
182
+ try { execSync(`launchctl unload ${JSON.stringify(EXEC_PLIST)}`, { stdio: 'ignore' }); } catch (_) {}
183
+ if (existsSync(EXEC_PLIST)) unlinkSync(EXEC_PLIST);
184
+ return { platform: 'darwin', unitPath: EXEC_PLIST };
185
+ }
186
+
187
+ export function executorStatus() {
188
+ if (platform() !== 'darwin') return { installed: false, running: false, manager: 'none' };
189
+ const installed = existsSync(EXEC_PLIST);
190
+ let running = false, detail = '';
191
+ if (installed) {
192
+ try {
193
+ const out = execSync(`launchctl list 2>/dev/null | grep ${EXEC_LABEL} || true`, { encoding: 'utf8' });
194
+ running = out.trim().length > 0; detail = out.trim();
195
+ } catch (_) {}
196
+ }
197
+ return { installed, running, manager: 'launchd', unitPath: EXEC_PLIST, detail };
198
+ }
199
+
200
+ // ── CLI ──────────────────────────────────────────────────────────────────────
201
+
202
+ export async function cmdTaskRunOnce() {
203
+ const argv = process.argv.slice(3);
204
+ const includeBroadcast = argv.includes('--any');
205
+ const acted = await runInboxOnce({ includeBroadcast, log: (m) => console.error(`[executor] ${m}`) });
206
+ const ran = acted.filter((a) => a.status === 'done' || a.status === 'failed');
207
+ if (!ran.length) { console.log('executor: nothing to run.'); process.exit(0); }
208
+ for (const r of ran) console.log(`${r.status === 'done' ? '✓' : '✗'} ${r.id} ${r.status}`);
209
+ process.exit(0);
210
+ }
211
+
212
+ export function cmdTaskExecutor() {
213
+ const sub = process.argv[3];
214
+ const argv = process.argv.slice(4);
215
+ const getOpt = (flag, def) => { const i = argv.indexOf(flag); return i >= 0 && argv[i + 1] ? argv[i + 1] : def; };
216
+ try {
217
+ if (sub === 'install') {
218
+ const r = installExecutor({
219
+ everyMinutes: parseInt(getOpt('--every', '10'), 10) || 10,
220
+ includeBroadcast: argv.includes('--any'),
221
+ });
222
+ console.log(`✓ Mac executor installed (every ${r.everyMinutes}m while awake).`);
223
+ console.log(` claude: ${r.claudeBin} broadcast tasks: ${r.includeBroadcast ? 'yes' : 'no (addressed-only)'}`);
224
+ console.log(` auto-runs kinds: ${SAFE_KINDS.join(', ')} — others wait in the inbox.`);
225
+ console.log(` unit: ${r.unitPath}`);
226
+ } else if (sub === 'uninstall') {
227
+ uninstallExecutor(); console.log('✓ Mac executor uninstalled (tasks/data preserved).');
228
+ } else if (sub === 'status') {
229
+ const s = executorStatus();
230
+ console.log(`executor: installed=${s.installed} running=${s.running} (${s.manager})`);
231
+ if (s.detail) console.log(` ${s.detail}`);
232
+ } else {
233
+ console.error('usage: task-executor <install|uninstall|status> [--every <min>] [--any]');
234
+ process.exit(2);
235
+ }
236
+ } catch (e) { console.error(`✗ ${e.message}`); process.exit(1); }
237
+ process.exit(0);
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parallelclaw",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Local-first personal AI ops layer. Shared verbatim memory across your agents (Claude Code, Cursor, OpenClaw, Hermes, Obsidian, Telegram) in one SQLite + FTS5 corpus — plus a coordination layer where any of your agents can delegate tasks to any other. Searchable from any MCP-compatible client.",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -31,7 +31,7 @@
31
31
  "sync": "node ingest.js",
32
32
  "ingest": "node ingest.js",
33
33
  "bot": "node bot/index.js",
34
- "test": "node test/parser.test.js && node test/bot-inbox.test.js && node test/search-sort.test.js && node test/get-conversation-paging.test.js && node test/search-filters.test.js && node test/origin.test.js && node test/tasks.test.js && node test/store-document.test.js && node test/cli.test.js && node test/hook.test.js && node test/telegram-html.test.js && node test/telegram-decisions.test.js && node test/telegram-pending.test.js && node test/telegram-notify.test.js && node test/notify-click-action.test.js && node test/inbox-watcher.test.js && node test/e2e-inbox.test.js && node test/ingest-file.test.js && node test/openclaw-channel.test.js && node test/openclaw-channel-e2e.test.js && node test/wire-openclaw.test.js && node test/sync/server-bootstrap.test.js && node test/sync/push-pull-roundtrip.test.js && node test/sync/cli-end-to-end.test.js && node test/sync/adaptive-batch.test.js && node test/sync/service.test.js && node test/sync/skip-accounting.test.js && node test/sync/pair.test.js && node test/sync/join-token.test.js && node test/sync/tunnel-service.test.js && node test/sync/mcp-invite.test.js && node test/sync/skip-accounting.test.js",
34
+ "test": "node test/parser.test.js && node test/bot-inbox.test.js && node test/search-sort.test.js && node test/get-conversation-paging.test.js && node test/search-filters.test.js && node test/origin.test.js && node test/tasks.test.js && node test/executor.test.js && node test/store-document.test.js && node test/cli.test.js && node test/hook.test.js && node test/telegram-html.test.js && node test/telegram-decisions.test.js && node test/telegram-pending.test.js && node test/telegram-notify.test.js && node test/notify-click-action.test.js && node test/inbox-watcher.test.js && node test/e2e-inbox.test.js && node test/ingest-file.test.js && node test/openclaw-channel.test.js && node test/openclaw-channel-e2e.test.js && node test/wire-openclaw.test.js && node test/sync/server-bootstrap.test.js && node test/sync/push-pull-roundtrip.test.js && node test/sync/cli-end-to-end.test.js && node test/sync/adaptive-batch.test.js && node test/sync/service.test.js && node test/sync/skip-accounting.test.js && node test/sync/pair.test.js && node test/sync/join-token.test.js && node test/sync/tunnel-service.test.js && node test/sync/mcp-invite.test.js && node test/sync/skip-accounting.test.js",
35
35
  "prepublishOnly": "npm test"
36
36
  },
37
37
  "engines": {
package/server.js CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  KNOWN_SOURCES,
45
45
  CONFIG_PATH,
46
46
  } from './lib/config.js';
47
+ import { createTask, listTasks, listTaskResults } from './lib/tasks.js';
47
48
  import {
48
49
  canonicalize as canonicalizeUrl,
49
50
  extractDomain,
@@ -1329,6 +1330,18 @@ This merges main + subagent messages in chronological order with a
1329
1330
  titles. Compare short_ids and date ranges before concluding duplicate.
1330
1331
  - Re-imports are idempotent (UNIQUE on msg_id, recount-on-upsert).
1331
1332
 
1333
+ ══ AGENT COORDINATION (delegate tasks across your agents) ══
1334
+
1335
+ ParallelClaw isn't only memory — your agents can hand tasks to each other over
1336
+ the same synced ledger. When the user says "tell Kimi to …", "ask the VPS agent
1337
+ to …", "delegate this to <agent>", or "have <agent> do X while I'm away":
1338
+ → call parallelclaw_delegate({ prompt, to: "<agent>" }) (to:"any" = broadcast)
1339
+ The target agent claims it (atomic — never double-run) and runs it when
1340
+ available; the answer returns to you. To check delegated work or surface answers,
1341
+ call parallelclaw_tasks (filter: mine / inbox / results). Newly-finished results
1342
+ also appear automatically in this session's "📥 results from tasks you delegated"
1343
+ block. Don't shell out to raw SQL or the task-* CLI from here — use these tools.
1344
+
1332
1345
  ══ CONVENTIONS ══
1333
1346
 
1334
1347
  - BE PROACTIVE. If past context would sharpen your answer, pull it
@@ -1985,6 +1998,43 @@ const TOOLS = [
1985
1998
  },
1986
1999
  },
1987
2000
  },
2001
+ {
2002
+ name: 'parallelclaw_delegate',
2003
+ description:
2004
+ 'Hand a task to ANOTHER of the user\'s agents (cross-tool, cross-machine) via the shared ParallelClaw ledger. ' +
2005
+ 'CALL THIS when the user says "tell Kimi to …", "ask the VPS agent to …", "delegate this to <agent>", ' +
2006
+ '"have <agent> do X while I\'m away", or when an always-on agent should run bulk/long work. The task is ' +
2007
+ 'written to the synced ledger; the target agent claims and runs it when available, and the result returns ' +
2008
+ 'to you (your next session\'s 📥 block, or via parallelclaw_tasks filter:"results"). Set `to` to the target ' +
2009
+ 'node\'s origin label (e.g. "kimi", "vps1"); omit or "any" to broadcast to whichever always-on agent grabs ' +
2010
+ 'it first. Destructive cross-machine actions are NOT auto-run — state that in the prompt.',
2011
+ inputSchema: {
2012
+ type: 'object',
2013
+ properties: {
2014
+ prompt: { type: 'string', description: 'What the other agent should do — self-contained; it has no access to this conversation.' },
2015
+ to: { type: 'string', default: 'any', description: 'Target agent origin label (e.g. "kimi", "vps1"), or "any" to broadcast.' },
2016
+ kind: { type: 'string', default: 'general', description: 'Task class hint: research / code / content / test / …' },
2017
+ content: { type: 'string', description: 'Optional extra context/material to carry with the task.' },
2018
+ },
2019
+ required: ['prompt'],
2020
+ },
2021
+ },
2022
+ {
2023
+ name: 'parallelclaw_tasks',
2024
+ description:
2025
+ 'Check the ParallelClaw task ledger. filter="mine" (default): tasks you delegated + their status/result; ' +
2026
+ '"inbox": tasks waiting for THIS node to run; "results": completed answers to your delegations; "all": ' +
2027
+ 'everything. Use after parallelclaw_delegate to see whether the other agent finished, or to surface results.',
2028
+ inputSchema: {
2029
+ type: 'object',
2030
+ properties: {
2031
+ filter: { type: 'string', enum: ['mine', 'inbox', 'results', 'all'], default: 'mine' },
2032
+ status: { type: 'string', enum: ['submitted', 'working', 'done', 'failed'], description: 'Optional status filter.' },
2033
+ limit: { type: 'integer', default: 20, minimum: 1, maximum: 100 },
2034
+ format: { type: 'string', enum: ['markdown', 'json'], default: 'markdown' },
2035
+ },
2036
+ },
2037
+ },
1988
2038
  ];
1989
2039
 
1990
2040
  // v0.11.11 experimental sync — expose the one-paste pairing tool ONLY when
@@ -3575,6 +3625,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
3575
3625
  });
3576
3626
  }
3577
3627
 
3628
+ if (name === 'parallelclaw_delegate') {
3629
+ const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
3630
+ if (!prompt) return textResult('parallelclaw_delegate: `prompt` is required (what should the other agent do?).');
3631
+ const to = (typeof args.to === 'string' && args.to.trim()) ? args.to.trim().toLowerCase() : 'any';
3632
+ const kind = (typeof args.kind === 'string' && args.kind.trim()) ? args.kind.trim() : 'general';
3633
+ const content = typeof args.content === 'string' ? args.content : null;
3634
+ const { id } = createTask({ prompt, to, kind, content });
3635
+ return textResult(
3636
+ `✓ Delegated task \`${id}\` → **${to}** (from ${getOrigin()}).\n` +
3637
+ `Runs when ${to === 'any' ? 'an always-on agent' : to} is available; the result returns to you ` +
3638
+ `(next session's 📥 block, or parallelclaw_tasks filter:"results").`
3639
+ );
3640
+ }
3641
+
3642
+ if (name === 'parallelclaw_tasks') {
3643
+ const filter = ['mine', 'inbox', 'results', 'all'].includes(args.filter) ? args.filter : 'mine';
3644
+ const limit = Math.min(100, Math.max(1, args.limit || 20));
3645
+ const statusFilter = ['submitted', 'working', 'done', 'failed'].includes(args.status) ? args.status : null;
3646
+ let rows;
3647
+ if (filter === 'results') rows = listTaskResults({});
3648
+ else if (filter === 'inbox') rows = listTasks({ inbox: true, limit });
3649
+ else if (filter === 'mine') rows = listTasks({ mine: true, status: statusFilter, limit });
3650
+ else rows = listTasks({ status: statusFilter, limit });
3651
+ if (pickFormat(args) === 'json') return jsonResult({ filter, count: rows.length, tasks: rows });
3652
+ if (!rows.length) return textResult(`No tasks (filter: ${filter}).`);
3653
+ const icon = { submitted: '○', working: '◐', done: '✓', failed: '✗' };
3654
+ const lines = [`ParallelClaw tasks (${filter}):`, ''];
3655
+ for (const t of rows) {
3656
+ lines.push(`${icon[t.status] || '?'} \`${t.id}\` ${t.from} → ${t.to} [${t.status}]`);
3657
+ lines.push(` ${String(t.prompt || '').slice(0, 90)}`);
3658
+ if (t.result) lines.push(` └ result: ${String(t.result).slice(0, 200)}`);
3659
+ }
3660
+ return textResult(lines.join('\n'));
3661
+ }
3662
+
3578
3663
  return textResult(`Unknown tool: ${name}`);
3579
3664
  } catch (err) {
3580
3665
  log('tool error:', name, err.message);