@synergenius/flow-weaver-pack-weaver 0.9.140 → 0.9.144
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/behavior-defaults.d.ts +2 -2
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +11 -6
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts +2 -0
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +282 -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 +46 -0
- package/dist/bot/profile-store.js.map +1 -1
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +51 -16
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +36 -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 +64 -6
- 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/bot/types.d.ts +4 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/node-types/bot-report.d.ts +3 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +62 -11
- 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 +2 -0
- package/dist/node-types/report.d.ts.map +1 -1
- package/dist/node-types/report.js +61 -7
- package/dist/node-types/report.js.map +1 -1
- package/dist/ui/capability-editor.js +268 -15
- package/dist/ui/profile-editor.js +271 -18
- package/dist/ui/swarm-dashboard.js +340 -51
- package/dist/ui/task-detail-view.js +69 -33
- package/dist/ui/trace-to-timeline.d.ts +2 -0
- package/dist/ui/trace-to-timeline.d.ts.map +1 -1
- package/dist/ui/trace-to-timeline.js.map +1 -1
- package/dist/ui/use-stream-timeline.d.ts +2 -0
- package/dist/ui/use-stream-timeline.d.ts.map +1 -1
- package/dist/ui/use-stream-timeline.js +76 -34
- 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/behavior-defaults.ts +12 -5
- package/src/bot/capability-registry.ts +300 -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 +46 -0
- package/src/bot/runner.ts +51 -16
- package/src/bot/step-executor.ts +32 -1
- package/src/bot/swarm-controller.ts +67 -6
- package/src/bot/task-store.ts +22 -38
- package/src/bot/task-types.ts +7 -1
- package/src/bot/types.ts +4 -0
- package/src/node-types/bot-report.ts +63 -12
- package/src/node-types/build-context.ts +32 -0
- package/src/node-types/report.ts +60 -8
- package/src/ui/task-detail-view.tsx +3 -0
- package/src/ui/trace-to-timeline.ts +2 -0
- package/src/ui/use-stream-timeline.ts +81 -38
package/src/bot/step-executor.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
8
|
import { runCommand } from '@synergenius/flow-weaver';
|
|
9
9
|
import { BLOCKED_SHELL_PATTERNS } from './safety.js';
|
|
10
|
-
import { OP_WRITE_FILE, OP_READ_FILE, OP_PATCH_FILE, OP_LIST_FILES, OP_RUN_SHELL, OP_TASK_CREATE, OP_RESPOND, OP_NO_OP, OP_NOOP, OP_DONE, OP_COMPLETE, OP_CREATE_WORKFLOW, OP_MODIFY_SOURCE, OP_IMPLEMENT_NODE, normalizeOperation } from './operations.js';
|
|
10
|
+
import { OP_WRITE_FILE, OP_READ_FILE, OP_PATCH_FILE, OP_LIST_FILES, OP_RUN_SHELL, OP_TASK_CREATE, OP_RESPOND, OP_NO_OP, OP_NOOP, OP_DONE, OP_COMPLETE, OP_CREATE_WORKFLOW, OP_MODIFY_SOURCE, OP_IMPLEMENT_NODE, OP_REMEMBER, OP_RECALL, normalizeOperation } from './operations.js';
|
|
11
11
|
import { TaskStore } from './task-store.js';
|
|
12
12
|
import type { CreateTaskInput } from './task-types.js';
|
|
13
13
|
|
|
@@ -398,6 +398,7 @@ export async function executeStep(
|
|
|
398
398
|
if (!title) return { blocked: true, blockReason: 'task_create requires a "title" argument.' };
|
|
399
399
|
|
|
400
400
|
const store = new TaskStore(projectDir);
|
|
401
|
+
const assignedProfile = args.assignedProfile as string | undefined;
|
|
401
402
|
const input: CreateTaskInput = {
|
|
402
403
|
title,
|
|
403
404
|
description: (args.description as string) ?? title,
|
|
@@ -405,6 +406,7 @@ export async function executeStep(
|
|
|
405
406
|
priority: (args.priority as number) ?? 0,
|
|
406
407
|
parentId: args.parentId as string | undefined,
|
|
407
408
|
dependsOn: (args.dependsOn as string[]) ?? [],
|
|
409
|
+
assignedProfile,
|
|
408
410
|
};
|
|
409
411
|
|
|
410
412
|
// Support inline subtasks
|
|
@@ -415,6 +417,7 @@ export async function executeStep(
|
|
|
415
417
|
description: (s.description as string) ?? '',
|
|
416
418
|
complexity: (s.complexity as 'trivial' | 'simple' | 'moderate' | 'complex') ?? 'simple',
|
|
417
419
|
priority: (s.priority as number) ?? 0,
|
|
420
|
+
assignedProfile: (s.assignedProfile as string) ?? assignedProfile ?? 'developer',
|
|
418
421
|
}));
|
|
419
422
|
}
|
|
420
423
|
|
|
@@ -424,6 +427,34 @@ export async function executeStep(
|
|
|
424
427
|
return { output: `Created task "${title}" (${task.id})${subtasks.length ? ` with ${subtasks.length} subtasks` : ''}. IDs: ${ids.join(', ')}` };
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
// -----------------------------------------------------------------
|
|
431
|
+
// Memory operations — persist/recall project conventions
|
|
432
|
+
// -----------------------------------------------------------------
|
|
433
|
+
case OP_REMEMBER: {
|
|
434
|
+
const key = args.key as string | undefined;
|
|
435
|
+
if (!key) return { blocked: true, blockReason: 'remember requires a "key" argument.' };
|
|
436
|
+
const value = (args.value as string) ?? '';
|
|
437
|
+
const memDir = path.join(projectDir, '.weaver');
|
|
438
|
+
if (!fs.existsSync(memDir)) fs.mkdirSync(memDir, { recursive: true });
|
|
439
|
+
const memPath = path.join(memDir, 'project-memory.json');
|
|
440
|
+
let memory: Record<string, string> = {};
|
|
441
|
+
try {
|
|
442
|
+
if (fs.existsSync(memPath)) memory = JSON.parse(fs.readFileSync(memPath, 'utf-8'));
|
|
443
|
+
} catch { /* start fresh */ }
|
|
444
|
+
memory[key] = value;
|
|
445
|
+
fs.writeFileSync(memPath, JSON.stringify(memory, null, 2), 'utf-8');
|
|
446
|
+
return { output: `Remembered: ${key} = ${value}` };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
case OP_RECALL: {
|
|
450
|
+
const memPath = path.join(projectDir, '.weaver', 'project-memory.json');
|
|
451
|
+
let memory: Record<string, string> = {};
|
|
452
|
+
try {
|
|
453
|
+
if (fs.existsSync(memPath)) memory = JSON.parse(fs.readFileSync(memPath, 'utf-8'));
|
|
454
|
+
} catch { /* no memory yet */ }
|
|
455
|
+
return { output: JSON.stringify(memory) };
|
|
456
|
+
}
|
|
457
|
+
|
|
427
458
|
// -----------------------------------------------------------------
|
|
428
459
|
// Passthrough operations
|
|
429
460
|
// -----------------------------------------------------------------
|
|
@@ -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,26 @@ export class SwarmController {
|
|
|
426
429
|
await this._checkSteering(inst.instanceId);
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
// Route unassigned tasks: top-level → orchestrator, subtasks → developer (fallback)
|
|
433
|
+
const unassignedTasks = await this.taskStore.list({ status: ['open', 'pending'] });
|
|
434
|
+
for (const t of unassignedTasks) {
|
|
435
|
+
if (!t.assignedProfile && !t.isParent) {
|
|
436
|
+
// Top-level tasks (no parent) go to orchestrator for decomposition.
|
|
437
|
+
// Subtasks without a profile default to developer — the orchestrator
|
|
438
|
+
// should have set assignedProfile, but if it didn't, assume code work.
|
|
439
|
+
const defaultProfile = t.parentId ? 'developer' : 'orchestrator';
|
|
440
|
+
await this.taskStore.update(t.id, { assignedProfile: defaultProfile });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
429
444
|
// Collect pending tasks (and all tasks for dependency checks)
|
|
430
|
-
const pendingTasks = await this.taskStore.list({ status: ['
|
|
445
|
+
const pendingTasks = await this.taskStore.list({ status: ['open', 'pending'] });
|
|
431
446
|
const allTasks = await this.taskStore.list();
|
|
432
447
|
const routableTasks = pendingTasks.filter((t) => {
|
|
433
448
|
// Skip parent tasks
|
|
434
449
|
if (t.isParent) return false;
|
|
435
450
|
// Skip tasks that have exhausted retries
|
|
436
|
-
if (t.
|
|
451
|
+
if (t.attempt >= t.maxAttempts) return false;
|
|
437
452
|
// Skip tasks over per-task budget
|
|
438
453
|
if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens) return false;
|
|
439
454
|
if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost) return false;
|
|
@@ -503,12 +518,40 @@ export class SwarmController {
|
|
|
503
518
|
}
|
|
504
519
|
}
|
|
505
520
|
|
|
521
|
+
// Build file conflict map: which files are being modified by currently executing tasks
|
|
522
|
+
const activeFiles = new Map<string, string>(); // filepath → taskId
|
|
523
|
+
for (const [instId] of this.executionPromises) {
|
|
524
|
+
const inst = this.instanceManager.listAll().find(i => i.instanceId === instId);
|
|
525
|
+
if (inst?.currentTaskId) {
|
|
526
|
+
const runningTask = await this.taskStore.get(inst.currentTaskId);
|
|
527
|
+
if (runningTask?.context.files) {
|
|
528
|
+
for (const f of runningTask.context.files) {
|
|
529
|
+
activeFiles.set(f, runningTask.id);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
506
535
|
// Apply assignments
|
|
507
536
|
for (const assignment of output.assignments) {
|
|
508
537
|
const profile = profiles.find((p) => p.id === assignment.profileId);
|
|
509
538
|
const bot = this.profileBotMap.get(assignment.profileId);
|
|
510
539
|
if (!profile || !bot) continue;
|
|
511
540
|
|
|
541
|
+
// File conflict check: skip if task targets files currently being modified
|
|
542
|
+
const candidateTask = routableTasks.find(t => t.id === assignment.taskId);
|
|
543
|
+
if (candidateTask?.context.files?.length) {
|
|
544
|
+
const hasFileConflict = candidateTask.context.files.some(f => activeFiles.has(f));
|
|
545
|
+
if (hasFileConflict) {
|
|
546
|
+
this.eventLog.emit({
|
|
547
|
+
type: 'task-skipped',
|
|
548
|
+
timestamp: Date.now(),
|
|
549
|
+
data: { taskId: assignment.taskId, reason: 'file-conflict' },
|
|
550
|
+
});
|
|
551
|
+
continue; // Skip this assignment, retry next cycle
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
512
555
|
try {
|
|
513
556
|
// Assign task in TaskStore
|
|
514
557
|
await this.taskStore.assignToInstance(assignment.taskId, assignment.instanceId, assignment.profileId);
|
|
@@ -613,7 +656,7 @@ export class SwarmController {
|
|
|
613
656
|
// Build behavior config from profile preferences, adjusted for task complexity.
|
|
614
657
|
// Trivial tasks get cheaper models and fewer retries; complex tasks are unchanged.
|
|
615
658
|
const baseBehavior: ProfileBehavior = profile.preferences.behavior
|
|
616
|
-
?? buildDefaultBehavior(profile.preferences.costStrategy);
|
|
659
|
+
?? buildDefaultBehavior(profile.preferences.costStrategy, undefined, profile.id);
|
|
617
660
|
const behavior = adjustBehaviorForComplexity(baseBehavior, task.complexity);
|
|
618
661
|
const behaviorJson = JSON.stringify(behavior);
|
|
619
662
|
|
|
@@ -668,7 +711,7 @@ export class SwarmController {
|
|
|
668
711
|
};
|
|
669
712
|
|
|
670
713
|
// Release task
|
|
671
|
-
const releaseStatus = result.success ? 'done' : '
|
|
714
|
+
const releaseStatus = result.success ? 'done' : 'open';
|
|
672
715
|
await this.taskStore.release(taskId, releaseStatus, runSummary);
|
|
673
716
|
|
|
674
717
|
// Record token usage
|
|
@@ -686,13 +729,31 @@ export class SwarmController {
|
|
|
686
729
|
this.instanceManager.markIdle(instanceId, result.success);
|
|
687
730
|
} catch { /* instance may have been stopped */ }
|
|
688
731
|
|
|
689
|
-
// Emit task event
|
|
732
|
+
// Emit swarm-level task event
|
|
690
733
|
this.eventLog.emit({
|
|
691
734
|
type: result.success ? 'task-done' : 'task-failed',
|
|
692
735
|
timestamp: Date.now(),
|
|
693
736
|
data: { botId: instanceId, taskId, outcome: runSummary.outcome },
|
|
694
737
|
});
|
|
695
738
|
|
|
739
|
+
// Emit hierarchy-scoped event so sibling tasks can see what happened
|
|
740
|
+
try {
|
|
741
|
+
const task = await this.taskStore.get(taskId);
|
|
742
|
+
if (task?.parentId) {
|
|
743
|
+
this.hierarchyEventLog.emit({
|
|
744
|
+
parentId: task.parentId,
|
|
745
|
+
type: result.success ? 'task-completed' : 'task-run-failed',
|
|
746
|
+
taskId,
|
|
747
|
+
data: {
|
|
748
|
+
summary: runSummary.summary,
|
|
749
|
+
filesModified: runSummary.filesModified,
|
|
750
|
+
botId: instanceId,
|
|
751
|
+
attempt: task.attempt,
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
} catch { /* non-fatal — hierarchy events are best-effort */ }
|
|
756
|
+
|
|
696
757
|
this._syncInstancesState();
|
|
697
758
|
this._persist();
|
|
698
759
|
}
|
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;
|
package/src/bot/types.ts
CHANGED
|
@@ -167,6 +167,8 @@ export interface WorkflowResult {
|
|
|
167
167
|
success: boolean;
|
|
168
168
|
summary: string;
|
|
169
169
|
outcome: string;
|
|
170
|
+
/** Optional markdown report for rich UI display. */
|
|
171
|
+
report?: string;
|
|
170
172
|
functionName?: string;
|
|
171
173
|
executionTime?: number;
|
|
172
174
|
cost?: RunCostSummary;
|
|
@@ -191,6 +193,8 @@ export interface RunRecord {
|
|
|
191
193
|
success: boolean;
|
|
192
194
|
outcome: RunOutcome;
|
|
193
195
|
summary: string;
|
|
196
|
+
/** Optional markdown report for rich UI display. */
|
|
197
|
+
report?: string;
|
|
194
198
|
functionName?: string;
|
|
195
199
|
executionTime?: number;
|
|
196
200
|
params?: Record<string, unknown>;
|
|
@@ -16,7 +16,8 @@ import { TaskStore } from '../bot/task-store.js';
|
|
|
16
16
|
* @input [abortCtx] [order:2] - Context from abort path (JSON, optional)
|
|
17
17
|
* @input [failCtx] [order:3] - Context from failure paths (exec/plan fail, JSON, optional)
|
|
18
18
|
* @output summary [order:0] - Summary text
|
|
19
|
-
* @output
|
|
19
|
+
* @output report [order:1] - Markdown report
|
|
20
|
+
* @output reportJson [order:2] [hidden] - Full report (JSON)
|
|
20
21
|
* @output onFailure [hidden]
|
|
21
22
|
*/
|
|
22
23
|
export async function weaverBotReport(
|
|
@@ -25,24 +26,24 @@ export async function weaverBotReport(
|
|
|
25
26
|
readCtx?: string,
|
|
26
27
|
abortCtx?: string,
|
|
27
28
|
failCtx?: string,
|
|
28
|
-
): Promise<{ onSuccess: boolean; onFailure: boolean; summary: string; reportJson: string }> {
|
|
29
|
+
): Promise<{ onSuccess: boolean; onFailure: boolean; summary: string; report: string; reportJson: string }> {
|
|
29
30
|
const ctxStr = mainCtx ?? readCtx ?? abortCtx ?? failCtx;
|
|
30
31
|
|
|
31
32
|
if (!execute) {
|
|
32
|
-
const
|
|
33
|
-
return { onSuccess: true, onFailure: false, summary: '', reportJson: JSON.stringify(
|
|
33
|
+
const reportObj = { task: {}, path: 'unknown', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
|
|
34
|
+
return { onSuccess: true, onFailure: false, summary: '', report: '', reportJson: JSON.stringify(reportObj) };
|
|
34
35
|
}
|
|
35
36
|
if (!ctxStr) {
|
|
36
|
-
const
|
|
37
|
-
return { onSuccess: false, onFailure: true, summary: 'No context received', reportJson: JSON.stringify(
|
|
37
|
+
const reportObj = { task: {}, path: 'no-context', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
|
|
38
|
+
return { onSuccess: false, onFailure: true, summary: 'No context received', report: '', reportJson: JSON.stringify(reportObj) };
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
let context: WeaverContext;
|
|
41
42
|
try {
|
|
42
43
|
context = JSON.parse(ctxStr) as WeaverContext;
|
|
43
44
|
} catch {
|
|
44
|
-
const
|
|
45
|
-
return { onSuccess: false, onFailure: true, summary: 'Failed to parse context', reportJson: JSON.stringify(
|
|
45
|
+
const reportObj = { task: {}, path: 'parse-error', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
|
|
46
|
+
return { onSuccess: false, onFailure: true, summary: 'Failed to parse context', report: '', reportJson: JSON.stringify(reportObj) };
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
const safeJsonParse = <T>(str: string | undefined, fallback: T): T => {
|
|
@@ -120,12 +121,12 @@ export async function weaverBotReport(
|
|
|
120
121
|
if (task.queueId && context.env?.projectDir) {
|
|
121
122
|
try {
|
|
122
123
|
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' : '
|
|
124
|
+
await store.update(task.queueId, { status: success ? 'done' : 'open' });
|
|
125
|
+
console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : 'open'}\x1b[0m`);
|
|
125
126
|
} catch (err) { if (process.env.WEAVER_VERBOSE) console.error('[bot-report] queue update failed:', err); }
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
const
|
|
129
|
+
const reportObj = {
|
|
129
130
|
task,
|
|
130
131
|
path: pathName,
|
|
131
132
|
result,
|
|
@@ -134,7 +135,57 @@ export async function weaverBotReport(
|
|
|
134
135
|
timestamp: Date.now(),
|
|
135
136
|
};
|
|
136
137
|
|
|
138
|
+
// Build markdown report
|
|
139
|
+
const md: string[] = [];
|
|
140
|
+
md.push(`## ${success ? 'Task Completed' : 'Task Failed'}`);
|
|
141
|
+
md.push('');
|
|
142
|
+
if (result?.summary) md.push(result.summary);
|
|
143
|
+
if (!reviewPassed && reviewReason) md.push(`\n> Review: ${reviewReason}`);
|
|
144
|
+
md.push('');
|
|
145
|
+
|
|
146
|
+
// Steps
|
|
147
|
+
const stepLog = safeJsonParse<Array<{ step: string; status: string; detail?: string }>>(context.stepLogJson, []);
|
|
148
|
+
if (stepLog.length > 0) {
|
|
149
|
+
md.push('### Steps');
|
|
150
|
+
md.push('');
|
|
151
|
+
for (const s of stepLog) {
|
|
152
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
153
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
154
|
+
}
|
|
155
|
+
md.push('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Files
|
|
159
|
+
if (files.length > 0) {
|
|
160
|
+
md.push('### Files Modified');
|
|
161
|
+
md.push('');
|
|
162
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
163
|
+
md.push('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Review
|
|
167
|
+
if (context.reviewJson) {
|
|
168
|
+
const review = safeJsonParse<Record<string, string>>(context.reviewJson, {});
|
|
169
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
170
|
+
md.push('### Review');
|
|
171
|
+
md.push('');
|
|
172
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
173
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
174
|
+
}
|
|
175
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
176
|
+
md.push('');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Git
|
|
181
|
+
if (gitResult && !gitResult.skipped) {
|
|
182
|
+
md.push('### Git');
|
|
183
|
+
md.push('');
|
|
184
|
+
md.push('Changes committed');
|
|
185
|
+
md.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
137
188
|
console.log(`\n\x1b[1m${success ? '\x1b[32m' : '\x1b[31m'}Bot Report: ${summary}\x1b[0m\n`);
|
|
138
189
|
|
|
139
|
-
return { onSuccess: success, onFailure: !success, summary, reportJson: JSON.stringify(
|
|
190
|
+
return { onSuccess: success, onFailure: !success, summary, report: md.join('\n'), reportJson: JSON.stringify(reportObj) };
|
|
140
191
|
}
|
|
@@ -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
|
@@ -11,17 +11,69 @@ import type { WeaverContext } from '../bot/types.js';
|
|
|
11
11
|
* @color cyan
|
|
12
12
|
* @input ctx [order:0] - Weaver context (JSON)
|
|
13
13
|
* @output summary [order:0] - Summary string
|
|
14
|
+
* @output report [order:1] - Markdown report
|
|
14
15
|
* @output onFailure [hidden]
|
|
15
16
|
*/
|
|
16
|
-
export function weaverReport(ctx: string): { summary: string } {
|
|
17
|
+
export function weaverReport(ctx: string): { summary: string; report: string } {
|
|
17
18
|
const context = JSON.parse(ctx) as WeaverContext;
|
|
18
19
|
const result = JSON.parse(context.resultJson!);
|
|
19
20
|
const relPath = path.relative(context.env.projectDir, context.targetPath!);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
|
|
22
|
+
// Plain one-liner for notifications, task lists, logs
|
|
23
|
+
const summaryLine = `Weaver: ${result.outcome} (${relPath})${result.summary ? ' — ' + result.summary : ''}`;
|
|
24
|
+
console.log(`\x1b[32m✓ ${summaryLine}\x1b[0m`);
|
|
25
|
+
|
|
26
|
+
// Markdown report for rich UI display
|
|
27
|
+
const md: string[] = [];
|
|
28
|
+
md.push(`## ${result.outcome === 'success' ? 'Task Completed' : 'Task Failed'}`);
|
|
29
|
+
md.push('');
|
|
30
|
+
if (result.summary) md.push(result.summary);
|
|
31
|
+
md.push('');
|
|
32
|
+
|
|
33
|
+
// Steps
|
|
34
|
+
try {
|
|
35
|
+
const steps = JSON.parse(context.stepLogJson ?? '[]') as Array<{ step: string; status: string; detail?: string }>;
|
|
36
|
+
if (steps.length > 0) {
|
|
37
|
+
md.push('### Steps');
|
|
38
|
+
md.push('');
|
|
39
|
+
for (const s of steps) {
|
|
40
|
+
const icon = s.status === 'ok' ? '**ok**' : s.status === 'error' ? '**error**' : '**blocked**';
|
|
41
|
+
md.push(`- ${s.step} (${icon})${s.detail ? `: ${s.detail}` : ''}`);
|
|
42
|
+
}
|
|
43
|
+
md.push('');
|
|
44
|
+
}
|
|
45
|
+
} catch { /* skip */ }
|
|
46
|
+
|
|
47
|
+
// Files
|
|
48
|
+
try {
|
|
49
|
+
const files = JSON.parse(context.filesModified ?? '[]') as string[];
|
|
50
|
+
if (files.length > 0) {
|
|
51
|
+
md.push('### Files Modified');
|
|
52
|
+
md.push('');
|
|
53
|
+
for (const f of files) md.push(`- \`${f}\``);
|
|
54
|
+
md.push('');
|
|
55
|
+
}
|
|
56
|
+
} catch { /* skip */ }
|
|
57
|
+
|
|
58
|
+
// Review
|
|
59
|
+
try {
|
|
60
|
+
const review = JSON.parse(context.reviewJson ?? '{}') as Record<string, string>;
|
|
61
|
+
if (review.intent || review.execution || review.result || review.completeness) {
|
|
62
|
+
md.push('### Review');
|
|
63
|
+
md.push('');
|
|
64
|
+
for (const key of ['intent', 'execution', 'result', 'completeness']) {
|
|
65
|
+
if (review[key]) md.push(`- **${key}:** ${review[key]}`);
|
|
66
|
+
}
|
|
67
|
+
if (review.reason) md.push(`\n${review.reason}`);
|
|
68
|
+
md.push('');
|
|
69
|
+
}
|
|
70
|
+
} catch { /* skip */ }
|
|
71
|
+
|
|
72
|
+
// Meta
|
|
73
|
+
const meta: string[] = [];
|
|
74
|
+
if (result.executionTime) meta.push(`**Duration:** ${result.executionTime}s`);
|
|
75
|
+
if (context.env.providerInfo?.model) meta.push(`**Model:** ${context.env.providerInfo.model}`);
|
|
76
|
+
if (meta.length > 0) md.push(meta.join(' | '));
|
|
77
|
+
|
|
78
|
+
return { summary: summaryLine, report: md.join('\n') };
|
|
27
79
|
}
|
|
@@ -281,6 +281,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
281
281
|
cost: liveCost,
|
|
282
282
|
plan,
|
|
283
283
|
awaitingApproval,
|
|
284
|
+
report: liveReport,
|
|
284
285
|
} = useStreamTimeline(stream.events, stream.isDone);
|
|
285
286
|
|
|
286
287
|
const currentRunId = task?.currentRunId;
|
|
@@ -616,6 +617,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
616
617
|
state: isSuccess ? 'completed' : 'failed',
|
|
617
618
|
instruction: extractInstruction(run),
|
|
618
619
|
timeline: runTimeline,
|
|
620
|
+
report: run.report ?? null,
|
|
619
621
|
cost: typeof run.cost === 'number' ? run.cost : ((run.costDetail?.totalCost as number) ?? null),
|
|
620
622
|
plan: run.plan,
|
|
621
623
|
startedAt: run.startedAt,
|
|
@@ -630,6 +632,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
630
632
|
state: 'running',
|
|
631
633
|
instruction: liveInstruction ?? task.title,
|
|
632
634
|
timeline: liveTimeline,
|
|
635
|
+
report: liveReport,
|
|
633
636
|
phase: livePhase,
|
|
634
637
|
elapsed,
|
|
635
638
|
cost: liveCost,
|