@synergenius/flow-weaver-pack-weaver 0.9.193 → 0.9.196

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 (117) 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/assistant-core.js +2 -2
  6. package/dist/bot/assistant-core.js.map +1 -1
  7. package/dist/bot/behavior-defaults.d.ts +3 -1
  8. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  9. package/dist/bot/behavior-defaults.js +7 -0
  10. package/dist/bot/behavior-defaults.js.map +1 -1
  11. package/dist/bot/capability-registry.js +3 -3
  12. package/dist/bot/capability-registry.js.map +1 -1
  13. package/dist/bot/context-compactor.d.ts +35 -0
  14. package/dist/bot/context-compactor.d.ts.map +1 -0
  15. package/dist/bot/context-compactor.js +130 -0
  16. package/dist/bot/context-compactor.js.map +1 -0
  17. package/dist/bot/dream-task.d.ts +45 -0
  18. package/dist/bot/dream-task.d.ts.map +1 -0
  19. package/dist/bot/dream-task.js +125 -0
  20. package/dist/bot/dream-task.js.map +1 -0
  21. package/dist/bot/knowledge-store.d.ts +9 -0
  22. package/dist/bot/knowledge-store.d.ts.map +1 -1
  23. package/dist/bot/knowledge-store.js +21 -0
  24. package/dist/bot/knowledge-store.js.map +1 -1
  25. package/dist/bot/memory-extraction-worker.d.ts +14 -0
  26. package/dist/bot/memory-extraction-worker.d.ts.map +1 -0
  27. package/dist/bot/memory-extraction-worker.js +42 -0
  28. package/dist/bot/memory-extraction-worker.js.map +1 -0
  29. package/dist/bot/memory-extractor.d.ts +27 -0
  30. package/dist/bot/memory-extractor.d.ts.map +1 -0
  31. package/dist/bot/memory-extractor.js +155 -0
  32. package/dist/bot/memory-extractor.js.map +1 -0
  33. package/dist/bot/operations.d.ts +3 -1
  34. package/dist/bot/operations.d.ts.map +1 -1
  35. package/dist/bot/operations.js +3 -1
  36. package/dist/bot/operations.js.map +1 -1
  37. package/dist/bot/post-turn-hooks.d.ts +57 -0
  38. package/dist/bot/post-turn-hooks.d.ts.map +1 -0
  39. package/dist/bot/post-turn-hooks.js +108 -0
  40. package/dist/bot/post-turn-hooks.js.map +1 -0
  41. package/dist/bot/profile-types.d.ts +16 -0
  42. package/dist/bot/profile-types.d.ts.map +1 -1
  43. package/dist/bot/swarm-controller.d.ts +7 -0
  44. package/dist/bot/swarm-controller.d.ts.map +1 -1
  45. package/dist/bot/swarm-controller.js +121 -1
  46. package/dist/bot/swarm-controller.js.map +1 -1
  47. package/dist/bot/task-prompt-builder.js +35 -21
  48. package/dist/bot/task-prompt-builder.js.map +1 -1
  49. package/dist/bot/task-types.d.ts +13 -0
  50. package/dist/bot/task-types.d.ts.map +1 -1
  51. package/dist/bot/tool-registry.d.ts +13 -0
  52. package/dist/bot/tool-registry.d.ts.map +1 -1
  53. package/dist/bot/tool-registry.js +80 -0
  54. package/dist/bot/tool-registry.js.map +1 -1
  55. package/dist/bot/types.d.ts +2 -0
  56. package/dist/bot/types.d.ts.map +1 -1
  57. package/dist/node-types/agent-execute.d.ts.map +1 -1
  58. package/dist/node-types/agent-execute.js +38 -17
  59. package/dist/node-types/agent-execute.js.map +1 -1
  60. package/dist/node-types/build-context.d.ts +4 -3
  61. package/dist/node-types/build-context.d.ts.map +1 -1
  62. package/dist/node-types/build-context.js +37 -6
  63. package/dist/node-types/build-context.js.map +1 -1
  64. package/dist/node-types/receive-task.d.ts +2 -1
  65. package/dist/node-types/receive-task.d.ts.map +1 -1
  66. package/dist/node-types/receive-task.js +4 -1
  67. package/dist/node-types/receive-task.js.map +1 -1
  68. package/dist/node-types/review-result.d.ts +9 -0
  69. package/dist/node-types/review-result.d.ts.map +1 -1
  70. package/dist/node-types/review-result.js +20 -5
  71. package/dist/node-types/review-result.js.map +1 -1
  72. package/dist/node-types/verify-task.d.ts +22 -0
  73. package/dist/node-types/verify-task.d.ts.map +1 -0
  74. package/dist/node-types/verify-task.js +143 -0
  75. package/dist/node-types/verify-task.js.map +1 -0
  76. package/dist/ui/capability-editor.js +3 -3
  77. package/dist/ui/profile-editor.js +3 -3
  78. package/dist/ui/swarm-dashboard.js +3 -3
  79. package/dist/workflows/weaver-agent.d.ts +3 -3
  80. package/dist/workflows/weaver-agent.d.ts.map +1 -1
  81. package/dist/workflows/weaver-agent.js +267 -18
  82. package/dist/workflows/weaver-agent.js.map +1 -1
  83. package/dist/workflows/weaver-bot-batch.d.ts +3 -3
  84. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  85. package/dist/workflows/weaver-bot-batch.js +280 -24
  86. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  87. package/dist/workflows/weaver-bot.d.ts +2 -0
  88. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  89. package/dist/workflows/weaver-bot.js +15 -10
  90. package/dist/workflows/weaver-bot.js.map +1 -1
  91. package/flowweaver.manifest.json +1 -1
  92. package/package.json +3 -3
  93. package/src/bot/ai-client.ts +54 -0
  94. package/src/bot/assistant-core.ts +2 -2
  95. package/src/bot/behavior-defaults.ts +9 -1
  96. package/src/bot/capability-registry.ts +3 -3
  97. package/src/bot/context-compactor.ts +147 -0
  98. package/src/bot/dream-task.ts +167 -0
  99. package/src/bot/knowledge-store.ts +27 -0
  100. package/src/bot/memory-extraction-worker.ts +58 -0
  101. package/src/bot/memory-extractor.ts +213 -0
  102. package/src/bot/operations.ts +3 -1
  103. package/src/bot/post-turn-hooks.ts +137 -0
  104. package/src/bot/profile-types.ts +17 -0
  105. package/src/bot/swarm-controller.ts +129 -2
  106. package/src/bot/task-prompt-builder.ts +37 -21
  107. package/src/bot/task-types.ts +21 -0
  108. package/src/bot/tool-registry.ts +89 -0
  109. package/src/bot/types.ts +2 -0
  110. package/src/node-types/agent-execute.ts +44 -17
  111. package/src/node-types/build-context.ts +45 -7
  112. package/src/node-types/receive-task.ts +3 -0
  113. package/src/node-types/review-result.ts +22 -5
  114. package/src/node-types/verify-task.ts +181 -0
  115. package/src/workflows/weaver-agent.ts +429 -18
  116. package/src/workflows/weaver-bot-batch.ts +443 -24
  117. package/src/workflows/weaver-bot.ts +16 -11
@@ -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,7 +32,11 @@ 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
+ import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
37
+ import { shouldCompact, compactRunHistory } from './context-compactor.js';
38
+ import { DreamTask } from './dream-task.js';
39
+ import { callAI } from './ai-client.js';
36
40
 
37
41
  // ---------------------------------------------------------------------------
38
42
  // Types
@@ -118,6 +122,15 @@ export class SwarmController {
118
122
  /** Last emitted dispatch-filter-summary JSON (for dedup / throttling). */
119
123
  private lastFilterSummaryJson: string | null = null;
120
124
 
125
+ /** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
126
+ private frozenPromptPrefix: string | null = null;
127
+
128
+ /** Background knowledge consolidation during idle periods. */
129
+ private dreamTask: DreamTask;
130
+
131
+ /** Counter for verification frequency gating (incremented per completed task). */
132
+ private verificationCounter = 0;
133
+
121
134
  // -----------------------------------------------------------------------
122
135
  // Singleton
123
136
  // -----------------------------------------------------------------------
@@ -148,6 +161,7 @@ export class SwarmController {
148
161
  this.orchestrator = new Orchestrator({ aiRouter: new AIRouterImpl(projectDir) });
149
162
  this.instanceManager = new InstanceManager();
150
163
  this.profileStore = new ProfileStore(projectDir);
164
+ this.dreamTask = new DreamTask({ projectDir });
151
165
 
152
166
  // Load persisted state or create default
153
167
  this.state = this._loadState();
@@ -210,6 +224,16 @@ export class SwarmController {
210
224
  this.state.startedAt = new Date().toISOString();
211
225
  this._persist();
212
226
 
227
+ // Freeze the stable system prompt prefix for cross-slot cache sharing.
228
+ // All bot slots will use this identical prefix; only the per-task suffix varies.
229
+ try {
230
+ const { buildSystemPrompt } = await import('./system-prompt.js');
231
+ this.frozenPromptPrefix = await buildSystemPrompt();
232
+ } catch (err) {
233
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] failed to freeze system prompt prefix:', err);
234
+ this.frozenPromptPrefix = null;
235
+ }
236
+
213
237
  console.log(`\x1b[36m[swarm] started (pack-weaver v${PACK_VERSION})\x1b[0m`);
214
238
  this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now(), data: { packVersion: PACK_VERSION } });
215
239
 
@@ -593,6 +617,7 @@ export class SwarmController {
593
617
  return !routableTasks.includes(t);
594
618
  }).length;
595
619
  _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}`);
620
+ await this._maybeDream();
596
621
  await this._sleep(DISPATCH_LOOP_SLEEP_MS, signal);
597
622
  continue;
598
623
  }
@@ -775,6 +800,26 @@ export class SwarmController {
775
800
  }
776
801
  }
777
802
 
803
+ // -----------------------------------------------------------------------
804
+ // Idle-time knowledge consolidation
805
+ // -----------------------------------------------------------------------
806
+
807
+ private async _maybeDream(): Promise<void> {
808
+ if (!this.dreamTask.shouldRun()) return;
809
+ try {
810
+ const result = await this.dreamTask.consolidate();
811
+ if (result.staleEntriesCleaned + result.insightsConverted > 0) {
812
+ this.eventLog.emit({
813
+ type: 'dream-consolidation',
814
+ timestamp: Date.now(),
815
+ data: result as unknown as Record<string, unknown>,
816
+ });
817
+ }
818
+ } catch (err) {
819
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] dream-task failed:', err);
820
+ }
821
+ }
822
+
778
823
  // -----------------------------------------------------------------------
779
824
  // Task execution
780
825
  // -----------------------------------------------------------------------
@@ -793,6 +838,31 @@ export class SwarmController {
793
838
  const task = await this.taskStore.get(taskId);
794
839
  if (!task) throw new Error(`Task not found: ${taskId}`);
795
840
 
841
+ // LLM-based context compaction — produces a structured summary of all runs
842
+ // when the task has enough history. The summary replaces verbose per-run
843
+ // sections in the prompt, preserving semantic signal.
844
+ if (shouldCompact(task, profile.preferences?.costStrategy)) {
845
+ try {
846
+ const { resolveModelTier } = await import('./behavior-defaults.js');
847
+ const { resolveProviderConfig } = await import('./agent-provider.js');
848
+ const detected = resolveProviderConfig('auto');
849
+ const providerType = detected.name;
850
+ const compactModel = detected.model ?? resolveModelTier('fast', providerType);
851
+ const compactPInfo = {
852
+ type: providerType as ProviderInfo['type'],
853
+ apiKey: process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
854
+ model: compactModel,
855
+ };
856
+ const summary = await compactRunHistory(task, compactPInfo, callAI);
857
+ if (summary) {
858
+ task.context.compactedSummary = summary;
859
+ await this.taskStore.update(taskId, { context: task.context });
860
+ }
861
+ } catch {
862
+ // Compaction failure is non-fatal — prompt builder falls back to context decay
863
+ }
864
+ }
865
+
796
866
  // Build prompt from task context
797
867
  const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
798
868
  const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
@@ -834,7 +904,12 @@ export class SwarmController {
834
904
  taskId,
835
905
  botId: workerId,
836
906
  config: { provider: 'auto' },
837
- params: { taskJson, projectDir: this.projectDir, behaviorJson },
907
+ params: {
908
+ taskJson,
909
+ projectDir: this.projectDir,
910
+ behaviorJson,
911
+ ...(this.frozenPromptPrefix ? { frozenPromptPrefix: this.frozenPromptPrefix } : {}),
912
+ },
838
913
  eventLog: runEventLog,
839
914
  });
840
915
 
@@ -911,6 +986,53 @@ export class SwarmController {
911
986
  }
912
987
  }
913
988
 
989
+ // Independent verification — runs after acceptance passes, before release.
990
+ // Uses a fresh LLM session with a potentially different model tier.
991
+ if (releaseStatus === 'done' && task) {
992
+ const behavior: import('./profile-types.js').ProfileBehavior | undefined =
993
+ profile.preferences?.behavior;
994
+ const vConfig = behavior?.verification;
995
+ if (vConfig?.enabled) {
996
+ this.verificationCounter++;
997
+ const shouldVerify = this.verificationCounter % vConfig.frequency === 0;
998
+ if (shouldVerify) {
999
+ try {
1000
+ const { runVerification } = await import('../node-types/verify-task.js');
1001
+ const { resolveProviderConfig } = await import('./agent-provider.js');
1002
+ const { resolveModelTier } = await import('./behavior-defaults.js');
1003
+ const detected = resolveProviderConfig('auto');
1004
+ const verifyModel = resolveModelTier(vConfig.tier, detected.name);
1005
+ const verifyResult = await runVerification(
1006
+ {
1007
+ taskTitle: task.title,
1008
+ taskDescription: task.description,
1009
+ filesCreated: runProgress.filesCreated,
1010
+ filesModified: runProgress.filesModified,
1011
+ summary: runProgress.summary,
1012
+ checks: runProgress.checks,
1013
+ },
1014
+ detected.name,
1015
+ verifyModel,
1016
+ process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
1017
+ );
1018
+ await this.taskStore.update(taskId, { lastVerification: verifyResult } as Record<string, unknown>);
1019
+ this.eventLog.emit({
1020
+ type: 'verification',
1021
+ timestamp: Date.now(),
1022
+ data: { taskId, runId, verdict: verifyResult.verdict, summary: verifyResult.summary, cost: verifyResult.cost } as unknown as Record<string, unknown>,
1023
+ });
1024
+ if (verifyResult.verdict === 'fail' && vConfig.reopenOnFail) {
1025
+ releaseStatus = 'open';
1026
+ console.log(`\x1b[33m[swarm] verification FAILED for task ${taskId}: ${verifyResult.summary}\x1b[0m`);
1027
+ }
1028
+ } catch (verifyErr) {
1029
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] verification failed:', verifyErr);
1030
+ // Verification failure is non-fatal — release as originally planned
1031
+ }
1032
+ }
1033
+ }
1034
+ }
1035
+
914
1036
  // Health checks — detect suspicious runs before releasing
915
1037
  if (tokensUsed === 0 && runProgress.outcome === 'completed') {
916
1038
  console.warn(`[swarm] HEALTH: zero-token completion task=${taskId} worker=${workerId} duration=${durationMs}ms`);
@@ -925,6 +1047,11 @@ export class SwarmController {
925
1047
 
926
1048
  await this.taskStore.release(taskId, releaseStatus, runProgress);
927
1049
 
1050
+ // Fire-and-forget memory extraction — persists project facts for future runs
1051
+ if (task) {
1052
+ scheduleMemoryExtraction(this.projectDir, task, runProgress);
1053
+ }
1054
+
928
1055
  // Record token usage
929
1056
  this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
930
1057
 
@@ -65,10 +65,13 @@ function buildFull(
65
65
  }
66
66
 
67
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.
68
+ // If a compacted summary exists (from LLM compaction after 3+ runs),
69
+ // use it instead of the per-run sections it preserves semantic signal.
70
+ if (task.context.compactedSummary) {
71
+ sections.push(`### Execution History (Compacted)\n${task.context.compactedSummary}`);
72
+ }
70
73
 
71
- // 2.3.2: Last acceptance check result
74
+ // 2.3.2: Last acceptance check result (always shown, even with compacted summary)
72
75
  if (task.lastAcceptanceCheck) {
73
76
  const ac = task.lastAcceptanceCheck;
74
77
  const checkLines = ac.results
@@ -78,6 +81,7 @@ function buildFull(
78
81
  }
79
82
 
80
83
  // 2.3.3: Continue from last run's remaining work
84
+ // (always shown — most recent actionable data, even with compacted summary)
81
85
  const lastRun = task.context.runHistory.length > 0
82
86
  ? task.context.runHistory[task.context.runHistory.length - 1]
83
87
  : undefined;
@@ -90,18 +94,21 @@ function buildFull(
90
94
  sections.push(`### Previous Run Blocked By\n${(lastRun.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
91
95
  }
92
96
 
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
- }
97
+ // Per-run sections — skipped when compacted summary exists (it covers this info)
98
+ if (!task.context.compactedSummary) {
99
+ // Last run summary (one run only, not full history)
100
+ if (lastRun && 'summary' in lastRun) {
101
+ sections.push(`### Last Run\nOutcome: ${lastRun.outcome} | ${lastRun.summary}`);
102
+ }
97
103
 
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`;
104
+ // Run count + stagnation
105
+ if (task.context.runHistory.length > 0) {
106
+ let meta = `Total runs: ${task.context.runHistory.length}`;
107
+ if (task.context.stagnationCount > 0) {
108
+ meta += ` | Stagnation: ${task.context.stagnationCount} run(s) with no new changes — try a different approach`;
109
+ }
110
+ sections.push(`### Run History\n${meta}`);
103
111
  }
104
- sections.push(`### Run History\n${meta}`);
105
112
  }
106
113
 
107
114
  // Directive: read the workspace, don't rely on stale context
@@ -226,6 +233,11 @@ function buildWithTruncation(
226
233
  sections.push(`### Relevant Files\n${task.context.files.join('\n')}`);
227
234
  }
228
235
 
236
+ // Compacted summary (same guard as buildFull)
237
+ if (task.context.compactedSummary) {
238
+ sections.push(`### Execution History (Compacted)\n${task.context.compactedSummary}`);
239
+ }
240
+
229
241
  // Context decay: last acceptance check + last run only
230
242
  if (task.lastAcceptanceCheck) {
231
243
  const ac = task.lastAcceptanceCheck;
@@ -244,16 +256,20 @@ function buildWithTruncation(
244
256
  if (lastRunT && 'blockers' in lastRunT && Array.isArray(lastRunT.blockers) && lastRunT.blockers.length > 0) {
245
257
  sections.push(`### Previous Run Blocked By\n${(lastRunT.blockers as string[]).map((b: string) => `- ${b}`).join('\n')}`);
246
258
  }
247
- if (lastRunT && 'summary' in lastRunT) {
248
- sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
249
- }
250
259
 
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`;
260
+ // Per-run sections skipped when compacted summary exists
261
+ if (!task.context.compactedSummary) {
262
+ if (lastRunT && 'summary' in lastRunT) {
263
+ sections.push(`### Last Run\nOutcome: ${lastRunT.outcome} | ${lastRunT.summary}`);
264
+ }
265
+
266
+ if (task.context.runHistory.length > 0) {
267
+ let meta = `Total runs: ${task.context.runHistory.length}`;
268
+ if (task.context.stagnationCount > 0) {
269
+ meta += ` | Stagnation: ${task.context.stagnationCount} — try a different approach`;
270
+ }
271
+ sections.push(`### Run History\n${meta}`);
255
272
  }
256
- sections.push(`### Run History\n${meta}`);
257
273
  }
258
274
 
259
275
  // Directive: read the workspace, don't rely on stale context
@@ -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
  // ---------------------------------------------------------------------------
@@ -71,6 +87,8 @@ export interface TaskContext {
71
87
  stagnationCount: number;
72
88
  budgetExhausted?: boolean;
73
89
  projectBrief?: string;
90
+ /** LLM-generated summary of all runs, replacing verbose run history in prompts. */
91
+ compactedSummary?: string;
74
92
  }
75
93
 
76
94
  // ---------------------------------------------------------------------------
@@ -103,6 +121,9 @@ export interface Task {
103
121
  acceptance?: AcceptanceCriteria;
104
122
  lastAcceptanceCheck?: AcceptanceResult;
105
123
 
124
+ // Verification
125
+ lastVerification?: VerificationResult;
126
+
106
127
  // Context
107
128
  context: TaskContext;
108
129
 
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { ToolDefinition } from '@synergenius/flow-weaver/agent';
8
+ import { getCapability } from './capability-registry.js';
8
9
 
9
10
  export interface WeaverTool extends ToolDefinition {
10
11
  verboseOutput?: boolean;
@@ -575,6 +576,94 @@ export const BOT_TOOLS: ToolDefinition[] = ALL_TOOLS.filter(t => t.contexts.incl
575
576
  export const ASSISTANT_TOOLS: ToolDefinition[] = ALL_TOOLS.filter(t => t.contexts.includes('assistant'));
576
577
  export const VERBOSE_TOOL_NAMES = new Set(ALL_TOOLS.filter(t => t.verboseOutput).map(t => t.name));
577
578
 
579
+ // ── Mode-based tool filtering ───────────────────────────────────────
580
+
581
+ /** Core tools included in every mode regardless of profile. */
582
+ const CORE_TOOLS = new Set([
583
+ 'read_file', 'list_files', 'run_shell', 'validate', 'learn', 'recall',
584
+ ]);
585
+
586
+ /** Tools allowed per task mode. Keys match task.mode values. */
587
+ const MODE_TOOLS: Record<string, Set<string>> = {
588
+ create: new Set([
589
+ 'read_file', 'list_files', 'write_file', 'patch_file',
590
+ 'run_shell', 'validate', 'tsc_check', 'run_tests',
591
+ 'learn', 'recall',
592
+ ]),
593
+ modify: new Set([
594
+ 'read_file', 'list_files', 'patch_file',
595
+ 'run_shell', 'validate', 'tsc_check', 'run_tests',
596
+ 'learn', 'recall',
597
+ ]),
598
+ read: new Set([
599
+ 'read_file', 'list_files', 'run_shell', 'validate',
600
+ 'learn', 'recall',
601
+ ]),
602
+ batch: new Set([
603
+ 'read_file', 'list_files', 'write_file', 'patch_file',
604
+ 'run_shell', 'validate', 'tsc_check', 'run_tests',
605
+ 'learn', 'recall',
606
+ ]),
607
+ };
608
+
609
+ /**
610
+ * Resolve which tools a bot should have for a given task and profile.
611
+ *
612
+ * Uses the task mode to select a base tool pool, then intersects with
613
+ * profile-granted tools (from capabilities). Core tools are always included.
614
+ *
615
+ * @param task - Task with mode and optional capabilities
616
+ * @param capabilities - Profile capability names (e.g., ['role-developer', 'file-ops', 'shell'])
617
+ * @returns Set of tool names the bot should receive
618
+ */
619
+ export function resolveToolsForTask(
620
+ task: { mode?: string },
621
+ capabilities?: string[],
622
+ ): Set<string> {
623
+ // Start with the mode-based pool (default to 'create' = full set)
624
+ const modePool = MODE_TOOLS[task.mode ?? 'create'] ?? MODE_TOOLS.create;
625
+
626
+ // If capabilities are specified, compute the capability-granted tools
627
+ if (capabilities && capabilities.length > 0) {
628
+ const capTools = new Set<string>();
629
+ for (const capName of capabilities) {
630
+ const cap = getCapability(capName);
631
+ if (cap?.tools) {
632
+ for (const tool of cap.tools) capTools.add(tool);
633
+ }
634
+ }
635
+
636
+ // Build the tool set in two steps:
637
+ // 1. Mode-restricted tools: must be in BOTH mode pool AND capability set (or core).
638
+ // This ensures modify mode excludes write_file even if the capability grants it.
639
+ // 2. Role-specific tools: tools granted by capabilities but not present in ANY
640
+ // mode pool (e.g., task_create, ask_user). These are additive — the capability
641
+ // is the sole authority for them.
642
+ const allModeTools = new Set<string>();
643
+ for (const pool of Object.values(MODE_TOOLS)) {
644
+ for (const t of pool) allModeTools.add(t);
645
+ }
646
+
647
+ const result = new Set<string>();
648
+ // Step 1: mode-restricted intersection
649
+ for (const tool of modePool) {
650
+ if (capTools.has(tool) || CORE_TOOLS.has(tool)) {
651
+ result.add(tool);
652
+ }
653
+ }
654
+ // Step 2: role-specific tools (not in any mode pool)
655
+ for (const tool of capTools) {
656
+ if (!allModeTools.has(tool)) {
657
+ result.add(tool);
658
+ }
659
+ }
660
+ return result;
661
+ }
662
+
663
+ // No capability restriction — use mode pool as-is
664
+ return new Set(modePool);
665
+ }
666
+
578
667
  /**
579
668
  * Generate a prompt section grouping assistant tools by category.
580
669
  */
package/src/bot/types.ts CHANGED
@@ -642,6 +642,8 @@ export interface WeaverContext {
642
642
  allValid?: boolean;
643
643
  gitResultJson?: string;
644
644
  reviewJson?: string;
645
+ /** Frozen system prompt prefix from swarm controller for cross-slot cache sharing. */
646
+ frozenPromptPrefix?: string;
645
647
  }
646
648
 
647
649
  export interface GenesisContext {