@synergenius/flow-weaver-pack-weaver 0.9.195 → 0.9.197

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 (72) hide show
  1. package/dist/bot/ai-client.d.ts +5 -0
  2. package/dist/bot/ai-client.d.ts.map +1 -1
  3. package/dist/bot/ai-client.js +43 -0
  4. package/dist/bot/ai-client.js.map +1 -1
  5. package/dist/bot/behavior-defaults.d.ts +3 -1
  6. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  7. package/dist/bot/behavior-defaults.js +7 -0
  8. package/dist/bot/behavior-defaults.js.map +1 -1
  9. package/dist/bot/capability-registry.js +1 -1
  10. package/dist/bot/dream-task.d.ts +45 -0
  11. package/dist/bot/dream-task.d.ts.map +1 -0
  12. package/dist/bot/dream-task.js +125 -0
  13. package/dist/bot/dream-task.js.map +1 -0
  14. package/dist/bot/knowledge-store.d.ts +9 -0
  15. package/dist/bot/knowledge-store.d.ts.map +1 -1
  16. package/dist/bot/knowledge-store.js +21 -0
  17. package/dist/bot/knowledge-store.js.map +1 -1
  18. package/dist/bot/post-turn-hooks.d.ts +57 -0
  19. package/dist/bot/post-turn-hooks.d.ts.map +1 -0
  20. package/dist/bot/post-turn-hooks.js +108 -0
  21. package/dist/bot/post-turn-hooks.js.map +1 -0
  22. package/dist/bot/profile-types.d.ts +16 -0
  23. package/dist/bot/profile-types.d.ts.map +1 -1
  24. package/dist/bot/swarm-controller.d.ts +5 -0
  25. package/dist/bot/swarm-controller.d.ts.map +1 -1
  26. package/dist/bot/swarm-controller.js +92 -9
  27. package/dist/bot/swarm-controller.js.map +1 -1
  28. package/dist/bot/task-types.d.ts +11 -0
  29. package/dist/bot/task-types.d.ts.map +1 -1
  30. package/dist/node-types/agent-execute.d.ts.map +1 -1
  31. package/dist/node-types/agent-execute.js +18 -2
  32. package/dist/node-types/agent-execute.js.map +1 -1
  33. package/dist/node-types/build-context.d.ts +4 -3
  34. package/dist/node-types/build-context.d.ts.map +1 -1
  35. package/dist/node-types/build-context.js +21 -5
  36. package/dist/node-types/build-context.js.map +1 -1
  37. package/dist/node-types/verify-task.d.ts +22 -0
  38. package/dist/node-types/verify-task.d.ts.map +1 -0
  39. package/dist/node-types/verify-task.js +143 -0
  40. package/dist/node-types/verify-task.js.map +1 -0
  41. package/dist/ui/capability-editor.js +1 -1
  42. package/dist/ui/profile-editor.js +1 -1
  43. package/dist/ui/swarm-dashboard.js +1 -1
  44. package/dist/workflows/weaver-agent.d.ts +3 -3
  45. package/dist/workflows/weaver-agent.d.ts.map +1 -1
  46. package/dist/workflows/weaver-agent.js +267 -18
  47. package/dist/workflows/weaver-agent.js.map +1 -1
  48. package/dist/workflows/weaver-bot-batch.d.ts +3 -3
  49. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  50. package/dist/workflows/weaver-bot-batch.js +280 -24
  51. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  52. package/dist/workflows/weaver-bot.d.ts +2 -0
  53. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  54. package/dist/workflows/weaver-bot.js +15 -10
  55. package/dist/workflows/weaver-bot.js.map +1 -1
  56. package/flowweaver.manifest.json +1 -1
  57. package/package.json +3 -3
  58. package/src/bot/ai-client.ts +54 -0
  59. package/src/bot/behavior-defaults.ts +9 -1
  60. package/src/bot/capability-registry.ts +1 -1
  61. package/src/bot/dream-task.ts +167 -0
  62. package/src/bot/knowledge-store.ts +27 -0
  63. package/src/bot/post-turn-hooks.ts +137 -0
  64. package/src/bot/profile-types.ts +17 -0
  65. package/src/bot/swarm-controller.ts +103 -13
  66. package/src/bot/task-types.ts +19 -0
  67. package/src/node-types/agent-execute.ts +19 -2
  68. package/src/node-types/build-context.ts +28 -6
  69. package/src/node-types/verify-task.ts +181 -0
  70. package/src/workflows/weaver-agent.ts +429 -18
  71. package/src/workflows/weaver-bot-batch.ts +443 -24
  72. package/src/workflows/weaver-bot.ts +16 -11
@@ -0,0 +1,167 @@
1
+ /**
2
+ * DreamTask — idle-time knowledge consolidation.
3
+ *
4
+ * Runs during swarm dispatch loop idle periods to extract cross-task
5
+ * patterns, detect recurring failures, clean stale knowledge, and
6
+ * convert InsightEngine findings into knowledge entries.
7
+ *
8
+ * Purely heuristic — no LLM calls. Designed to run within 500ms.
9
+ * Key namespaces (non-overlapping with memory-extractor's project:*):
10
+ * - pattern:hot-file:* — files modified by multiple tasks (24h TTL)
11
+ * - warning:recurring-failure:* — recurring failures (7d TTL)
12
+ * - insight:* — converted InsightEngine insights (7d TTL)
13
+ * - session:stats — rolling session statistics (overwritten each cycle)
14
+ */
15
+
16
+ import { KnowledgeStore, type KnowledgeEntry } from './knowledge-store.js';
17
+ import { RunStore } from './run-store.js';
18
+ import type { Insight, ProjectModel } from './types.js';
19
+
20
+ /** Minimum time between consolidation runs (ms). */
21
+ const COOLDOWN_MS = 60_000;
22
+
23
+ /** TTL for dream-sourced entries (ms). */
24
+ const STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
25
+
26
+ /** TTL for hot-file patterns (ms). */
27
+ const HOT_FILE_TTL_MS = 24 * 60 * 60 * 1000;
28
+
29
+ /** Minimum confidence for converting insights to knowledge. */
30
+ const MIN_CONFIDENCE = 0.6;
31
+
32
+ export interface DreamTaskOptions {
33
+ projectDir: string;
34
+ }
35
+
36
+ export interface DreamResult {
37
+ warningsStored: number;
38
+ staleEntriesCleaned: number;
39
+ insightsConverted: number;
40
+ durationMs: number;
41
+ }
42
+
43
+ export class DreamTask {
44
+ private lastConsolidatedAt = 0;
45
+ private readonly projectDir: string;
46
+
47
+ constructor(opts: DreamTaskOptions) {
48
+ this.projectDir = opts.projectDir;
49
+ }
50
+
51
+ shouldRun(): boolean {
52
+ return Date.now() - this.lastConsolidatedAt >= COOLDOWN_MS;
53
+ }
54
+
55
+ async consolidate(): Promise<DreamResult> {
56
+ const start = Date.now();
57
+ this.lastConsolidatedAt = start;
58
+
59
+ const store = new KnowledgeStore(this.projectDir);
60
+ let warningsStored = 0;
61
+ let staleEntriesCleaned = 0;
62
+ let insightsConverted = 0;
63
+
64
+ // Phase 1: Clean stale dream-sourced entries
65
+ staleEntriesCleaned = this.cleanStaleEntries(store);
66
+
67
+ // Phase 2: Convert InsightEngine findings to knowledge
68
+ try {
69
+ insightsConverted = await this.convertInsights(store);
70
+ } catch { /* non-fatal */ }
71
+
72
+ // Phase 3: Session stats
73
+ try {
74
+ this.updateSessionStats(store);
75
+ } catch { /* non-fatal */ }
76
+
77
+ return {
78
+ warningsStored,
79
+ staleEntriesCleaned,
80
+ insightsConverted,
81
+ durationMs: Date.now() - start,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Phase 1: Remove stale dream-sourced entries.
87
+ * Only cleans entries with source starting with 'dream-task'.
88
+ */
89
+ private cleanStaleEntries(store: KnowledgeStore): number {
90
+ const now = Date.now();
91
+ const entries = store.list();
92
+ let cleaned = 0;
93
+
94
+ for (const entry of entries) {
95
+ if (!entry.source?.startsWith('dream-task')) continue;
96
+
97
+ const age = now - entry.createdAt;
98
+ const isHotFile = entry.key.startsWith('pattern:hot-file:');
99
+ const ttl = isHotFile ? HOT_FILE_TTL_MS : STALE_THRESHOLD_MS;
100
+
101
+ if (age > ttl) {
102
+ store.forget(entry.key);
103
+ cleaned++;
104
+ }
105
+ }
106
+
107
+ return cleaned;
108
+ }
109
+
110
+ /**
111
+ * Phase 2: Convert high-confidence InsightEngine insights to knowledge.
112
+ * Imports ProjectModel and InsightEngine dynamically to avoid circular deps.
113
+ */
114
+ private async convertInsights(store: KnowledgeStore): Promise<number> {
115
+ const { ProjectModelStore } = await import('./project-model.js');
116
+ const { InsightEngine } = await import('./insight-engine.js');
117
+
118
+ const pms = new ProjectModelStore(this.projectDir);
119
+ const model = await pms.getOrBuild();
120
+ if (!model) return 0;
121
+
122
+ const engine = new InsightEngine();
123
+ const insights = engine.analyze(model);
124
+ const existingKeys = new Set(store.list().map(e => e.key));
125
+ let converted = 0;
126
+
127
+ for (const insight of insights) {
128
+ if (insight.confidence < MIN_CONFIDENCE) continue;
129
+ if (insight.severity !== 'warning' && insight.severity !== 'critical') continue;
130
+
131
+ const key = `insight:${insight.type}:${insight.id}`;
132
+ if (existingKeys.has(key)) continue;
133
+
134
+ store.learn(
135
+ key,
136
+ `${insight.title}: ${insight.description}. Suggestion: ${insight.suggestion}`,
137
+ 'dream-task:insight-engine',
138
+ );
139
+ converted++;
140
+ }
141
+
142
+ return converted;
143
+ }
144
+
145
+ /**
146
+ * Phase 3: Update rolling session statistics.
147
+ */
148
+ private updateSessionStats(store: KnowledgeStore): void {
149
+ try {
150
+ const runStore = new RunStore();
151
+ const recentRuns = runStore.list({ limit: 50 });
152
+
153
+ if (recentRuns.length === 0) return;
154
+
155
+ const successCount = recentRuns.filter(r => r.success).length;
156
+ const avgDuration = Math.round(
157
+ recentRuns.reduce((sum, r) => sum + (r.durationMs ?? 0), 0) / recentRuns.length,
158
+ );
159
+
160
+ store.learn(
161
+ 'session:stats',
162
+ `${recentRuns.length} recent runs, ${Math.round(successCount / recentRuns.length * 100)}% success, avg ${Math.round(avgDuration / 1000)}s`,
163
+ 'dream-task',
164
+ );
165
+ } catch { /* non-fatal */ }
166
+ }
167
+ }
@@ -42,6 +42,33 @@ export class KnowledgeStore {
42
42
  return this.readAll();
43
43
  }
44
44
 
45
+ /**
46
+ * Build a compact manifest of knowledge entries for LLM relevance selection.
47
+ * Returns entries sorted newest-first, capped at maxEntries.
48
+ * Format per line: `- [N] key (Xd ago): truncated_value`
49
+ */
50
+ static buildManifest(
51
+ entries: KnowledgeEntry[],
52
+ maxEntries = 200,
53
+ ): { manifest: string; entries: KnowledgeEntry[] } {
54
+ const now = Date.now();
55
+ const DAY_MS = 24 * 60 * 60 * 1000;
56
+ const sorted = [...entries]
57
+ .sort((a, b) => b.createdAt - a.createdAt)
58
+ .slice(0, maxEntries);
59
+
60
+ const lines = sorted.map((e, i) => {
61
+ const ageMs = now - e.createdAt;
62
+ const age = ageMs < DAY_MS
63
+ ? `${Math.round(ageMs / (60 * 60 * 1000))}h ago`
64
+ : `${Math.floor(ageMs / DAY_MS)}d ago`;
65
+ const value = e.value.length > 80 ? e.value.slice(0, 77) + '...' : e.value;
66
+ return `- [${i}] ${e.key} (${age}): ${value}`;
67
+ });
68
+
69
+ return { manifest: lines.join('\n'), entries: sorted };
70
+ }
71
+
45
72
  private readAll(): KnowledgeEntry[] {
46
73
  if (!fs.existsSync(this.filePath)) return [];
47
74
  const content = fs.readFileSync(this.filePath, 'utf-8').trim();
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Post-turn hook system for the agent loop.
3
+ *
4
+ * Hooks run after each iteration of the agent loop, enabling cost checks,
5
+ * steering, progress reporting, and knowledge extraction between LLM turns.
6
+ *
7
+ * Hooks run sequentially (not parallel) so abort hooks fire before
8
+ * subsequent hooks. Errors are caught per-hook — a failing hook does
9
+ * not block other hooks or the agent loop.
10
+ */
11
+
12
+ import type { TurnEndContext, TurnEndResult } from '@synergenius/flow-weaver/agent';
13
+ import { CostTracker } from './cost-tracker.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Hook interface
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export type HookTiming = 'every' | 'final' | 'between';
20
+
21
+ export interface PostTurnHook {
22
+ name: string;
23
+ /** When to run: 'every' = every turn, 'final' = only final turn, 'between' = only between turns */
24
+ timing: HookTiming;
25
+ execute(context: TurnEndContext): Promise<PostTurnHookResult>;
26
+ }
27
+
28
+ export interface PostTurnHookResult {
29
+ /** If false, abort the agent loop. Default: true. */
30
+ continue?: boolean;
31
+ /** Optional message to inject into conversation (steering nudge). */
32
+ injectMessage?: string;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Hook runner
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export class PostTurnHookRunner {
40
+ private hooks: PostTurnHook[] = [];
41
+
42
+ register(hook: PostTurnHook): void {
43
+ this.hooks.push(hook);
44
+ }
45
+
46
+ /** Returns the onTurnEnd callback to pass to runAgentLoop options. */
47
+ createCallback(): (ctx: TurnEndContext) => Promise<TurnEndResult | void> {
48
+ return async (ctx: TurnEndContext): Promise<TurnEndResult | void> => {
49
+ let injectMessage: string | undefined;
50
+
51
+ for (const hook of this.hooks) {
52
+ // Check timing
53
+ if (hook.timing === 'final' && !ctx.isFinalTurn) continue;
54
+ if (hook.timing === 'between' && ctx.isFinalTurn) continue;
55
+
56
+ try {
57
+ const result = await hook.execute(ctx);
58
+ if (result.continue === false) {
59
+ return { continue: false, injectMessage: result.injectMessage };
60
+ }
61
+ if (result.injectMessage) {
62
+ injectMessage = injectMessage
63
+ ? injectMessage + '\n' + result.injectMessage
64
+ : result.injectMessage;
65
+ }
66
+ } catch (err) {
67
+ // Hook failure is non-fatal — log and continue
68
+ if (process.env.WEAVER_VERBOSE) {
69
+ console.error(`[post-turn-hook] ${hook.name} failed:`, err);
70
+ }
71
+ }
72
+ }
73
+
74
+ if (injectMessage) return { injectMessage };
75
+ };
76
+ }
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Built-in hooks
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Cost checkpoint — aborts the loop when cumulative cost exceeds budget.
85
+ * Subsumes the standalone #6 per-turn budget enforcement approach.
86
+ */
87
+ export class CostCheckpointHook implements PostTurnHook {
88
+ name = 'cost-checkpoint';
89
+ timing: HookTiming = 'between';
90
+
91
+ constructor(
92
+ private maxCost: number,
93
+ private model: string,
94
+ ) {}
95
+
96
+ async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
97
+ const cost = CostTracker.estimateCost(this.model, {
98
+ inputTokens: ctx.usage.promptTokens,
99
+ outputTokens: ctx.usage.completionTokens,
100
+ });
101
+
102
+ if (cost >= this.maxCost) {
103
+ return {
104
+ continue: false,
105
+ injectMessage: `Budget exceeded: $${cost.toFixed(4)} >= $${this.maxCost.toFixed(4)}`,
106
+ };
107
+ }
108
+ return {};
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Progress report — emits a stream event with turn progress for UI updates.
114
+ */
115
+ export class ProgressReportHook implements PostTurnHook {
116
+ name = 'progress-report';
117
+ timing: HookTiming = 'every';
118
+
119
+ constructor(
120
+ private emitEvent: (event: { type: string; timestamp: number; data: Record<string, unknown> }) => void,
121
+ ) {}
122
+
123
+ async execute(ctx: TurnEndContext): Promise<PostTurnHookResult> {
124
+ this.emitEvent({
125
+ type: 'turn-progress',
126
+ timestamp: Date.now(),
127
+ data: {
128
+ iteration: ctx.iteration,
129
+ maxIterations: ctx.maxIterations,
130
+ toolCallCount: ctx.toolCallCount,
131
+ isFinalTurn: ctx.isFinalTurn,
132
+ usage: ctx.usage,
133
+ },
134
+ });
135
+ return {};
136
+ }
137
+ }
@@ -92,6 +92,21 @@ export interface PhaseDescriptor {
92
92
  * not hardcoded. Each workflow can have different phases (e.g. a review
93
93
  * bot might have analyze/report/suggest instead of plan/execute/review).
94
94
  */
95
+ /** Post-run verification config — independent review of completed work. */
96
+ export interface VerificationConfig {
97
+ /** Whether verification is enabled. Default: false. */
98
+ enabled: boolean;
99
+ /** Model tier for the verification agent. Default: 'standard'. */
100
+ tier: ModelTier;
101
+ /**
102
+ * How often to run verification (1 = every completed task, 2 = every other, etc.).
103
+ * Default: 1 (always verify).
104
+ */
105
+ frequency: number;
106
+ /** Re-open the task if verification fails. Default: true. */
107
+ reopenOnFail: boolean;
108
+ }
109
+
95
110
  export interface ProfileBehavior {
96
111
  /** Capability names this profile can use (e.g. ['core', 'file-ops', 'shell']). */
97
112
  capabilities?: string[];
@@ -107,6 +122,8 @@ export interface ProfileBehavior {
107
122
  exitProtocol: ExitProtocol;
108
123
  /** Structured output requirements. */
109
124
  outputContract?: OutputContract;
125
+ /** Post-run verification by an independent agent. */
126
+ verification?: VerificationConfig;
110
127
  }
111
128
 
112
129
  /** Structured exit status from a bot run. */
@@ -32,9 +32,10 @@ import { ProfileStore } from './profile-store.js';
32
32
  import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision, ProfileBehavior } from './profile-types.js';
33
33
  import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
34
34
  import type { Task, RunProgress } from './task-types.js';
35
- import type { WorkflowResult } from './types.js';
35
+ import type { WorkflowResult, ProviderInfo } from './types.js';
36
36
  import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
37
37
  import { shouldCompact, compactRunHistory } from './context-compactor.js';
38
+ import { DreamTask } from './dream-task.js';
38
39
  import { callAI } from './ai-client.js';
39
40
 
40
41
  // ---------------------------------------------------------------------------
@@ -87,6 +88,11 @@ const DISPATCH_LOOP_SLEEP_MS = 2000;
87
88
  // No TASK_TIMEOUT_MS — AI call timeout (10min) in the worker is the only boundary.
88
89
  const SWARM_STATE_FILE = 'swarm.json';
89
90
 
91
+ /** Safely access runHistory — older tasks may have `runSummaries` instead. */
92
+ function getRunHistory(ctx: { runHistory?: unknown; runSummaries?: unknown }): Array<Record<string, unknown>> {
93
+ return (ctx.runHistory ?? ctx.runSummaries ?? []) as Array<Record<string, unknown>>;
94
+ }
95
+
90
96
  // ---------------------------------------------------------------------------
91
97
  // SwarmController
92
98
  // ---------------------------------------------------------------------------
@@ -124,6 +130,12 @@ export class SwarmController {
124
130
  /** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
125
131
  private frozenPromptPrefix: string | null = null;
126
132
 
133
+ /** Background knowledge consolidation during idle periods. */
134
+ private dreamTask: DreamTask;
135
+
136
+ /** Counter for verification frequency gating (incremented per completed task). */
137
+ private verificationCounter = 0;
138
+
127
139
  // -----------------------------------------------------------------------
128
140
  // Singleton
129
141
  // -----------------------------------------------------------------------
@@ -154,6 +166,7 @@ export class SwarmController {
154
166
  this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
155
167
  this.instanceManager = new InstanceManager();
156
168
  this.profileStore = new ProfileStore(projectDir);
169
+ this.dreamTask = new DreamTask({ projectDir });
157
170
 
158
171
  // Load persisted state or create default
159
172
  this.state = this._loadState();
@@ -550,10 +563,11 @@ export class SwarmController {
550
563
  if (t.context.budgetExhausted) return false;
551
564
  // Skip tasks in rapid-loop cooldown: if last run ended recently AND was zero-work,
552
565
  // apply exponential backoff based on stagnation count
553
- if (t.context.runHistory.length > 0) {
554
- const lastRun = t.context.runHistory[t.context.runHistory.length - 1];
566
+ const taskRunHistory = getRunHistory(t.context);
567
+ if (taskRunHistory.length > 0) {
568
+ const lastRun = taskRunHistory[taskRunHistory.length - 1];
555
569
  if ('endedAt' in lastRun && lastRun.endedAt) {
556
- const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt).getTime()) / 1000;
570
+ const secsSinceLastRun = (Date.now() - new Date(lastRun.endedAt as string).getTime()) / 1000;
557
571
  const stag = t.context.stagnationCount;
558
572
  // Exponential backoff: 10s, 20s, 40s, 80s, 160s... based on stagnation
559
573
  const cooldownSecs = stag > 0 ? Math.min(10 * Math.pow(2, stag - 1), 300) : 0;
@@ -609,6 +623,7 @@ export class SwarmController {
609
623
  return !routableTasks.includes(t);
610
624
  }).length;
611
625
  _dl(`[dispatch] 0 routable from ${pendingTasks.length} open. parent=${pendingTasks.filter(t => t.isParent).length} budget=${pendingTasks.filter(t => t.context.budgetExhausted).length} deps-blocked=${depsBlocked}`);
626
+ await this._maybeDream();
612
627
  await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
613
628
  continue;
614
629
  }
@@ -650,9 +665,9 @@ export class SwarmController {
650
665
  complexity: t.complexity ?? 'moderate',
651
666
  assignedProfile: t.assignedProfile,
652
667
  context: {
653
- runHistory: t.context.runHistory.map((rp) => ({
654
- outcome: rp.outcome,
655
- botId: rp.botId,
668
+ runHistory: getRunHistory(t.context).map((rp) => ({
669
+ outcome: rp.outcome as string,
670
+ botId: rp.botId as string,
656
671
  })),
657
672
  },
658
673
  })),
@@ -791,6 +806,26 @@ export class SwarmController {
791
806
  }
792
807
  }
793
808
 
809
+ // -----------------------------------------------------------------------
810
+ // Idle-time knowledge consolidation
811
+ // -----------------------------------------------------------------------
812
+
813
+ private async _maybeDream(): Promise<void> {
814
+ if (!this.dreamTask.shouldRun()) return;
815
+ try {
816
+ const result = await this.dreamTask.consolidate();
817
+ if (result.staleEntriesCleaned + result.insightsConverted > 0) {
818
+ this.eventLog.emit({
819
+ type: 'dream-consolidation',
820
+ timestamp: Date.now(),
821
+ data: result as unknown as Record<string, unknown>,
822
+ });
823
+ }
824
+ } catch (err) {
825
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] dream-task failed:', err);
826
+ }
827
+ }
828
+
794
829
  // -----------------------------------------------------------------------
795
830
  // Task execution
796
831
  // -----------------------------------------------------------------------
@@ -815,10 +850,13 @@ export class SwarmController {
815
850
  if (shouldCompact(task, profile.preferences?.costStrategy)) {
816
851
  try {
817
852
  const { resolveModelTier } = await import('./behavior-defaults.js');
818
- const compactModel = resolveModelTier('fast', 'anthropic');
853
+ const { resolveProviderConfig } = await import('./agent-provider.js');
854
+ const detected = resolveProviderConfig('auto');
855
+ const providerType = detected.name;
856
+ const compactModel = detected.model ?? resolveModelTier('fast', providerType);
819
857
  const compactPInfo = {
820
- type: 'anthropic' as const,
821
- apiKey: process.env.ANTHROPIC_API_KEY,
858
+ type: providerType as ProviderInfo['type'],
859
+ apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
822
860
  model: compactModel,
823
861
  };
824
862
  const summary = await compactRunHistory(task, compactPInfo, callAI);
@@ -846,7 +884,7 @@ export class SwarmController {
846
884
  mode: task.context.files.length > 0 ? 'modify' : 'create',
847
885
  targets: task.context.files.length > 0 ? task.context.files : undefined,
848
886
  options: { autoApprove: true },
849
- runHistory: task.context.runHistory,
887
+ runHistory: getRunHistory(task.context),
850
888
  stagnationCount: task.context.stagnationCount,
851
889
  });
852
890
 
@@ -872,7 +910,12 @@ export class SwarmController {
872
910
  taskId,
873
911
  botId: workerId,
874
912
  config: { provider: 'auto' },
875
- params: { taskJson, projectDir: this.projectDir, behaviorJson },
913
+ params: {
914
+ taskJson,
915
+ projectDir: this.projectDir,
916
+ behaviorJson,
917
+ ...(this.frozenPromptPrefix ? { frozenPromptPrefix: this.frozenPromptPrefix } : {}),
918
+ },
876
919
  eventLog: runEventLog,
877
920
  });
878
921
 
@@ -949,6 +992,53 @@ export class SwarmController {
949
992
  }
950
993
  }
951
994
 
995
+ // Independent verification — runs after acceptance passes, before release.
996
+ // Uses a fresh LLM session with a potentially different model tier.
997
+ if (releaseStatus === 'done' && task) {
998
+ const behavior: import('./profile-types.js').ProfileBehavior | undefined =
999
+ profile.preferences?.behavior;
1000
+ const vConfig = behavior?.verification;
1001
+ if (vConfig?.enabled) {
1002
+ this.verificationCounter++;
1003
+ const shouldVerify = this.verificationCounter % vConfig.frequency === 0;
1004
+ if (shouldVerify) {
1005
+ try {
1006
+ const { runVerification } = await import('../node-types/verify-task.js');
1007
+ const { resolveProviderConfig } = await import('./agent-provider.js');
1008
+ const { resolveModelTier } = await import('./behavior-defaults.js');
1009
+ const detected = resolveProviderConfig('auto');
1010
+ const verifyModel = resolveModelTier(vConfig.tier, detected.name);
1011
+ const verifyResult = await runVerification(
1012
+ {
1013
+ taskTitle: task.title,
1014
+ taskDescription: task.description,
1015
+ filesCreated: runProgress.filesCreated,
1016
+ filesModified: runProgress.filesModified,
1017
+ summary: runProgress.summary,
1018
+ checks: runProgress.checks,
1019
+ },
1020
+ detected.name,
1021
+ verifyModel,
1022
+ process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
1023
+ );
1024
+ await this.taskStore.update(taskId, { lastVerification: verifyResult } as Record<string, unknown>);
1025
+ this.eventLog.emit({
1026
+ type: 'verification',
1027
+ timestamp: Date.now(),
1028
+ data: { taskId, runId, verdict: verifyResult.verdict, summary: verifyResult.summary, cost: verifyResult.cost } as unknown as Record<string, unknown>,
1029
+ });
1030
+ if (verifyResult.verdict === 'fail' && vConfig.reopenOnFail) {
1031
+ releaseStatus = 'open';
1032
+ console.log(`\x1b[33m[swarm] verification FAILED for task ${taskId}: ${verifyResult.summary}\x1b[0m`);
1033
+ }
1034
+ } catch (verifyErr) {
1035
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] verification failed:', verifyErr);
1036
+ // Verification failure is non-fatal — release as originally planned
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+
952
1042
  // Health checks — detect suspicious runs before releasing
953
1043
  if (tokensUsed === 0 && runProgress.outcome === 'completed') {
954
1044
  console.warn(`[swarm] HEALTH: zero-token completion task=${taskId} worker=${workerId} duration=${durationMs}ms`);
@@ -1002,7 +1092,7 @@ export class SwarmController {
1002
1092
  summary: runProgress.summary,
1003
1093
  filesModified: runProgress.filesModified,
1004
1094
  botId: workerId,
1005
- runCount: task.context.runHistory.length,
1095
+ runCount: getRunHistory(task.context).length,
1006
1096
  },
1007
1097
  });
1008
1098
  }
@@ -60,6 +60,22 @@ export interface AcceptanceResult {
60
60
  checkedAt: string;
61
61
  }
62
62
 
63
+ // ---------------------------------------------------------------------------
64
+ // Verification result — independent post-run review
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export type VerificationVerdict = 'pass' | 'fail' | 'inconclusive';
68
+
69
+ export interface VerificationResult {
70
+ verdict: VerificationVerdict;
71
+ summary: string;
72
+ issues: string[];
73
+ filesReviewed: string[];
74
+ verifiedAt: string;
75
+ /** Cost of the verification run in USD. */
76
+ cost: number;
77
+ }
78
+
63
79
  // ---------------------------------------------------------------------------
64
80
  // Task context
65
81
  // ---------------------------------------------------------------------------
@@ -105,6 +121,9 @@ export interface Task {
105
121
  acceptance?: AcceptanceCriteria;
106
122
  lastAcceptanceCheck?: AcceptanceResult;
107
123
 
124
+ // Verification
125
+ lastVerification?: VerificationResult;
126
+
108
127
  // Context
109
128
  context: TaskContext;
110
129
 
@@ -18,6 +18,7 @@ import { resolveToolsForTask } from '../bot/tool-registry.js';
18
18
  import { auditEmit } from '../bot/audit-logger.js';
19
19
  import { withRetry, getErrorGuidance } from '../bot/error-classifier.js';
20
20
  import { CostTracker } from '../bot/cost-tracker.js';
21
+ import { PostTurnHookRunner, CostCheckpointHook, ProgressReportHook } from '../bot/post-turn-hooks.js';
21
22
 
22
23
  // Clean up persistent sessions on process exit
23
24
  let cleanupRegistered = false;
@@ -238,13 +239,30 @@ export async function weaverAgentExecute(
238
239
  );
239
240
  const tools = WEAVER_TOOLS.filter(t => grantedToolNames.has(t.name));
240
241
 
242
+ // Set up post-turn hooks — cost checkpoint + progress reporting.
243
+ // CostCheckpointHook aborts the loop when cumulative cost exceeds budget.
244
+ // ProgressReportHook emits turn-progress events for UI updates.
245
+ const hookRunner = new PostTurnHookRunner();
246
+ const model = pInfo.model ?? 'claude-sonnet-4-6';
247
+ const budget = behavior?.budget;
248
+ if (budget != null && budget > 0) {
249
+ hookRunner.register(new CostCheckpointHook(budget, model));
250
+ }
251
+ hookRunner.register(new ProgressReportHook((event) => {
252
+ renderer.onStreamEvent?.({ type: 'text_delta', text: '' }); // keep renderer alive
253
+ if (process.env.WEAVER_VERBOSE) {
254
+ console.log(`[post-turn] ${event.type}: iter=${event.data.iteration} tools=${event.data.toolCallCount}`);
255
+ }
256
+ }));
257
+ const onTurnEnd = hookRunner.createCallback();
258
+
241
259
  const result = await withRetry(
242
260
  () => runAgentLoop(
243
261
  provider,
244
262
  tools,
245
263
  executor,
246
264
  [{ role: 'user', content: taskPrompt }],
247
- { systemPrompt, maxIterations: 15, onToolEvent, onStreamEvent },
265
+ { systemPrompt, maxIterations: 15, onToolEvent, onStreamEvent, onTurnEnd },
248
266
  ),
249
267
  {
250
268
  maxRetries: 3,
@@ -256,7 +274,6 @@ export async function weaverAgentExecute(
256
274
  );
257
275
 
258
276
  const usage = result.usage;
259
- const model = pInfo.model ?? 'claude-sonnet-4-6';
260
277
  const estimatedCost = CostTracker.estimateCost(model, {
261
278
  inputTokens: usage.promptTokens,
262
279
  outputTokens: usage.completionTokens,