@synergenius/flow-weaver-pack-weaver 0.9.159 → 0.9.164

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 (102) hide show
  1. package/dist/ai-chat-provider.d.ts.map +1 -1
  2. package/dist/ai-chat-provider.js +17 -11
  3. package/dist/ai-chat-provider.js.map +1 -1
  4. package/dist/bot/ai-router.js +5 -5
  5. package/dist/bot/ai-router.js.map +1 -1
  6. package/dist/bot/assistant-tools.d.ts.map +1 -1
  7. package/dist/bot/assistant-tools.js +6 -7
  8. package/dist/bot/assistant-tools.js.map +1 -1
  9. package/dist/bot/capability-registry.d.ts.map +1 -1
  10. package/dist/bot/capability-registry.js +37 -14
  11. package/dist/bot/capability-registry.js.map +1 -1
  12. package/dist/bot/dashboard.js +1 -1
  13. package/dist/bot/dashboard.js.map +1 -1
  14. package/dist/bot/index.d.ts +1 -1
  15. package/dist/bot/index.d.ts.map +1 -1
  16. package/dist/bot/index.js.map +1 -1
  17. package/dist/bot/instance-manager.js +3 -3
  18. package/dist/bot/instance-manager.js.map +1 -1
  19. package/dist/bot/profile-store.d.ts.map +1 -1
  20. package/dist/bot/profile-store.js +11 -9
  21. package/dist/bot/profile-store.js.map +1 -1
  22. package/dist/bot/profile-types.d.ts +2 -2
  23. package/dist/bot/profile-types.d.ts.map +1 -1
  24. package/dist/bot/runner.d.ts +1 -0
  25. package/dist/bot/runner.d.ts.map +1 -1
  26. package/dist/bot/runner.js +6 -2
  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 +10 -0
  30. package/dist/bot/step-executor.js.map +1 -1
  31. package/dist/bot/swarm-controller.d.ts +3 -5
  32. package/dist/bot/swarm-controller.d.ts.map +1 -1
  33. package/dist/bot/swarm-controller.js +157 -74
  34. package/dist/bot/swarm-controller.js.map +1 -1
  35. package/dist/bot/task-prompt-builder.d.ts +2 -3
  36. package/dist/bot/task-prompt-builder.d.ts.map +1 -1
  37. package/dist/bot/task-prompt-builder.js +81 -67
  38. package/dist/bot/task-prompt-builder.js.map +1 -1
  39. package/dist/bot/task-store.d.ts +3 -3
  40. package/dist/bot/task-store.d.ts.map +1 -1
  41. package/dist/bot/task-store.js +89 -75
  42. package/dist/bot/task-store.js.map +1 -1
  43. package/dist/bot/task-types.d.ts +54 -26
  44. package/dist/bot/task-types.d.ts.map +1 -1
  45. package/dist/bot/task-types.js +6 -2
  46. package/dist/bot/task-types.js.map +1 -1
  47. package/dist/bot/types.d.ts +2 -0
  48. package/dist/bot/types.d.ts.map +1 -1
  49. package/dist/bot/weaver-tools.d.ts.map +1 -1
  50. package/dist/bot/weaver-tools.js +10 -0
  51. package/dist/bot/weaver-tools.js.map +1 -1
  52. package/dist/cli-handlers.d.ts +0 -1
  53. package/dist/cli-handlers.d.ts.map +1 -1
  54. package/dist/cli-handlers.js +5 -9
  55. package/dist/cli-handlers.js.map +1 -1
  56. package/dist/index.d.ts +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/node-types/agent-execute.d.ts.map +1 -1
  60. package/dist/node-types/agent-execute.js +95 -63
  61. package/dist/node-types/agent-execute.js.map +1 -1
  62. package/dist/node-types/plan-task.js +8 -8
  63. package/dist/node-types/plan-task.js.map +1 -1
  64. package/dist/ui/bot-panel.js +1 -1
  65. package/dist/ui/capability-editor.js +37 -14
  66. package/dist/ui/chat-task-result.js +1 -7
  67. package/dist/ui/profile-editor.js +37 -14
  68. package/dist/ui/swarm-controls.js +2 -2
  69. package/dist/ui/swarm-dashboard.js +72 -109
  70. package/dist/ui/task-detail-view.js +21 -42
  71. package/dist/ui/task-editor.js +13 -50
  72. package/dist/ui/task-pool-list.js +0 -2
  73. package/flowweaver.manifest.json +1 -1
  74. package/package.json +1 -1
  75. package/src/ai-chat-provider.ts +15 -11
  76. package/src/bot/ai-router.ts +5 -5
  77. package/src/bot/assistant-tools.ts +6 -7
  78. package/src/bot/capability-registry.ts +37 -14
  79. package/src/bot/dashboard.ts +1 -1
  80. package/src/bot/index.ts +5 -1
  81. package/src/bot/instance-manager.ts +3 -3
  82. package/src/bot/profile-store.ts +12 -10
  83. package/src/bot/profile-types.ts +2 -2
  84. package/src/bot/runner.ts +6 -2
  85. package/src/bot/step-executor.ts +11 -0
  86. package/src/bot/swarm-controller.ts +164 -78
  87. package/src/bot/task-prompt-builder.ts +86 -71
  88. package/src/bot/task-store.ts +101 -78
  89. package/src/bot/task-types.ts +81 -36
  90. package/src/bot/types.ts +2 -0
  91. package/src/bot/weaver-tools.ts +11 -0
  92. package/src/cli-handlers.ts +5 -9
  93. package/src/index.ts +6 -0
  94. package/src/node-types/agent-execute.ts +99 -62
  95. package/src/node-types/plan-task.ts +8 -8
  96. package/src/ui/bot-panel.tsx +3 -3
  97. package/src/ui/chat-task-result.tsx +5 -14
  98. package/src/ui/swarm-controls.tsx +3 -3
  99. package/src/ui/swarm-dashboard.tsx +3 -3
  100. package/src/ui/task-detail-view.tsx +29 -52
  101. package/src/ui/task-editor.tsx +14 -51
  102. package/src/ui/task-pool-list.tsx +1 -3
@@ -27,7 +27,7 @@ import { InstanceManager } from './instance-manager.js';
27
27
  import { ProfileStore } from './profile-store.js';
28
28
  import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision, ProfileBehavior } from './profile-types.js';
29
29
  import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
30
- import type { Task, RunSummary } from './task-types.js';
30
+ import type { Task, RunProgress } from './task-types.js';
31
31
  import type { WorkflowResult } from './types.js';
32
32
 
33
33
  // ---------------------------------------------------------------------------
@@ -49,12 +49,9 @@ export interface SwarmState {
49
49
 
50
50
  budgets: SwarmBudgets;
51
51
 
52
- autoRetry: boolean;
53
- maxAttemptsDefault: number;
54
-
55
52
  // Stats
56
53
  tasksCompleted: number;
57
- tasksFailed: number;
54
+ runsIncomplete: number;
58
55
  totalTokensUsed: number;
59
56
  totalCost: number;
60
57
  }
@@ -65,8 +62,6 @@ export interface SwarmConfig {
65
62
  sessionBudgetCost?: number;
66
63
  workspaceBudgetTokens?: number;
67
64
  workspaceBudgetCost?: number;
68
- autoRetry?: boolean;
69
- maxAttemptsDefault?: number;
70
65
  }
71
66
 
72
67
  export interface SwarmStartConfig {
@@ -80,7 +75,7 @@ export interface SwarmStartConfig {
80
75
  // ---------------------------------------------------------------------------
81
76
 
82
77
  const DISPATCH_LOOP_SLEEP_MS = 2000;
83
- const TASK_TIMEOUT_MS = 12 * 60 * 1000; // 12 minutes (> AI_CALL_TIMEOUT 10min, so AI times out first)
78
+ // No TASK_TIMEOUT_MS AI call timeout (10min) in the worker is the only boundary.
84
79
  const SWARM_STATE_FILE = 'swarm.json';
85
80
 
86
81
  // ---------------------------------------------------------------------------
@@ -277,8 +272,6 @@ export class SwarmController {
277
272
  if (config.sessionBudgetCost !== undefined) this.state.budgets.session.limitCost = config.sessionBudgetCost;
278
273
  if (config.workspaceBudgetTokens !== undefined) this.state.budgets.workspace.limitTokens = config.workspaceBudgetTokens;
279
274
  if (config.workspaceBudgetCost !== undefined) this.state.budgets.workspace.limitCost = config.workspaceBudgetCost;
280
- if (config.autoRetry !== undefined) this.state.autoRetry = config.autoRetry;
281
- if (config.maxAttemptsDefault !== undefined) this.state.maxAttemptsDefault = config.maxAttemptsDefault;
282
275
  this._persist();
283
276
  }
284
277
 
@@ -295,20 +288,13 @@ export class SwarmController {
295
288
  }
296
289
  }
297
290
 
298
- // Reset in-progress tasks
291
+ // Reset in-progress tasks back to open
299
292
  const tasks = await this.taskStore.list({ status: 'in-progress' });
300
293
  for (const task of tasks) {
301
- if (this.state.autoRetry && task.attempt < task.maxAttempts) {
302
- await this.taskStore.update(task.id, {
303
- status: 'pending',
304
- currentBotId: undefined,
305
- });
306
- } else {
307
- await this.taskStore.update(task.id, {
308
- status: 'open',
309
- currentBotId: undefined,
310
- });
311
- }
294
+ await this.taskStore.update(task.id, {
295
+ status: 'open',
296
+ activeRunId: undefined,
297
+ });
312
298
  }
313
299
 
314
300
  // Reset swarm status to idle
@@ -321,6 +307,66 @@ export class SwarmController {
321
307
  // Token/cost recording
322
308
  // -----------------------------------------------------------------------
323
309
 
310
+ private _buildBasicRunProgress(
311
+ runId: string, workerId: string, profileId: string,
312
+ result: { success?: boolean; summary?: string },
313
+ tokensUsed: number, costUsed: number, durationMs: number,
314
+ ): RunProgress {
315
+ // Determine outcome: zero-work runs (0 tokens, short duration) are stalled, not contributed
316
+ let outcome: 'completed' | 'contributed' | 'stalled' | 'crashed';
317
+ if (result.success) {
318
+ outcome = 'completed';
319
+ } else if (tokensUsed === 0 && durationMs < 10_000) {
320
+ // Ran for less than 10s with no AI tokens — something failed before work started
321
+ outcome = 'stalled';
322
+ } else {
323
+ outcome = 'contributed';
324
+ }
325
+
326
+ return {
327
+ runId,
328
+ botId: workerId,
329
+ profileId,
330
+ outcome,
331
+ filesCreated: [],
332
+ filesModified: [],
333
+ summary: result.summary || (outcome === 'completed' ? 'Completed' : outcome === 'stalled' ? 'Stalled (no AI work)' : 'Contributed'),
334
+ tokensUsed,
335
+ cost: costUsed,
336
+ durationMs,
337
+ endedAt: new Date().toISOString(),
338
+ };
339
+ }
340
+
341
+ private async _checkAcceptance(task: Task): Promise<import('./task-types.js').AcceptanceResult> {
342
+ const criteria = task.acceptance;
343
+ if (!criteria || !criteria.checks.length) {
344
+ return { met: true, results: [], checkedAt: new Date().toISOString() };
345
+ }
346
+
347
+ const { execSync } = await import('node:child_process');
348
+ const results: Array<{ name: string; pass: boolean; detail?: string }> = [];
349
+
350
+ for (const check of criteria.checks) {
351
+ try {
352
+ execSync(check.command, {
353
+ cwd: this.projectDir,
354
+ encoding: 'utf-8',
355
+ timeout: 60_000,
356
+ stdio: ['pipe', 'pipe', 'pipe'],
357
+ });
358
+ results.push({ name: check.name, pass: true });
359
+ } catch (err: unknown) {
360
+ const e = err as { stdout?: string; stderr?: string; message?: string };
361
+ const detail = (e.stdout ?? e.stderr ?? e.message ?? '').slice(0, 500);
362
+ results.push({ name: check.name, pass: false, detail });
363
+ }
364
+ }
365
+
366
+ const met = results.every(r => r.pass);
367
+ return { met, results, checkedAt: new Date().toISOString() };
368
+ }
369
+
324
370
  recordTokenUsage(workerId: string, _taskId: string, tokensUsed: number, cost: number): void {
325
371
  // Update worker in InstanceManager
326
372
  try {
@@ -419,7 +465,7 @@ export class SwarmController {
419
465
  }
420
466
 
421
467
  // Route unassigned tasks: top-level → orchestrator, subtasks → developer (fallback)
422
- const unassignedTasks = await this.taskStore.list({ status: ['open', 'pending'] });
468
+ const unassignedTasks = await this.taskStore.list({ status: 'open' });
423
469
  for (const t of unassignedTasks) {
424
470
  if (!t.assignedProfile && !t.isParent) {
425
471
  // Top-level tasks (no parent) go to orchestrator for decomposition.
@@ -430,17 +476,26 @@ export class SwarmController {
430
476
  }
431
477
  }
432
478
 
433
- // Collect pending tasks (and all tasks for dependency checks)
434
- const pendingTasks = await this.taskStore.list({ status: ['open', 'pending'] });
479
+ // Collect open tasks (and all tasks for dependency checks)
480
+ const pendingTasks = await this.taskStore.list({ status: 'open' });
435
481
  const allTasks = await this.taskStore.list();
436
482
  const routableTasks = pendingTasks.filter((t) => {
437
483
  // Skip parent tasks
438
484
  if (t.isParent) return false;
439
- // Skip tasks that have exhausted retries
440
- if (t.attempt >= t.maxAttempts) return false;
441
- // Skip tasks over per-task budget
442
- if (t.budgetTokens !== undefined && t.tokensUsed >= t.budgetTokens) return false;
443
- if (t.budgetCost !== undefined && t.costUsed >= t.budgetCost) return false;
485
+ // Skip tasks with exhausted budget
486
+ if (t.context.budgetExhausted) return false;
487
+ // Skip tasks in rapid-loop cooldown: if last run ended recently AND was zero-work,
488
+ // apply exponential backoff based on stagnation count
489
+ if (t.context.runHistory.length > 0) {
490
+ const lastRun = t.context.runHistory[t.context.runHistory.length - 1];
491
+ if ('endedAt' in lastRun && lastRun.endedAt) {
492
+ const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt).getTime()) / 1000;
493
+ const stag = t.context.stagnationCount;
494
+ // Exponential backoff: 10s, 20s, 40s, 80s, 160s... based on stagnation
495
+ const cooldownSecs = stag > 0 ? Math.min(10 * Math.pow(2, stag - 1), 300) : 0;
496
+ if (secsSinceLastRun < cooldownSecs) return false;
497
+ }
498
+ }
444
499
  // Skip tasks whose dependencies are not all done
445
500
  if (t.dependsOn && t.dependsOn.length > 0) {
446
501
  const allDepsDone = t.dependsOn.every((depId) => {
@@ -453,7 +508,7 @@ export class SwarmController {
453
508
  );
454
509
  return true;
455
510
  }
456
- return dep.status === 'done';
511
+ return dep.status === 'done' || dep.status === 'cancelled';
457
512
  });
458
513
  if (!allDepsDone) return false;
459
514
  }
@@ -484,9 +539,9 @@ export class SwarmController {
484
539
  complexity: t.complexity ?? 'moderate',
485
540
  assignedProfile: t.assignedProfile,
486
541
  context: {
487
- runSummaries: t.context.runSummaries.map((rs) => ({
488
- outcome: rs.outcome,
489
- botId: rs.botId,
542
+ runHistory: t.context.runHistory.map((rp) => ({
543
+ outcome: rp.outcome,
544
+ botId: rp.botId,
490
545
  })),
491
546
  },
492
547
  })),
@@ -550,7 +605,7 @@ export class SwarmController {
550
605
  // Mark worker as executing with the task's profileId
551
606
  const runId = RunStore.newId();
552
607
  this.instanceManager.markExecuting(assignment.instanceId, assignment.taskId, runId, assignment.profileId);
553
- await this.taskStore.update(assignment.taskId, { currentRunId: runId });
608
+ await this.taskStore.update(assignment.taskId, { activeRunId: runId });
554
609
 
555
610
  // Sync state for dashboard
556
611
  this._syncInstancesState();
@@ -644,9 +699,8 @@ export class SwarmController {
644
699
  mode: task.context.files.length > 0 ? 'modify' : 'create',
645
700
  targets: task.context.files.length > 0 ? task.context.files : undefined,
646
701
  options: { autoApprove: true },
647
- attempt: task.attempt,
648
- lastError: task.context.lastError,
649
- runSummaries: task.context.runSummaries,
702
+ runHistory: task.context.runHistory,
703
+ stagnationCount: task.context.stagnationCount,
650
704
  });
651
705
 
652
706
  // Build behavior config from profile preferences, adjusted for task complexity.
@@ -664,20 +718,16 @@ export class SwarmController {
664
718
  const workflowPath = path.isAbsolute(bot.filePath)
665
719
  ? bot.filePath
666
720
  : path.resolve(this.projectDir, bot.filePath);
667
- const timeoutPromise = new Promise<never>((_, reject) =>
668
- setTimeout(() => reject(new Error('Task execution timeout (10min)')), TASK_TIMEOUT_MS),
669
- );
670
- result = await Promise.race([
671
- runWorkflow(workflowPath, {
672
- runId,
673
- taskId,
674
- botId: workerId,
675
- config: { provider: 'auto' },
676
- params: { taskJson, projectDir: this.projectDir, behaviorJson },
677
- eventLog: runEventLog,
678
- }),
679
- timeoutPromise,
680
- ]);
721
+ // No swarm-level timeout race — the AI call timeout (10min) in the worker
722
+ // is the only boundary. This prevents orphaned runs from timeout races.
723
+ result = await runWorkflow(workflowPath, {
724
+ runId,
725
+ taskId,
726
+ botId: workerId,
727
+ config: { provider: 'auto' },
728
+ params: { taskJson, projectDir: this.projectDir, behaviorJson },
729
+ eventLog: runEventLog,
730
+ });
681
731
 
682
732
  runEventLog.done();
683
733
  } catch (err) {
@@ -690,26 +740,64 @@ export class SwarmController {
690
740
 
691
741
  const durationMs = Date.now() - startTime;
692
742
 
693
- // Extract run summary
743
+ // Extract RunProgress from the workflow context if available (built by agent-execute),
744
+ // otherwise build a basic one from the WorkflowResult.
694
745
  const tokensUsed = Math.max(0, (result.cost?.totalInputTokens ?? 0) + (result.cost?.totalOutputTokens ?? 0));
695
746
  const costUsed = Math.max(0, result.cost?.totalCost ?? 0);
696
747
 
697
- const runSummary: RunSummary = {
698
- runId,
699
- botId: workerId,
700
- outcome: result.success ? 'success' : 'failed',
701
- summary: result.summary || (result.success ? 'Completed' : 'Failed'),
702
- report: result.report,
703
- filesModified: [],
704
- error: result.success ? undefined : (result.summary || 'Unknown error'),
705
- durationMs,
706
- tokensUsed,
707
- cost: costUsed,
708
- };
748
+ let runProgress: RunProgress;
749
+ const ctxRunProgress = result.runProgressJson;
750
+ if (ctxRunProgress) {
751
+ try {
752
+ const parsed = JSON.parse(ctxRunProgress);
753
+ runProgress = {
754
+ runId,
755
+ botId: workerId,
756
+ profileId: profile.id,
757
+ outcome: parsed.outcome ?? (result.success ? 'completed' : 'contributed'),
758
+ filesCreated: parsed.filesCreated ?? [],
759
+ filesModified: parsed.filesModified ?? [],
760
+ summary: parsed.summary ?? result.summary ?? '',
761
+ remainingWork: parsed.remainingWork,
762
+ blockers: parsed.blockers,
763
+ checks: parsed.checks,
764
+ tokensUsed: parsed.tokensUsed ?? tokensUsed,
765
+ cost: parsed.cost ?? costUsed,
766
+ durationMs: parsed.durationMs ?? durationMs,
767
+ endedAt: new Date().toISOString(),
768
+ };
769
+ } catch {
770
+ // JSON parse failed — fall back to basic
771
+ runProgress = this._buildBasicRunProgress(runId, workerId, profile.id, result, tokensUsed, costUsed, durationMs);
772
+ }
773
+ } else {
774
+ runProgress = this._buildBasicRunProgress(runId, workerId, profile.id, result, tokensUsed, costUsed, durationMs);
775
+ }
776
+
777
+ // Determine release status based on convergent model:
778
+ // - completed + no acceptance → done
779
+ // - completed + has acceptance → run acceptance check, done if met, open if not
780
+ // - contributed/stalled/crashed → open (task stays available for next run)
781
+ const task = await this.taskStore.get(taskId);
782
+ let releaseStatus: 'done' | 'open' = 'open';
783
+
784
+ if (runProgress.outcome === 'completed') {
785
+ if (!task?.acceptance) {
786
+ releaseStatus = 'done';
787
+ } else {
788
+ // Run deterministic acceptance check
789
+ const acceptResult = await this._checkAcceptance(task);
790
+ if (acceptResult.met) {
791
+ releaseStatus = 'done';
792
+ }
793
+ // Store the result on the task regardless
794
+ if (task) {
795
+ await this.taskStore.update(taskId, { lastAcceptanceCheck: acceptResult } as Record<string, unknown>);
796
+ }
797
+ }
798
+ }
709
799
 
710
- // Release task
711
- const releaseStatus = result.success ? 'done' : 'open';
712
- await this.taskStore.release(taskId, releaseStatus, runSummary);
800
+ await this.taskStore.release(taskId, releaseStatus, runProgress);
713
801
 
714
802
  // Record token usage
715
803
  this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
@@ -718,7 +806,7 @@ export class SwarmController {
718
806
  if (result.success) {
719
807
  this.state.tasksCompleted += 1;
720
808
  } else {
721
- this.state.tasksFailed += 1;
809
+ this.state.runsIncomplete += 1;
722
810
  }
723
811
 
724
812
  // Mark worker as idle (clears profile association)
@@ -728,9 +816,9 @@ export class SwarmController {
728
816
 
729
817
  // Emit swarm-level task event
730
818
  this.eventLog.emit({
731
- type: result.success ? 'task-done' : 'task-failed',
819
+ type: result.success ? 'task-done' : 'task-run-incomplete',
732
820
  timestamp: Date.now(),
733
- data: { botId: workerId, taskId, outcome: runSummary.outcome },
821
+ data: { botId: workerId, taskId, outcome: runProgress.outcome },
734
822
  });
735
823
 
736
824
  // Emit hierarchy-scoped event so sibling tasks can see what happened
@@ -739,13 +827,13 @@ export class SwarmController {
739
827
  if (task?.parentId) {
740
828
  this.hierarchyEventLog.emit({
741
829
  parentId: task.parentId,
742
- type: result.success ? 'task-completed' : 'task-run-failed',
830
+ type: result.success ? 'task-completed' : 'task-run-incomplete',
743
831
  taskId,
744
832
  data: {
745
- summary: runSummary.summary,
746
- filesModified: runSummary.filesModified,
833
+ summary: runProgress.summary,
834
+ filesModified: runProgress.filesModified,
747
835
  botId: workerId,
748
- attempt: task.attempt,
836
+ runCount: task.context.runHistory.length,
749
837
  },
750
838
  });
751
839
  }
@@ -851,10 +939,8 @@ export class SwarmController {
851
939
  workspace: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
852
940
  session: { limitTokens: 0, usedTokens: 0, limitCost: 0, usedCost: 0 },
853
941
  },
854
- autoRetry: true,
855
- maxAttemptsDefault: 3,
856
942
  tasksCompleted: 0,
857
- tasksFailed: 0,
943
+ runsIncomplete: 0,
858
944
  totalTokensUsed: 0,
859
945
  totalCost: 0,
860
946
  };
@@ -6,25 +6,22 @@
6
6
  * {description}
7
7
  * ### Notes
8
8
  * ### Relevant Files
9
- * ### Previous Attempts (last 3 full, older condensed)
10
- * ### Last Error
9
+ * ### Previous Runs (context decay: last run + acceptance check)
11
10
  * ### Parent Context (title + description + sibling status)
12
11
  *
13
12
  * Truncation cascade when over budget:
14
- * 1. Older summaries → one-liners only
13
+ * 1. Older runs → one-liners only
15
14
  * 2. File list → only files from last 2 runs
16
15
  * 3. Parent context → title only
17
16
  * 4. Notes → first 500 chars
18
17
  */
19
18
 
20
- import type { Task, RunSummary } from './task-types.js';
19
+ import type { Task, RunProgress } from './task-types.js';
21
20
  import type { ProfilePreferences } from './profile-types.js';
22
21
 
23
22
  const MAX_CONTEXT_TOKENS = 4000;
24
23
  const CHARS_PER_TOKEN = 4; // rough estimate
25
24
  const MAX_CONTEXT_CHARS = MAX_CONTEXT_TOKENS * CHARS_PER_TOKEN;
26
- const FULL_SUMMARY_COUNT = 3;
27
-
28
25
  export function buildTaskPrompt(
29
26
  task: Task,
30
27
  parentTask?: Task | null,
@@ -67,28 +64,49 @@ function buildFull(
67
64
  sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
68
65
  }
69
66
 
70
- // Run summaries: last 3 in full, older condensed
71
- const summaries = task.context.runSummaries;
72
- if (summaries.length > 0) {
73
- const lines: string[] = [];
74
- const recentStart = Math.max(0, summaries.length - FULL_SUMMARY_COUNT);
67
+ // --- Context decay: workspace is the source of truth, not history ---
68
+ // Workers see: last acceptance check, last run's remainingWork/blockers,
69
+ // stagnation count, and a directive to read the workspace.
75
70
 
76
- // Condensed older summaries
77
- for (let i = 0; i < recentStart; i++) {
78
- lines.push(condenseSummary(summaries[i]!));
79
- }
71
+ // 2.3.2: Last acceptance check result
72
+ if (task.lastAcceptanceCheck) {
73
+ const ac = task.lastAcceptanceCheck;
74
+ const checkLines = ac.results
75
+ .map(r => `- ${r.name}: ${r.pass ? 'PASS' : 'FAIL'}${r.detail ? ` (${r.detail.slice(0, 100)})` : ''}`)
76
+ .join('\n');
77
+ sections.push(`### Acceptance Check (${ac.met ? 'ALL PASS' : 'ISSUES FOUND'})\n${checkLines}`);
78
+ }
80
79
 
81
- // Full recent summaries
82
- for (let i = recentStart; i < summaries.length; i++) {
83
- lines.push(fullSummary(summaries[i]!));
84
- }
80
+ // 2.3.3: Continue from last run's remaining work
81
+ const lastRun = task.context.runHistory.length > 0
82
+ ? task.context.runHistory[task.context.runHistory.length - 1]
83
+ : undefined;
84
+ if (lastRun && 'remainingWork' in lastRun && lastRun.remainingWork) {
85
+ sections.push(`### Continue From\n${lastRun.remainingWork}`);
86
+ }
87
+
88
+ // 2.3.4: Blockers from last stalled run
89
+ if (lastRun && 'blockers' in lastRun && Array.isArray(lastRun.blockers) && lastRun.blockers.length > 0) {
90
+ sections.push(`### Previous Run Blocked By\n${(lastRun.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
91
+ }
92
+
93
+ // Last run summary (one run only, not full history)
94
+ if (lastRun && 'summary' in lastRun) {
95
+ sections.push(`### Last Run\nOutcome: ${lastRun.outcome} | ${lastRun.summary}`);
96
+ }
85
97
 
86
- sections.push(`### Previous Attempts\n${lines.join('\n')}`);
98
+ // Run count + stagnation
99
+ if (task.context.runHistory.length > 0) {
100
+ let meta = `Total runs: ${task.context.runHistory.length}`;
101
+ if (task.context.stagnationCount > 0) {
102
+ meta += ` | Stagnation: ${task.context.stagnationCount} run(s) with no new changes — try a different approach`;
103
+ }
104
+ sections.push(`### Run History\n${meta}`);
87
105
  }
88
106
 
89
- // Last error
90
- if (task.context.lastError) {
91
- sections.push(`### Last Error\n${task.context.lastError}`);
107
+ // Directive: read the workspace, don't rely on stale context
108
+ if (task.context.runHistory.length > 0) {
109
+ sections.push(`### Important\nRead the workspace to understand current state. Do not assume previous run summaries are accurate — files may have changed since then.`);
92
110
  }
93
111
 
94
112
  // Execution preferences
@@ -119,9 +137,9 @@ function truncatePrompt(
119
137
  siblingTasks?: Task[],
120
138
  preferences?: ProfilePreferences,
121
139
  ): string {
122
- // Level 1: All summaries condensed to one-liners
140
+ // Level 1: All runs condensed to one-liners
123
141
  let result = buildWithTruncation(task, parentTask, siblingTasks, {
124
- allSummariesCondensed: true,
142
+ allRunsCondensed: true,
125
143
  filesFromRecentRunsOnly: false,
126
144
  parentTitleOnly: false,
127
145
  notesCapped: false,
@@ -130,7 +148,7 @@ function truncatePrompt(
130
148
 
131
149
  // Level 2: File list → only files from last 2 runs
132
150
  result = buildWithTruncation(task, parentTask, siblingTasks, {
133
- allSummariesCondensed: true,
151
+ allRunsCondensed: true,
134
152
  filesFromRecentRunsOnly: true,
135
153
  parentTitleOnly: false,
136
154
  notesCapped: false,
@@ -139,7 +157,7 @@ function truncatePrompt(
139
157
 
140
158
  // Level 3: Parent context → title only
141
159
  result = buildWithTruncation(task, parentTask, siblingTasks, {
142
- allSummariesCondensed: true,
160
+ allRunsCondensed: true,
143
161
  filesFromRecentRunsOnly: true,
144
162
  parentTitleOnly: true,
145
163
  notesCapped: false,
@@ -148,7 +166,7 @@ function truncatePrompt(
148
166
 
149
167
  // Level 4: Notes → first 500 chars
150
168
  result = buildWithTruncation(task, parentTask, siblingTasks, {
151
- allSummariesCondensed: true,
169
+ allRunsCondensed: true,
152
170
  filesFromRecentRunsOnly: true,
153
171
  parentTitleOnly: true,
154
172
  notesCapped: true,
@@ -163,7 +181,7 @@ function truncatePrompt(
163
181
  }
164
182
 
165
183
  interface TruncationFlags {
166
- allSummariesCondensed: boolean;
184
+ allRunsCondensed: boolean;
167
185
  filesFromRecentRunsOnly: boolean;
168
186
  parentTitleOnly: boolean;
169
187
  notesCapped: boolean;
@@ -193,8 +211,13 @@ function buildWithTruncation(
193
211
  // Files (potentially only from recent runs)
194
212
  if (flags.filesFromRecentRunsOnly) {
195
213
  const recentFiles = new Set<string>();
196
- for (const s of task.context.runSummaries.slice(-2)) {
197
- for (const f of s.filesModified) recentFiles.add(f);
214
+ for (const rp of task.context.runHistory.slice(-2)) {
215
+ if ('filesModified' in rp) {
216
+ for (const f of rp.filesModified) recentFiles.add(f);
217
+ }
218
+ if ('filesCreated' in rp) {
219
+ for (const f of (rp as RunProgress).filesCreated) recentFiles.add(f);
220
+ }
198
221
  }
199
222
  if (recentFiles.size > 0) {
200
223
  sections.push(`### Relevant Files\n${[...recentFiles].join('\n')}`);
@@ -203,31 +226,39 @@ function buildWithTruncation(
203
226
  sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
204
227
  }
205
228
 
206
- // All summaries condensed
207
- if (task.context.runSummaries.length > 0) {
208
- if (flags.allSummariesCondensed) {
209
- const lines = task.context.runSummaries.map((s) => condenseSummary(s));
210
- sections.push(`### Previous Attempts\n${lines.join('\n')}`);
211
- } else {
212
- // This path isn't used in truncation, but included for completeness
213
- const lines: string[] = [];
214
- const recentStart = Math.max(0, task.context.runSummaries.length - FULL_SUMMARY_COUNT);
215
- for (let i = 0; i < recentStart; i++) {
216
- lines.push(condenseSummary(task.context.runSummaries[i]!));
217
- }
218
- for (let i = recentStart; i < task.context.runSummaries.length; i++) {
219
- lines.push(fullSummary(task.context.runSummaries[i]!));
220
- }
221
- sections.push(`### Previous Attempts\n${lines.join('\n')}`);
229
+ // Context decay: last acceptance check + last run only
230
+ if (task.lastAcceptanceCheck) {
231
+ const ac = task.lastAcceptanceCheck;
232
+ const checkLines = ac.results
233
+ .map(r => `- ${r.name}: ${r.pass ? 'PASS' : 'FAIL'}`)
234
+ .join('\n');
235
+ sections.push(`### Acceptance Check (${ac.met ? 'ALL PASS' : 'ISSUES FOUND'})\n${checkLines}`);
236
+ }
237
+
238
+ const lastRunT = task.context.runHistory.length > 0
239
+ ? task.context.runHistory[task.context.runHistory.length - 1]
240
+ : undefined;
241
+ if (lastRunT && 'remainingWork' in lastRunT && lastRunT.remainingWork) {
242
+ sections.push(`### Continue From\n${lastRunT.remainingWork}`);
243
+ }
244
+ if (lastRunT && 'blockers' in lastRunT && Array.isArray(lastRunT.blockers) && lastRunT.blockers.length > 0) {
245
+ sections.push(`### Previous Run Blocked By\n${(lastRunT.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
246
+ }
247
+ if (lastRunT && 'summary' in lastRunT) {
248
+ sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
249
+ }
250
+
251
+ if (task.context.runHistory.length > 0) {
252
+ let meta = `Total runs: ${task.context.runHistory.length}`;
253
+ if (task.context.stagnationCount > 0) {
254
+ meta += ` | Stagnation: ${task.context.stagnationCount} — try a different approach`;
222
255
  }
256
+ sections.push(`### Run History\n${meta}`);
223
257
  }
224
258
 
225
- // Last error (truncated in aggressive mode)
226
- if (task.context.lastError) {
227
- const errorText = flags.notesCapped
228
- ? task.context.lastError.slice(0, 300)
229
- : task.context.lastError;
230
- sections.push(`### Last Error\n${errorText}`);
259
+ // Directive: read the workspace, don't rely on stale context
260
+ if (task.context.runHistory.length > 0) {
261
+ sections.push(`### Important\nRead the workspace to understand current state. Do not assume previous run summaries are accurate — files may have changed since then.`);
231
262
  }
232
263
 
233
264
  // Execution preferences
@@ -256,27 +287,11 @@ function buildWithTruncation(
256
287
  // Formatting helpers
257
288
  // ---------------------------------------------------------------------------
258
289
 
259
- function condenseSummary(s: RunSummary): string {
260
- return `- Run ${s.runId.slice(0, 8)} (${s.botId}): ${s.outcome} — ${s.summary.slice(0, 80)} (${Math.round(s.durationMs / 1000)}s, ${s.tokensUsed} tok)`;
261
- }
262
-
263
- function fullSummary(s: RunSummary): string {
264
- const lines: string[] = [];
265
- lines.push(`\n**Run ${s.runId.slice(0, 8)} (${s.botId}) — ${s.outcome}**`);
266
- lines.push(s.summary);
267
- if (s.filesModified.length > 0) lines.push(`Files: ${s.filesModified.join(', ')}`);
268
- if (s.error) lines.push(`Error: ${s.error}`);
269
- lines.push(
270
- `Duration: ${Math.round(s.durationMs / 1000)}s · Tokens: ${s.tokensUsed} · Cost: $${s.cost.toFixed(3)}`,
271
- );
272
- return lines.join('\n');
273
- }
274
-
275
290
  function buildParentSection(
276
291
  parentTask: Task,
277
292
  siblingTasks?: Task[],
278
293
  ): string {
279
- let section = `### Parent Context\nPart of: "${parentTask.title}" ${parentTask.description}`;
294
+ let section = `### Parent Context\nPart of: "${parentTask.title}" \u2014 ${parentTask.description}`;
280
295
  if (siblingTasks && siblingTasks.length > 0) {
281
296
  const siblingLines = siblingTasks
282
297
  .map(
@@ -309,7 +324,7 @@ function statusIcon(status: string): string {
309
324
  switch (status) {
310
325
  case 'done':
311
326
  return '\u2705';
312
- case 'failed':
327
+ case 'cancelled':
313
328
  return '\u274C';
314
329
  case 'in-progress':
315
330
  return '\u25CF';