taskplane 0.22.17 → 0.23.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/dashboard/public/app.js +365 -7
- package/dashboard/public/index.html +16 -0
- package/dashboard/public/style.css +105 -0
- package/dashboard/server.cjs +199 -0
- package/extensions/task-runner.ts +64 -349
- package/extensions/taskplane/abort.ts +11 -1
- package/extensions/taskplane/agent-bridge-extension.ts +159 -0
- package/extensions/taskplane/agent-host.ts +686 -0
- package/extensions/taskplane/engine.ts +75 -3
- package/extensions/taskplane/execution.ts +403 -9
- package/extensions/taskplane/extension.ts +322 -28
- package/extensions/taskplane/lane-runner.ts +567 -0
- package/extensions/taskplane/mailbox.ts +349 -1
- package/extensions/taskplane/merge.ts +208 -51
- package/extensions/taskplane/process-registry.ts +345 -0
- package/extensions/taskplane/resume.ts +185 -47
- package/extensions/taskplane/supervisor.ts +16 -12
- package/extensions/taskplane/task-executor-core.ts +553 -0
- package/extensions/taskplane/types.ts +517 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +41 -33
- package/skills/create-taskplane-task/references/prompt-template.md +3 -3
package/dashboard/server.cjs
CHANGED
|
@@ -352,6 +352,167 @@ function tailJsonlFile(filePath) {
|
|
|
352
352
|
* @param {object|null} batchState - The batch state from batch-state.json
|
|
353
353
|
* @returns {object} Map of tmuxPrefix → accumulated telemetry
|
|
354
354
|
*/
|
|
355
|
+
|
|
356
|
+
// ── Runtime V2 Data Loaders (TP-107) ─────────────────────────────
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Load the Runtime V2 process registry for the current batch.
|
|
360
|
+
* Returns null if no registry exists (legacy batch).
|
|
361
|
+
*/
|
|
362
|
+
function loadRuntimeRegistry(batchId) {
|
|
363
|
+
if (!batchId) return null;
|
|
364
|
+
const registryPath = path.join(REPO_ROOT, ".pi", "runtime", batchId, "registry.json");
|
|
365
|
+
try {
|
|
366
|
+
if (!fs.existsSync(registryPath)) return null;
|
|
367
|
+
return JSON.parse(fs.readFileSync(registryPath, "utf-8"));
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Load Runtime V2 lane snapshots for the current batch.
|
|
375
|
+
* Returns a map of laneNumber → snapshot data.
|
|
376
|
+
*/
|
|
377
|
+
function loadRuntimeLaneSnapshots(batchId) {
|
|
378
|
+
if (!batchId) return {};
|
|
379
|
+
const lanesDir = path.join(REPO_ROOT, ".pi", "runtime", batchId, "lanes");
|
|
380
|
+
const snapshots = {};
|
|
381
|
+
try {
|
|
382
|
+
if (!fs.existsSync(lanesDir)) return snapshots;
|
|
383
|
+
const files = fs.readdirSync(lanesDir).filter(f => f.startsWith("lane-") && f.endsWith(".json"));
|
|
384
|
+
for (const file of files) {
|
|
385
|
+
try {
|
|
386
|
+
const data = JSON.parse(fs.readFileSync(path.join(lanesDir, file), "utf-8"));
|
|
387
|
+
if (data.laneNumber != null) snapshots[data.laneNumber] = data;
|
|
388
|
+
} catch { continue; }
|
|
389
|
+
}
|
|
390
|
+
} catch { /* dir missing */ }
|
|
391
|
+
return snapshots;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Load Runtime V2 agent events for a specific agent.
|
|
396
|
+
* Returns the last N events from the agent's events.jsonl.
|
|
397
|
+
*/
|
|
398
|
+
function loadRuntimeAgentEvents(batchId, agentId, maxEvents) {
|
|
399
|
+
if (!batchId || !agentId) return [];
|
|
400
|
+
maxEvents = maxEvents || 200;
|
|
401
|
+
const eventsPath = path.join(REPO_ROOT, ".pi", "runtime", batchId, "agents", agentId, "events.jsonl");
|
|
402
|
+
try {
|
|
403
|
+
if (!fs.existsSync(eventsPath)) return [];
|
|
404
|
+
const raw = fs.readFileSync(eventsPath, "utf-8");
|
|
405
|
+
const lines = raw.split("\n").filter(l => l.trim());
|
|
406
|
+
const events = [];
|
|
407
|
+
const start = Math.max(0, lines.length - maxEvents);
|
|
408
|
+
for (let i = start; i < lines.length; i++) {
|
|
409
|
+
try { events.push(JSON.parse(lines[i])); } catch { continue; }
|
|
410
|
+
}
|
|
411
|
+
return events;
|
|
412
|
+
} catch {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Load mailbox message activity for the current batch.
|
|
419
|
+
*
|
|
420
|
+
* TP-093 hardening: event-authoritative model.
|
|
421
|
+
* Primary source: .pi/mailbox/{batchId}/events.jsonl (audit event stream).
|
|
422
|
+
* Fallback: directory scans (inbox/ack/outbox/outbox/processed) for
|
|
423
|
+
* compatibility when events.jsonl is absent.
|
|
424
|
+
*
|
|
425
|
+
* Includes:
|
|
426
|
+
* - Consumed replies (outbox/processed/) so they don't disappear after ack
|
|
427
|
+
* - Per-recipient broadcast delivery state from ack markers
|
|
428
|
+
* - Rate-limited events in the timeline
|
|
429
|
+
*/
|
|
430
|
+
function loadMailboxData(batchId) {
|
|
431
|
+
if (!batchId) return { messages: [], agentIds: [], auditEvents: [] };
|
|
432
|
+
const mbRoot = path.join(REPO_ROOT, ".pi", "mailbox", batchId);
|
|
433
|
+
if (!fs.existsSync(mbRoot)) return { messages: [], agentIds: [], auditEvents: [] };
|
|
434
|
+
|
|
435
|
+
// ── Primary: events.jsonl audit trail ──
|
|
436
|
+
const auditEvents = loadMailboxAuditEvents(mbRoot);
|
|
437
|
+
|
|
438
|
+
// ── Fallback: directory scan ──
|
|
439
|
+
const messages = [];
|
|
440
|
+
const agentIds = [];
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const dirs = fs.readdirSync(mbRoot, { withFileTypes: true })
|
|
444
|
+
.filter(d => d.isDirectory())
|
|
445
|
+
.map(d => d.name);
|
|
446
|
+
|
|
447
|
+
for (const agentDir of dirs) {
|
|
448
|
+
if (agentDir === "_broadcast") continue;
|
|
449
|
+
agentIds.push(agentDir);
|
|
450
|
+
|
|
451
|
+
// Scan inbox (pending), ack (delivered), outbox (active replies), outbox/processed (consumed replies)
|
|
452
|
+
for (const subdir of ["inbox", "ack", "outbox", "outbox/processed"]) {
|
|
453
|
+
const dir = path.join(mbRoot, agentDir, subdir);
|
|
454
|
+
if (!fs.existsSync(dir)) continue;
|
|
455
|
+
try {
|
|
456
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith(".msg.json"));
|
|
457
|
+
for (const file of files) {
|
|
458
|
+
try {
|
|
459
|
+
const msg = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
|
|
460
|
+
let status;
|
|
461
|
+
if (subdir === "inbox") status = "pending";
|
|
462
|
+
else if (subdir === "ack") status = "delivered";
|
|
463
|
+
else if (subdir === "outbox") status = "reply";
|
|
464
|
+
else status = "reply-acked"; // outbox/processed
|
|
465
|
+
const isBroadcast = msg.to === "_broadcast";
|
|
466
|
+
messages.push({ ...msg, _status: status, _agentDir: agentDir, _isBroadcast: isBroadcast });
|
|
467
|
+
} catch { continue; }
|
|
468
|
+
}
|
|
469
|
+
} catch { continue; }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// _broadcast: per-recipient delivery state
|
|
474
|
+
const broadcastInbox = path.join(mbRoot, "_broadcast", "inbox");
|
|
475
|
+
const broadcastAck = path.join(mbRoot, "_broadcast", "ack");
|
|
476
|
+
for (const [dir, status] of [[broadcastInbox, "pending"], [broadcastAck, "delivered"]]) {
|
|
477
|
+
if (!fs.existsSync(dir)) continue;
|
|
478
|
+
try {
|
|
479
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith(".msg.json"));
|
|
480
|
+
for (const file of files) {
|
|
481
|
+
try {
|
|
482
|
+
const msg = JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8"));
|
|
483
|
+
messages.push({ ...msg, _status: status, _agentDir: "_broadcast", _isBroadcast: true });
|
|
484
|
+
} catch { continue; }
|
|
485
|
+
}
|
|
486
|
+
} catch { continue; }
|
|
487
|
+
}
|
|
488
|
+
} catch { /* mailbox dir issues */ }
|
|
489
|
+
|
|
490
|
+
messages.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
491
|
+
return { messages, agentIds, auditEvents };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Load mailbox audit events from events.jsonl.
|
|
496
|
+
* Returns events sorted by timestamp. Includes: message_sent, message_delivered,
|
|
497
|
+
* message_replied, message_escalated, message_rate_limited.
|
|
498
|
+
*/
|
|
499
|
+
function loadMailboxAuditEvents(mbRoot) {
|
|
500
|
+
const eventsPath = path.join(mbRoot, "events.jsonl");
|
|
501
|
+
if (!fs.existsSync(eventsPath)) return [];
|
|
502
|
+
try {
|
|
503
|
+
const raw = fs.readFileSync(eventsPath, "utf-8");
|
|
504
|
+
const events = [];
|
|
505
|
+
for (const line of raw.split("\n")) {
|
|
506
|
+
if (!line.trim()) continue;
|
|
507
|
+
try { events.push(JSON.parse(line)); } catch { continue; }
|
|
508
|
+
}
|
|
509
|
+
return events;
|
|
510
|
+
} catch {
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
355
516
|
function loadTelemetryData(batchState) {
|
|
356
517
|
const telemetryDir = path.join(REPO_ROOT, ".pi", "telemetry");
|
|
357
518
|
const result = {};
|
|
@@ -849,11 +1010,20 @@ function buildDashboardState() {
|
|
|
849
1010
|
return { ...task, statusData };
|
|
850
1011
|
});
|
|
851
1012
|
|
|
1013
|
+
// TP-107: Load Runtime V2 data when available
|
|
1014
|
+
const runtimeRegistry = loadRuntimeRegistry(state.batchId);
|
|
1015
|
+
const runtimeLaneSnapshots = loadRuntimeLaneSnapshots(state.batchId);
|
|
1016
|
+
const mailboxData = loadMailboxData(state.batchId);
|
|
1017
|
+
|
|
852
1018
|
return {
|
|
853
1019
|
laneStates,
|
|
854
1020
|
telemetry,
|
|
855
1021
|
batchTotalCost,
|
|
856
1022
|
supervisor,
|
|
1023
|
+
// Runtime V2 data (null/empty for legacy batches)
|
|
1024
|
+
runtimeRegistry,
|
|
1025
|
+
runtimeLaneSnapshots,
|
|
1026
|
+
mailbox: mailboxData,
|
|
857
1027
|
batch: {
|
|
858
1028
|
batchId: state.batchId,
|
|
859
1029
|
phase: state.phase,
|
|
@@ -1220,6 +1390,35 @@ function createServer() {
|
|
|
1220
1390
|
} else if (pathname.startsWith("/api/conversation/") && req.method === "GET") {
|
|
1221
1391
|
const prefix = pathname.slice("/api/conversation/".length);
|
|
1222
1392
|
serveConversation(req, res, prefix);
|
|
1393
|
+
} else if (pathname.startsWith("/api/agent-events/") && req.method === "GET") {
|
|
1394
|
+
// TP-107: Serve Runtime V2 agent events (hardened)
|
|
1395
|
+
const agentId = decodeURIComponent(pathname.slice("/api/agent-events/".length));
|
|
1396
|
+
// Strict validation: same pattern as /api/conversation/:prefix
|
|
1397
|
+
if (!/^[\w-]+$/.test(agentId)) {
|
|
1398
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
1399
|
+
res.end("Invalid agent ID");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const batchState = loadBatchState();
|
|
1403
|
+
// Path containment: verify resolved path stays inside runtime dir
|
|
1404
|
+
if (batchState?.batchId) {
|
|
1405
|
+
const runtimeBase = path.join(REPO_ROOT, ".pi", "runtime", batchState.batchId, "agents");
|
|
1406
|
+
const resolvedAgent = path.resolve(runtimeBase, agentId);
|
|
1407
|
+
if (!resolvedAgent.startsWith(path.resolve(runtimeBase))) {
|
|
1408
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
1409
|
+
res.end("Forbidden");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
// Optional: ?sinceTs= to return only events after a timestamp
|
|
1414
|
+
const reqUrl = new URL(req.url, "http://localhost");
|
|
1415
|
+
const sinceTs = parseInt(reqUrl.searchParams.get("sinceTs") || "0", 10);
|
|
1416
|
+
let events = loadRuntimeAgentEvents(batchState?.batchId, agentId, 300);
|
|
1417
|
+
if (sinceTs > 0) {
|
|
1418
|
+
events = events.filter(e => (e.ts || 0) > sinceTs);
|
|
1419
|
+
}
|
|
1420
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
1421
|
+
res.end(JSON.stringify(events));
|
|
1223
1422
|
} else if (pathname === "/api/state" && req.method === "GET") {
|
|
1224
1423
|
const state = buildDashboardState();
|
|
1225
1424
|
res.writeHead(200, {
|