@synergenius/flow-weaver-pack-weaver 0.9.140 → 0.9.142
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/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +5 -11
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +185 -15
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/dashboard.js +3 -3
- package/dist/bot/dashboard.js.map +1 -1
- package/dist/bot/hierarchy-event-log.d.ts +37 -0
- package/dist/bot/hierarchy-event-log.d.ts.map +1 -0
- package/dist/bot/hierarchy-event-log.js +58 -0
- package/dist/bot/hierarchy-event-log.js.map +1 -0
- package/dist/bot/operations.d.ts +2 -0
- package/dist/bot/operations.d.ts.map +1 -1
- package/dist/bot/operations.js +5 -0
- package/dist/bot/operations.js.map +1 -1
- package/dist/bot/profile-store.d.ts.map +1 -1
- package/dist/bot/profile-store.js +39 -0
- package/dist/bot/profile-store.js.map +1 -1
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +7 -17
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +33 -1
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +1 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +59 -5
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-store.d.ts +1 -1
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +21 -36
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/task-types.d.ts +5 -1
- package/dist/bot/task-types.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +2 -2
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +32 -0
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/report.d.ts +1 -1
- package/dist/node-types/report.d.ts.map +1 -1
- package/dist/node-types/report.js +58 -8
- package/dist/node-types/report.js.map +1 -1
- package/dist/ui/capability-editor.js +184 -15
- package/dist/ui/profile-editor.js +184 -15
- package/dist/ui/swarm-dashboard.js +244 -44
- package/dist/ui/task-detail-view.js +60 -29
- package/dist/ui/use-stream-timeline.d.ts.map +1 -1
- package/dist/ui/use-stream-timeline.js +69 -29
- package/dist/ui/use-stream-timeline.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +1 -1
- package/src/bot/assistant-tools.ts +5 -11
- package/src/bot/capability-registry.ts +196 -18
- package/src/bot/dashboard.ts +3 -3
- package/src/bot/hierarchy-event-log.ts +64 -0
- package/src/bot/operations.ts +7 -0
- package/src/bot/profile-store.ts +39 -0
- package/src/bot/runner.ts +8 -19
- package/src/bot/step-executor.ts +29 -1
- package/src/bot/swarm-controller.ts +62 -5
- package/src/bot/task-store.ts +22 -38
- package/src/bot/task-types.ts +7 -1
- package/src/node-types/bot-report.ts +2 -2
- package/src/node-types/build-context.ts +32 -0
- package/src/node-types/report.ts +56 -8
- package/src/ui/use-stream-timeline.ts +73 -33
|
@@ -14,6 +14,7 @@ import { TaskStore } from './task-store.js';
|
|
|
14
14
|
import { BotRegistry } from './bot-registry.js';
|
|
15
15
|
import type { BotRegistration } from './bot-registry.js';
|
|
16
16
|
import { SwarmEventLog } from './swarm-event-log.js';
|
|
17
|
+
import { HierarchyEventLog } from './hierarchy-event-log.js';
|
|
17
18
|
import { RunStore } from './run-store.js';
|
|
18
19
|
import { EventLog } from './event-log.js';
|
|
19
20
|
import { runRegistry } from './run-registry.js';
|
|
@@ -93,6 +94,7 @@ export class SwarmController {
|
|
|
93
94
|
private readonly statePath: string;
|
|
94
95
|
private readonly taskStore: TaskStore;
|
|
95
96
|
private readonly eventLog: SwarmEventLog;
|
|
97
|
+
private readonly hierarchyEventLog: HierarchyEventLog;
|
|
96
98
|
private readonly orchestrator: Orchestrator;
|
|
97
99
|
private readonly instanceManager: InstanceManager;
|
|
98
100
|
private readonly profileStore: ProfileStore;
|
|
@@ -137,6 +139,7 @@ export class SwarmController {
|
|
|
137
139
|
this.statePath = path.join(this.weaverDir, SWARM_STATE_FILE);
|
|
138
140
|
this.taskStore = new TaskStore(projectDir);
|
|
139
141
|
this.eventLog = new SwarmEventLog(projectDir);
|
|
142
|
+
this.hierarchyEventLog = new HierarchyEventLog(projectDir);
|
|
140
143
|
this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
|
|
141
144
|
this.instanceManager = new InstanceManager();
|
|
142
145
|
this.profileStore = new ProfileStore(projectDir);
|
|
@@ -313,7 +316,7 @@ export class SwarmController {
|
|
|
313
316
|
});
|
|
314
317
|
} else {
|
|
315
318
|
await this.taskStore.update(task.id, {
|
|
316
|
-
status: '
|
|
319
|
+
status: 'open',
|
|
317
320
|
currentBotId: undefined,
|
|
318
321
|
});
|
|
319
322
|
}
|
|
@@ -426,14 +429,22 @@ export class SwarmController {
|
|
|
426
429
|
await this._checkSteering(inst.instanceId);
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
// Route unassigned tasks to orchestrator profile for decomposition
|
|
433
|
+
const unassignedTasks = await this.taskStore.list({ status: ['open', 'pending'] });
|
|
434
|
+
for (const t of unassignedTasks) {
|
|
435
|
+
if (!t.assignedProfile && !t.isParent) {
|
|
436
|
+
await this.taskStore.update(t.id, { assignedProfile: 'orchestrator' });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
429
440
|
// Collect pending tasks (and all tasks for dependency checks)
|
|
430
|
-
const pendingTasks = await this.taskStore.list({ status: ['
|
|
441
|
+
const pendingTasks = await this.taskStore.list({ status: ['open', 'pending'] });
|
|
431
442
|
const allTasks = await this.taskStore.list();
|
|
432
443
|
const routableTasks = pendingTasks.filter((t) => {
|
|
433
444
|
// Skip parent tasks
|
|
434
445
|
if (t.isParent) return false;
|
|
435
446
|
// Skip tasks that have exhausted retries
|
|
436
|
-
if (t.
|
|
447
|
+
if (t.attempt >= t.maxAttempts) return false;
|
|
437
448
|
// Skip tasks over per-task budget
|
|
438
449
|
if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens) return false;
|
|
439
450
|
if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost) return false;
|
|
@@ -503,12 +514,40 @@ export class SwarmController {
|
|
|
503
514
|
}
|
|
504
515
|
}
|
|
505
516
|
|
|
517
|
+
// Build file conflict map: which files are being modified by currently executing tasks
|
|
518
|
+
const activeFiles = new Map<string, string>(); // filepath → taskId
|
|
519
|
+
for (const [instId] of this.executionPromises) {
|
|
520
|
+
const inst = this.instanceManager.listAll().find(i => i.instanceId === instId);
|
|
521
|
+
if (inst?.currentTaskId) {
|
|
522
|
+
const runningTask = await this.taskStore.get(inst.currentTaskId);
|
|
523
|
+
if (runningTask?.context.files) {
|
|
524
|
+
for (const f of runningTask.context.files) {
|
|
525
|
+
activeFiles.set(f, runningTask.id);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
506
531
|
// Apply assignments
|
|
507
532
|
for (const assignment of output.assignments) {
|
|
508
533
|
const profile = profiles.find((p) => p.id === assignment.profileId);
|
|
509
534
|
const bot = this.profileBotMap.get(assignment.profileId);
|
|
510
535
|
if (!profile || !bot) continue;
|
|
511
536
|
|
|
537
|
+
// File conflict check: skip if task targets files currently being modified
|
|
538
|
+
const candidateTask = routableTasks.find(t => t.id === assignment.taskId);
|
|
539
|
+
if (candidateTask?.context.files?.length) {
|
|
540
|
+
const hasFileConflict = candidateTask.context.files.some(f => activeFiles.has(f));
|
|
541
|
+
if (hasFileConflict) {
|
|
542
|
+
this.eventLog.emit({
|
|
543
|
+
type: 'task-skipped',
|
|
544
|
+
timestamp: Date.now(),
|
|
545
|
+
data: { taskId: assignment.taskId, reason: 'file-conflict' },
|
|
546
|
+
});
|
|
547
|
+
continue; // Skip this assignment, retry next cycle
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
512
551
|
try {
|
|
513
552
|
// Assign task in TaskStore
|
|
514
553
|
await this.taskStore.assignToInstance(assignment.taskId, assignment.instanceId, assignment.profileId);
|
|
@@ -668,7 +707,7 @@ export class SwarmController {
|
|
|
668
707
|
};
|
|
669
708
|
|
|
670
709
|
// Release task
|
|
671
|
-
const releaseStatus = result.success ? 'done' : '
|
|
710
|
+
const releaseStatus = result.success ? 'done' : 'open';
|
|
672
711
|
await this.taskStore.release(taskId, releaseStatus, runSummary);
|
|
673
712
|
|
|
674
713
|
// Record token usage
|
|
@@ -686,13 +725,31 @@ export class SwarmController {
|
|
|
686
725
|
this.instanceManager.markIdle(instanceId, result.success);
|
|
687
726
|
} catch { /* instance may have been stopped */ }
|
|
688
727
|
|
|
689
|
-
// Emit task event
|
|
728
|
+
// Emit swarm-level task event
|
|
690
729
|
this.eventLog.emit({
|
|
691
730
|
type: result.success ? 'task-done' : 'task-failed',
|
|
692
731
|
timestamp: Date.now(),
|
|
693
732
|
data: { botId: instanceId, taskId, outcome: runSummary.outcome },
|
|
694
733
|
});
|
|
695
734
|
|
|
735
|
+
// Emit hierarchy-scoped event so sibling tasks can see what happened
|
|
736
|
+
try {
|
|
737
|
+
const task = await this.taskStore.get(taskId);
|
|
738
|
+
if (task?.parentId) {
|
|
739
|
+
this.hierarchyEventLog.emit({
|
|
740
|
+
parentId: task.parentId,
|
|
741
|
+
type: result.success ? 'task-completed' : 'task-run-failed',
|
|
742
|
+
taskId,
|
|
743
|
+
data: {
|
|
744
|
+
summary: runSummary.summary,
|
|
745
|
+
filesModified: runSummary.filesModified,
|
|
746
|
+
botId: instanceId,
|
|
747
|
+
attempt: task.attempt,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
} catch { /* non-fatal — hierarchy events are best-effort */ }
|
|
752
|
+
|
|
696
753
|
this._syncInstancesState();
|
|
697
754
|
this._persist();
|
|
698
755
|
}
|
package/src/bot/task-store.ts
CHANGED
|
@@ -38,7 +38,7 @@ export class TaskStore {
|
|
|
38
38
|
id: parentId,
|
|
39
39
|
title: input.title,
|
|
40
40
|
description: input.description,
|
|
41
|
-
status: '
|
|
41
|
+
status: 'open',
|
|
42
42
|
priority: input.priority ?? 0,
|
|
43
43
|
isParent: true,
|
|
44
44
|
parentId: input.parentId,
|
|
@@ -77,7 +77,7 @@ export class TaskStore {
|
|
|
77
77
|
id: subId,
|
|
78
78
|
title: sub.title,
|
|
79
79
|
description: sub.description ?? '',
|
|
80
|
-
status: '
|
|
80
|
+
status: 'open',
|
|
81
81
|
priority: sub.priority ?? input.priority ?? 0,
|
|
82
82
|
isParent: false,
|
|
83
83
|
parentId: parentId,
|
|
@@ -115,7 +115,7 @@ export class TaskStore {
|
|
|
115
115
|
id: crypto.randomUUID().slice(0, 8),
|
|
116
116
|
title: input.title,
|
|
117
117
|
description: input.description,
|
|
118
|
-
status: '
|
|
118
|
+
status: 'open',
|
|
119
119
|
priority: input.priority ?? 0,
|
|
120
120
|
isParent: false,
|
|
121
121
|
parentId: input.parentId,
|
|
@@ -233,8 +233,8 @@ export class TaskStore {
|
|
|
233
233
|
|
|
234
234
|
const task = tasks[idx];
|
|
235
235
|
const assignable =
|
|
236
|
-
task.status === 'pending'
|
|
237
|
-
|
|
236
|
+
(task.status === 'open' || task.status === 'pending') &&
|
|
237
|
+
task.attempt < task.maxAttempts;
|
|
238
238
|
if (!assignable) {
|
|
239
239
|
throw new Error(`Task ${taskId} is not assignable (status: ${task.status}, attempt: ${task.attempt}/${task.maxAttempts})`);
|
|
240
240
|
}
|
|
@@ -254,7 +254,7 @@ export class TaskStore {
|
|
|
254
254
|
// Release
|
|
255
255
|
// ---------------------------------------------------------------------------
|
|
256
256
|
|
|
257
|
-
async release(taskId: string, status: 'done' | 'failed', runSummary: RunSummary): Promise<Task> {
|
|
257
|
+
async release(taskId: string, status: 'done' | 'failed' | 'open', runSummary: RunSummary): Promise<Task> {
|
|
258
258
|
return this.mutex.runExclusive(async () => {
|
|
259
259
|
const tasks = this._readAll();
|
|
260
260
|
const idx = tasks.findIndex(t => t.id === taskId);
|
|
@@ -279,7 +279,8 @@ export class TaskStore {
|
|
|
279
279
|
|
|
280
280
|
// Update execution tracking
|
|
281
281
|
task.attempt += 1;
|
|
282
|
-
|
|
282
|
+
// Tasks don't fail — runs fail. Map 'failed' to 'open' for backward compat.
|
|
283
|
+
task.status = status === 'failed' ? 'open' : status;
|
|
283
284
|
task.currentBotId = undefined;
|
|
284
285
|
task.currentRunId = undefined;
|
|
285
286
|
task.updatedAt = new Date().toISOString();
|
|
@@ -287,9 +288,7 @@ export class TaskStore {
|
|
|
287
288
|
if (status === 'done') {
|
|
288
289
|
task.completedAt = new Date().toISOString();
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
-
task.completedAt = new Date().toISOString();
|
|
292
|
-
}
|
|
291
|
+
// 'open' and 'failed' → task stays open, no completedAt
|
|
293
292
|
|
|
294
293
|
tasks[idx] = task;
|
|
295
294
|
|
|
@@ -308,28 +307,19 @@ export class TaskStore {
|
|
|
308
307
|
|
|
309
308
|
private _handleDependencyEffects(tasks: Task[], changedTask: Task): void {
|
|
310
309
|
if (changedTask.status === 'done') {
|
|
311
|
-
// Unblock dependents: set blocked tasks to
|
|
310
|
+
// Unblock dependents: set blocked tasks to open if all deps are now done
|
|
312
311
|
const doneIds = new Set(tasks.filter(t => t.status === 'done').map(t => t.id));
|
|
313
312
|
for (const t of tasks) {
|
|
314
313
|
if (t.status === 'blocked' && t.dependsOn.includes(changedTask.id)) {
|
|
315
314
|
if (t.dependsOn.every(depId => doneIds.has(depId))) {
|
|
316
|
-
t.status = '
|
|
317
|
-
t.updatedAt = new Date().toISOString();
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
} else if (changedTask.status === 'failed') {
|
|
322
|
-
// Check if failure is terminal (no retries left)
|
|
323
|
-
if (changedTask.attempt >= changedTask.maxAttempts) {
|
|
324
|
-
// Block dependents
|
|
325
|
-
for (const t of tasks) {
|
|
326
|
-
if (t.status === 'pending' && t.dependsOn.includes(changedTask.id)) {
|
|
327
|
-
t.status = 'blocked';
|
|
315
|
+
t.status = 'open';
|
|
328
316
|
t.updatedAt = new Date().toISOString();
|
|
329
317
|
}
|
|
330
318
|
}
|
|
331
319
|
}
|
|
332
320
|
}
|
|
321
|
+
// Tasks don't fail — runs fail. Dependents stay blocked until dep
|
|
322
|
+
// succeeds (status=done). No terminal 'failed' state to handle.
|
|
333
323
|
}
|
|
334
324
|
|
|
335
325
|
private _handleParentEffects(tasks: Task[], changedTask: Task): void {
|
|
@@ -351,14 +341,8 @@ export class TaskStore {
|
|
|
351
341
|
return;
|
|
352
342
|
}
|
|
353
343
|
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
s => s.status === 'failed' && s.attempt >= s.maxAttempts,
|
|
357
|
-
);
|
|
358
|
-
if (hasTerminalFailure) {
|
|
359
|
-
parent.status = 'failed';
|
|
360
|
-
parent.updatedAt = new Date().toISOString();
|
|
361
|
-
}
|
|
344
|
+
// Tasks don't fail. Parent stays open until all subtasks are done
|
|
345
|
+
// or user cancels. No terminal 'failed' state for parent.
|
|
362
346
|
}
|
|
363
347
|
|
|
364
348
|
// ---------------------------------------------------------------------------
|
|
@@ -369,7 +353,7 @@ export class TaskStore {
|
|
|
369
353
|
return this.mutex.runExclusive(async () => {
|
|
370
354
|
const tasks = this._readAll();
|
|
371
355
|
const before = tasks.length;
|
|
372
|
-
const kept = tasks.filter(t => t.status !== 'done' && t.status !== '
|
|
356
|
+
const kept = tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled');
|
|
373
357
|
this._writeAll(kept);
|
|
374
358
|
return before - kept.length;
|
|
375
359
|
});
|
|
@@ -389,17 +373,17 @@ export class TaskStore {
|
|
|
389
373
|
// ---------------------------------------------------------------------------
|
|
390
374
|
|
|
391
375
|
private _findDuplicate(tasks: Task[], title: string, description: string): Task | null {
|
|
392
|
-
// Check pending duplicates
|
|
393
|
-
const
|
|
394
|
-
t => t.status === 'pending' && t.title === title && t.description === description,
|
|
376
|
+
// Check open/pending duplicates
|
|
377
|
+
const openDup = tasks.find(
|
|
378
|
+
t => (t.status === 'open' || t.status === 'pending') && t.title === title && t.description === description,
|
|
395
379
|
);
|
|
396
|
-
if (
|
|
380
|
+
if (openDup) return openDup;
|
|
397
381
|
|
|
398
|
-
// Check recently completed
|
|
382
|
+
// Check recently completed (within dedup window)
|
|
399
383
|
const now = Date.now();
|
|
400
384
|
const recentDup = tasks.find(
|
|
401
385
|
t =>
|
|
402
|
-
|
|
386
|
+
t.status === 'done' &&
|
|
403
387
|
t.title === title &&
|
|
404
388
|
t.description === description &&
|
|
405
389
|
t.completedAt !== undefined &&
|
package/src/bot/task-types.ts
CHANGED
|
@@ -22,7 +22,13 @@ export interface TaskContext {
|
|
|
22
22
|
lastError?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Task status. Tasks NEVER go to 'failed' — runs fail, tasks stay open.
|
|
27
|
+
* 'open' replaces 'pending'. A task is open until done or cancelled.
|
|
28
|
+
*/
|
|
29
|
+
export type TaskStatus = 'open' | 'pending' | 'in-progress' | 'blocked' | 'done' | 'cancelled';
|
|
30
|
+
// NOTE: 'pending' kept for backward compatibility with existing data.
|
|
31
|
+
// New code should use 'open'. Both are treated as assignable.
|
|
26
32
|
|
|
27
33
|
export interface Task {
|
|
28
34
|
id: string;
|
|
@@ -120,8 +120,8 @@ export async function weaverBotReport(
|
|
|
120
120
|
if (task.queueId && context.env?.projectDir) {
|
|
121
121
|
try {
|
|
122
122
|
const store = new TaskStore(context.env.projectDir);
|
|
123
|
-
await store.update(task.queueId, { status: success ? 'done' : '
|
|
124
|
-
console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : '
|
|
123
|
+
await store.update(task.queueId, { status: success ? 'done' : 'open' });
|
|
124
|
+
console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : 'open'}\x1b[0m`);
|
|
125
125
|
} catch (err) { if (process.env.WEAVER_VERBOSE) console.error('[bot-report] queue update failed:', err); }
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import type { WeaverContext } from '../bot/types.js';
|
|
5
|
+
import { HierarchyEventLog } from '../bot/hierarchy-event-log.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Builds the knowledge bundle the AI needs for planning.
|
|
@@ -31,6 +32,37 @@ export function weaverBuildContext(ctx: string): { ctx: string } {
|
|
|
31
32
|
sections.push(...buildFullContext(projectDir, task.mode));
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// Auto-recall project memory (conventions persisted by previous bots)
|
|
36
|
+
try {
|
|
37
|
+
const memPath = path.join(projectDir, '.weaver', 'project-memory.json');
|
|
38
|
+
if (fs.existsSync(memPath)) {
|
|
39
|
+
const memory = JSON.parse(fs.readFileSync(memPath, 'utf-8')) as Record<string, string>;
|
|
40
|
+
const entries = Object.entries(memory);
|
|
41
|
+
if (entries.length > 0) {
|
|
42
|
+
const memLines = entries.map(([key, value]) => `- **${key}**: ${value}`);
|
|
43
|
+
sections.push(`## Project Conventions (from memory)\n\nFollow these established patterns:\n${memLines.join('\n')}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch { /* non-fatal — memory is best-effort */ }
|
|
47
|
+
|
|
48
|
+
// Include sibling context from hierarchy events (previous task completions in same hierarchy)
|
|
49
|
+
try {
|
|
50
|
+
const parsedTask = JSON.parse(context.taskJson!) as { parentId?: string };
|
|
51
|
+
const parentId = parsedTask.parentId;
|
|
52
|
+
if (parentId) {
|
|
53
|
+
const hierarchyLog = new HierarchyEventLog(projectDir);
|
|
54
|
+
const siblingEvents = hierarchyLog.tailByParent(parentId);
|
|
55
|
+
if (siblingEvents.length > 0) {
|
|
56
|
+
const siblingLines = siblingEvents.map(e => {
|
|
57
|
+
const d = e.data ?? {};
|
|
58
|
+
const files = (d.filesModified as string[])?.join(', ') || 'none';
|
|
59
|
+
return `- ${e.type}: ${d.summary ?? e.taskId} (files: ${files})`;
|
|
60
|
+
});
|
|
61
|
+
sections.push(`## Previous Task Completions\n\nYour sibling tasks in this hierarchy have completed:\n${siblingLines.join('\n')}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch { /* non-fatal — sibling context is best-effort */ }
|
|
65
|
+
|
|
34
66
|
const bundle = sections.join('\n\n---\n\n');
|
|
35
67
|
// Output handled by session renderer; keep a dim line for debugging
|
|
36
68
|
if (process.env.WEAVER_VERBOSE) process.stderr.write(`\x1b[2m Context: ${bundle.length} chars\x1b[0m\n`);
|
package/src/node-types/report.ts
CHANGED
|
@@ -10,18 +10,66 @@ import type { WeaverContext } from '../bot/types.js';
|
|
|
10
10
|
* @icon summarize
|
|
11
11
|
* @color cyan
|
|
12
12
|
* @input ctx [order:0] - Weaver context (JSON)
|
|
13
|
-
* @output summary [order:0] - Summary
|
|
13
|
+
* @output summary [order:0, format:md] - Summary
|
|
14
14
|
* @output onFailure [hidden]
|
|
15
15
|
*/
|
|
16
16
|
export function weaverReport(ctx: string): { summary: string } {
|
|
17
17
|
const context = JSON.parse(ctx) as WeaverContext;
|
|
18
18
|
const result = JSON.parse(context.resultJson!);
|
|
19
19
|
const relPath = path.relative(context.env.projectDir, context.targetPath!);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
|
|
21
|
+
console.log(`\x1b[32m✓ Weaver: ${result.outcome} (${relPath})\x1b[0m`);
|
|
22
|
+
|
|
23
|
+
const md: string[] = [];
|
|
24
|
+
md.push(`## ${result.outcome === 'success' ? 'Task Completed' : 'Task Failed'}`);
|
|
25
|
+
md.push('');
|
|
26
|
+
if (result.summary) md.push(result.summary);
|
|
27
|
+
md.push('');
|
|
28
|
+
|
|
29
|
+
// Steps
|
|
30
|
+
try {
|
|
31
|
+
const steps = JSON.parse(context.stepLogJson ?? '[]') as Array<{ step: string; status: string; detail?: string }>;
|
|
32
|
+
if (steps.length > 0) {
|
|
33
|
+
md.push('### Steps');
|
|
34
|
+
md.push('');
|
|
35
|
+
for (const s of steps) {
|
|
36
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
37
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
38
|
+
}
|
|
39
|
+
md.push('');
|
|
40
|
+
}
|
|
41
|
+
} catch { /* skip */ }
|
|
42
|
+
|
|
43
|
+
// Files
|
|
44
|
+
try {
|
|
45
|
+
const files = JSON.parse(context.filesModified ?? '[]') as string[];
|
|
46
|
+
if (files.length > 0) {
|
|
47
|
+
md.push('### Files Modified');
|
|
48
|
+
md.push('');
|
|
49
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
50
|
+
md.push('');
|
|
51
|
+
}
|
|
52
|
+
} catch { /* skip */ }
|
|
53
|
+
|
|
54
|
+
// Review
|
|
55
|
+
try {
|
|
56
|
+
const review = JSON.parse(context.reviewJson ?? '{}') as Record<string, string>;
|
|
57
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
58
|
+
md.push('### Review');
|
|
59
|
+
md.push('');
|
|
60
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
61
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
62
|
+
}
|
|
63
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
64
|
+
md.push('');
|
|
65
|
+
}
|
|
66
|
+
} catch { /* skip */ }
|
|
67
|
+
|
|
68
|
+
// Meta
|
|
69
|
+
const meta: string[] = [];
|
|
70
|
+
if (result.executionTime) meta.push(`**Duration:** ${result.executionTime}s`);
|
|
71
|
+
if (context.env.providerInfo?.model) meta.push(`**Model:** ${context.env.providerInfo.model}`);
|
|
72
|
+
if (meta.length > 0) md.push(meta.join(' | '));
|
|
73
|
+
|
|
74
|
+
return { summary: md.join('\n') };
|
|
27
75
|
}
|
|
@@ -76,6 +76,9 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
76
76
|
// Map nodeId → index in entries[] so we can replace start with complete
|
|
77
77
|
const nodeEntryIndex = new Map<string, number>();
|
|
78
78
|
const nodeStarts = new Map<string, number>();
|
|
79
|
+
// Track completed/failed nodes to skip duplicate completion events
|
|
80
|
+
// (FW runtime emits 2x STATUS_CHANGED per node transition)
|
|
81
|
+
const completedNodes = new Set<string>();
|
|
79
82
|
let idCounter = 0;
|
|
80
83
|
|
|
81
84
|
for (const event of events) {
|
|
@@ -95,6 +98,17 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
95
98
|
case 'node-start': {
|
|
96
99
|
const nodeId = d.nodeId as string;
|
|
97
100
|
if (nodeId) nodeStarts.set(nodeId, event.timestamp);
|
|
101
|
+
|
|
102
|
+
// If we already have an in-flight entry for this node (duplicate
|
|
103
|
+
// STATUS_CHANGED from the FW runtime), skip the duplicate.
|
|
104
|
+
const existingStartIdx = nodeEntryIndex.get(nodeId);
|
|
105
|
+
if (existingStartIdx != null && entries[existingStartIdx]?.type === 'node-started') {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Clear completed tracking so retry loops work (node can re-enter)
|
|
110
|
+
completedNodes.delete(nodeId);
|
|
111
|
+
|
|
98
112
|
const idx = entries.length;
|
|
99
113
|
nodeEntryIndex.set(nodeId, idx);
|
|
100
114
|
entries.push({
|
|
@@ -111,65 +125,91 @@ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): Strea
|
|
|
111
125
|
|
|
112
126
|
case 'node-complete': {
|
|
113
127
|
const nodeId = d.nodeId as string;
|
|
128
|
+
// Skip duplicate completion events for the same node
|
|
129
|
+
if (completedNodes.has(nodeId)) break;
|
|
130
|
+
|
|
114
131
|
const startTs = nodeStarts.get(nodeId);
|
|
115
132
|
const duration =
|
|
116
133
|
(d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
|
|
117
134
|
if (nodeId) nodeStarts.delete(nodeId);
|
|
118
|
-
|
|
119
|
-
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
120
|
-
|
|
121
|
-
const completed: TimelineEntry = {
|
|
122
|
-
id: `s-${idCounter++}`,
|
|
123
|
-
timestamp: new Date(startTs ?? event.timestamp),
|
|
124
|
-
type: 'node-completed',
|
|
125
|
-
nodeId,
|
|
126
|
-
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
127
|
-
duration,
|
|
128
|
-
color: d.color as string | undefined,
|
|
129
|
-
icon: d.icon as string | undefined,
|
|
130
|
-
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
131
|
-
};
|
|
135
|
+
completedNodes.add(nodeId);
|
|
132
136
|
|
|
133
137
|
// Replace the node-started entry in-place
|
|
134
138
|
const existingIdx = nodeEntryIndex.get(nodeId);
|
|
135
139
|
if (existingIdx != null && entries[existingIdx]) {
|
|
136
|
-
|
|
140
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
141
|
+
entries[existingIdx] = {
|
|
142
|
+
id: entries[existingIdx]!.id,
|
|
143
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
144
|
+
type: 'node-completed',
|
|
145
|
+
nodeId,
|
|
146
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
147
|
+
duration,
|
|
148
|
+
color: d.color as string | undefined,
|
|
149
|
+
icon: d.icon as string | undefined,
|
|
150
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
151
|
+
};
|
|
137
152
|
nodeEntryIndex.delete(nodeId);
|
|
138
153
|
} else {
|
|
139
|
-
|
|
154
|
+
// No matching start entry — standalone complete (e.g. Start node)
|
|
155
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
156
|
+
entries.push({
|
|
157
|
+
id: `s-${idCounter++}`,
|
|
158
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
159
|
+
type: 'node-completed',
|
|
160
|
+
nodeId,
|
|
161
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
162
|
+
duration,
|
|
163
|
+
color: d.color as string | undefined,
|
|
164
|
+
icon: d.icon as string | undefined,
|
|
165
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
166
|
+
});
|
|
140
167
|
}
|
|
141
168
|
break;
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
case 'node-error': {
|
|
145
172
|
const nodeId = d.nodeId as string;
|
|
173
|
+
if (completedNodes.has(nodeId)) break; // skip duplicate
|
|
174
|
+
|
|
146
175
|
const startTs = nodeStarts.get(nodeId);
|
|
147
176
|
const duration =
|
|
148
177
|
(d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
|
|
149
178
|
if (nodeId) nodeStarts.delete(nodeId);
|
|
150
|
-
|
|
151
|
-
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
152
|
-
|
|
153
|
-
const failed: TimelineEntry = {
|
|
154
|
-
id: `s-${idCounter++}`,
|
|
155
|
-
timestamp: new Date(startTs ?? event.timestamp),
|
|
156
|
-
type: 'node-failed',
|
|
157
|
-
nodeId,
|
|
158
|
-
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
159
|
-
detail: d.error as string | undefined,
|
|
160
|
-
duration,
|
|
161
|
-
color: d.color as string | undefined,
|
|
162
|
-
icon: d.icon as string | undefined,
|
|
163
|
-
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
164
|
-
};
|
|
179
|
+
completedNodes.add(nodeId);
|
|
165
180
|
|
|
166
181
|
// Replace the node-started entry in-place
|
|
167
182
|
const existingIdx = nodeEntryIndex.get(nodeId);
|
|
168
183
|
if (existingIdx != null && entries[existingIdx]) {
|
|
169
|
-
|
|
184
|
+
|
|
185
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
186
|
+
entries[existingIdx] = {
|
|
187
|
+
id: entries[existingIdx]!.id,
|
|
188
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
189
|
+
type: 'node-failed',
|
|
190
|
+
nodeId,
|
|
191
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
192
|
+
detail: d.error as string | undefined,
|
|
193
|
+
duration,
|
|
194
|
+
color: d.color as string | undefined,
|
|
195
|
+
icon: d.icon as string | undefined,
|
|
196
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
197
|
+
};
|
|
170
198
|
nodeEntryIndex.delete(nodeId);
|
|
171
199
|
} else {
|
|
172
|
-
|
|
200
|
+
const rawOutputs = d.outputs as Array<{ portLabel: string; value: unknown }> | undefined;
|
|
201
|
+
entries.push({
|
|
202
|
+
id: `s-${idCounter++}`,
|
|
203
|
+
timestamp: new Date(startTs ?? event.timestamp),
|
|
204
|
+
type: 'node-failed',
|
|
205
|
+
nodeId,
|
|
206
|
+
label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
|
|
207
|
+
detail: d.error as string | undefined,
|
|
208
|
+
duration,
|
|
209
|
+
color: d.color as string | undefined,
|
|
210
|
+
icon: d.icon as string | undefined,
|
|
211
|
+
outputs: rawOutputs && rawOutputs.length > 0 ? rawOutputs : undefined,
|
|
212
|
+
});
|
|
173
213
|
}
|
|
174
214
|
break;
|
|
175
215
|
}
|