@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.
Files changed (85) hide show
  1. package/dist/bot/assistant-tools.d.ts.map +1 -1
  2. package/dist/bot/assistant-tools.js +5 -11
  3. package/dist/bot/assistant-tools.js.map +1 -1
  4. package/dist/bot/behavior-defaults.d.ts +2 -2
  5. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  6. package/dist/bot/behavior-defaults.js +11 -6
  7. package/dist/bot/behavior-defaults.js.map +1 -1
  8. package/dist/bot/capability-registry.d.ts +2 -0
  9. package/dist/bot/capability-registry.d.ts.map +1 -1
  10. package/dist/bot/capability-registry.js +282 -15
  11. package/dist/bot/capability-registry.js.map +1 -1
  12. package/dist/bot/dashboard.js +3 -3
  13. package/dist/bot/dashboard.js.map +1 -1
  14. package/dist/bot/hierarchy-event-log.d.ts +37 -0
  15. package/dist/bot/hierarchy-event-log.d.ts.map +1 -0
  16. package/dist/bot/hierarchy-event-log.js +58 -0
  17. package/dist/bot/hierarchy-event-log.js.map +1 -0
  18. package/dist/bot/operations.d.ts +2 -0
  19. package/dist/bot/operations.d.ts.map +1 -1
  20. package/dist/bot/operations.js +5 -0
  21. package/dist/bot/operations.js.map +1 -1
  22. package/dist/bot/profile-store.d.ts.map +1 -1
  23. package/dist/bot/profile-store.js +46 -0
  24. package/dist/bot/profile-store.js.map +1 -1
  25. package/dist/bot/runner.d.ts.map +1 -1
  26. package/dist/bot/runner.js +51 -16
  27. package/dist/bot/runner.js.map +1 -1
  28. package/dist/bot/step-executor.d.ts.map +1 -1
  29. package/dist/bot/step-executor.js +36 -1
  30. package/dist/bot/step-executor.js.map +1 -1
  31. package/dist/bot/swarm-controller.d.ts +1 -0
  32. package/dist/bot/swarm-controller.d.ts.map +1 -1
  33. package/dist/bot/swarm-controller.js +64 -6
  34. package/dist/bot/swarm-controller.js.map +1 -1
  35. package/dist/bot/task-store.d.ts +1 -1
  36. package/dist/bot/task-store.d.ts.map +1 -1
  37. package/dist/bot/task-store.js +21 -36
  38. package/dist/bot/task-store.js.map +1 -1
  39. package/dist/bot/task-types.d.ts +5 -1
  40. package/dist/bot/task-types.d.ts.map +1 -1
  41. package/dist/bot/types.d.ts +4 -0
  42. package/dist/bot/types.d.ts.map +1 -1
  43. package/dist/node-types/bot-report.d.ts +3 -1
  44. package/dist/node-types/bot-report.d.ts.map +1 -1
  45. package/dist/node-types/bot-report.js +62 -11
  46. package/dist/node-types/bot-report.js.map +1 -1
  47. package/dist/node-types/build-context.d.ts.map +1 -1
  48. package/dist/node-types/build-context.js +32 -0
  49. package/dist/node-types/build-context.js.map +1 -1
  50. package/dist/node-types/report.d.ts +2 -0
  51. package/dist/node-types/report.d.ts.map +1 -1
  52. package/dist/node-types/report.js +61 -7
  53. package/dist/node-types/report.js.map +1 -1
  54. package/dist/ui/capability-editor.js +268 -15
  55. package/dist/ui/profile-editor.js +271 -18
  56. package/dist/ui/swarm-dashboard.js +340 -51
  57. package/dist/ui/task-detail-view.js +69 -33
  58. package/dist/ui/trace-to-timeline.d.ts +2 -0
  59. package/dist/ui/trace-to-timeline.d.ts.map +1 -1
  60. package/dist/ui/trace-to-timeline.js.map +1 -1
  61. package/dist/ui/use-stream-timeline.d.ts +2 -0
  62. package/dist/ui/use-stream-timeline.d.ts.map +1 -1
  63. package/dist/ui/use-stream-timeline.js +76 -34
  64. package/dist/ui/use-stream-timeline.js.map +1 -1
  65. package/flowweaver.manifest.json +1 -1
  66. package/package.json +1 -1
  67. package/src/bot/assistant-tools.ts +5 -11
  68. package/src/bot/behavior-defaults.ts +12 -5
  69. package/src/bot/capability-registry.ts +300 -18
  70. package/src/bot/dashboard.ts +3 -3
  71. package/src/bot/hierarchy-event-log.ts +64 -0
  72. package/src/bot/operations.ts +7 -0
  73. package/src/bot/profile-store.ts +46 -0
  74. package/src/bot/runner.ts +51 -16
  75. package/src/bot/step-executor.ts +32 -1
  76. package/src/bot/swarm-controller.ts +67 -6
  77. package/src/bot/task-store.ts +22 -38
  78. package/src/bot/task-types.ts +7 -1
  79. package/src/bot/types.ts +4 -0
  80. package/src/node-types/bot-report.ts +63 -12
  81. package/src/node-types/build-context.ts +32 -0
  82. package/src/node-types/report.ts +60 -8
  83. package/src/ui/task-detail-view.tsx +3 -0
  84. package/src/ui/trace-to-timeline.ts +2 -0
  85. package/src/ui/use-stream-timeline.ts +81 -38
@@ -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: 'failed',
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: ['pending', 'failed'] });
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.status === 'failed' && t.attempt >= t.maxAttempts) return false;
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' : 'failed';
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
  }
@@ -38,7 +38,7 @@ export class TaskStore {
38
38
  id: parentId,
39
39
  title: input.title,
40
40
  description: input.description,
41
- status: 'pending',
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: 'pending',
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: 'pending',
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
- (task.status === 'failed' && task.attempt < task.maxAttempts);
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
- task.status = status;
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
- if (status === 'failed') {
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 pending if all deps are now done
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 = 'pending';
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
- // Any terminal failure => parent failed
355
- const hasTerminalFailure = subtasks.some(
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 !== 'failed' && t.status !== 'cancelled');
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 pendingDup = tasks.find(
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 (pendingDup) return pendingDup;
380
+ if (openDup) return openDup;
397
381
 
398
- // Check recently completed/failed (within dedup window)
382
+ // Check recently completed (within dedup window)
399
383
  const now = Date.now();
400
384
  const recentDup = tasks.find(
401
385
  t =>
402
- (t.status === 'done' || t.status === 'failed') &&
386
+ t.status === 'done' &&
403
387
  t.title === title &&
404
388
  t.description === description &&
405
389
  t.completedAt !== undefined &&
@@ -22,7 +22,13 @@ export interface TaskContext {
22
22
  lastError?: string;
23
23
  }
24
24
 
25
- export type TaskStatus = 'pending' | 'in-progress' | 'blocked' | 'done' | 'failed' | 'cancelled';
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 reportJson [order:1] [hidden] - Full report (JSON)
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 report = { task: {}, path: 'unknown', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
33
- return { onSuccess: true, onFailure: false, summary: '', reportJson: JSON.stringify(report) };
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 report = { task: {}, path: 'no-context', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
37
- return { onSuccess: false, onFailure: true, summary: 'No context received', reportJson: JSON.stringify(report) };
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 report = { task: {}, path: 'parse-error', result: null, filesModified: [], gitResult: null, timestamp: Date.now() };
45
- return { onSuccess: false, onFailure: true, summary: 'Failed to parse context', reportJson: JSON.stringify(report) };
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' : 'failed' });
124
- console.log(`\x1b[36m→ Queue task ${task.queueId}: ${success ? 'done' : 'failed'}\x1b[0m`);
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 report = {
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(report) };
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`);
@@ -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
- const lines = [
21
- `Weaver: ${result.outcome} (${relPath})`,
22
- result.summary,
23
- ];
24
- if (result.executionTime) lines.push(`Time: ${result.executionTime}s`);
25
- console.log(`\x1b[32m✓ ${lines[0]}\x1b[0m`);
26
- return { summary: lines.join('\n') };
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,