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

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 (68) hide show
  1. package/dist/bot/assistant-core.js +2 -2
  2. package/dist/bot/assistant-core.js.map +1 -1
  3. package/dist/bot/capability-registry.js +2 -2
  4. package/dist/bot/capability-registry.js.map +1 -1
  5. package/dist/bot/context-compactor.d.ts +35 -0
  6. package/dist/bot/context-compactor.d.ts.map +1 -0
  7. package/dist/bot/context-compactor.js +130 -0
  8. package/dist/bot/context-compactor.js.map +1 -0
  9. package/dist/bot/memory-extraction-worker.d.ts +14 -0
  10. package/dist/bot/memory-extraction-worker.d.ts.map +1 -0
  11. package/dist/bot/memory-extraction-worker.js +42 -0
  12. package/dist/bot/memory-extraction-worker.js.map +1 -0
  13. package/dist/bot/memory-extractor.d.ts +27 -0
  14. package/dist/bot/memory-extractor.d.ts.map +1 -0
  15. package/dist/bot/memory-extractor.js +155 -0
  16. package/dist/bot/memory-extractor.js.map +1 -0
  17. package/dist/bot/operations.d.ts +3 -1
  18. package/dist/bot/operations.d.ts.map +1 -1
  19. package/dist/bot/operations.js +3 -1
  20. package/dist/bot/operations.js.map +1 -1
  21. package/dist/bot/swarm-controller.d.ts +2 -0
  22. package/dist/bot/swarm-controller.d.ts.map +1 -1
  23. package/dist/bot/swarm-controller.js +42 -0
  24. package/dist/bot/swarm-controller.js.map +1 -1
  25. package/dist/bot/task-prompt-builder.js +35 -21
  26. package/dist/bot/task-prompt-builder.js.map +1 -1
  27. package/dist/bot/task-types.d.ts +2 -0
  28. package/dist/bot/task-types.d.ts.map +1 -1
  29. package/dist/bot/tool-registry.d.ts +13 -0
  30. package/dist/bot/tool-registry.d.ts.map +1 -1
  31. package/dist/bot/tool-registry.js +80 -0
  32. package/dist/bot/tool-registry.js.map +1 -1
  33. package/dist/bot/types.d.ts +2 -0
  34. package/dist/bot/types.d.ts.map +1 -1
  35. package/dist/node-types/agent-execute.d.ts.map +1 -1
  36. package/dist/node-types/agent-execute.js +20 -15
  37. package/dist/node-types/agent-execute.js.map +1 -1
  38. package/dist/node-types/build-context.d.ts.map +1 -1
  39. package/dist/node-types/build-context.js +18 -3
  40. package/dist/node-types/build-context.js.map +1 -1
  41. package/dist/node-types/receive-task.d.ts +2 -1
  42. package/dist/node-types/receive-task.d.ts.map +1 -1
  43. package/dist/node-types/receive-task.js +4 -1
  44. package/dist/node-types/receive-task.js.map +1 -1
  45. package/dist/node-types/review-result.d.ts +9 -0
  46. package/dist/node-types/review-result.d.ts.map +1 -1
  47. package/dist/node-types/review-result.js +20 -5
  48. package/dist/node-types/review-result.js.map +1 -1
  49. package/dist/ui/capability-editor.js +2 -2
  50. package/dist/ui/profile-editor.js +2 -2
  51. package/dist/ui/swarm-dashboard.js +2 -2
  52. package/flowweaver.manifest.json +1 -1
  53. package/package.json +2 -2
  54. package/src/bot/assistant-core.ts +2 -2
  55. package/src/bot/capability-registry.ts +2 -2
  56. package/src/bot/context-compactor.ts +147 -0
  57. package/src/bot/memory-extraction-worker.ts +58 -0
  58. package/src/bot/memory-extractor.ts +213 -0
  59. package/src/bot/operations.ts +3 -1
  60. package/src/bot/swarm-controller.ts +43 -0
  61. package/src/bot/task-prompt-builder.ts +37 -21
  62. package/src/bot/task-types.ts +2 -0
  63. package/src/bot/tool-registry.ts +89 -0
  64. package/src/bot/types.ts +2 -0
  65. package/src/node-types/agent-execute.ts +25 -15
  66. package/src/node-types/build-context.ts +19 -3
  67. package/src/node-types/receive-task.ts +3 -0
  68. package/src/node-types/review-result.ts +22 -5
@@ -33,6 +33,9 @@ import type { BotProfile, BotInstance, OrchestratorInput, OrchestratorDecision,
33
33
  import { buildDefaultBehavior, adjustBehaviorForComplexity } from './behavior-defaults.js';
34
34
  import type { Task, RunProgress } from './task-types.js';
35
35
  import type { WorkflowResult } from './types.js';
36
+ import { scheduleMemoryExtraction } from './memory-extraction-worker.js';
37
+ import { shouldCompact, compactRunHistory } from './context-compactor.js';
38
+ import { callAI } from './ai-client.js';
36
39
 
37
40
  // ---------------------------------------------------------------------------
38
41
  // Types
@@ -118,6 +121,9 @@ export class SwarmController {
118
121
  /** Last emitted dispatch-filter-summary JSON (for dedup / throttling). */
119
122
  private lastFilterSummaryJson: string | null = null;
120
123
 
124
+ /** Frozen system prompt prefix for cross-slot Anthropic cache sharing. */
125
+ private frozenPromptPrefix: string | null = null;
126
+
121
127
  // -----------------------------------------------------------------------
122
128
  // Singleton
123
129
  // -----------------------------------------------------------------------
@@ -210,6 +216,16 @@ export class SwarmController {
210
216
  this.state.startedAt = new Date().toISOString();
211
217
  this._persist();
212
218
 
219
+ // Freeze the stable system prompt prefix for cross-slot cache sharing.
220
+ // All bot slots will use this identical prefix; only the per-task suffix varies.
221
+ try {
222
+ const { buildSystemPrompt } = await import('./system-prompt.js');
223
+ this.frozenPromptPrefix = await buildSystemPrompt();
224
+ } catch (err) {
225
+ if (process.env.WEAVER_VERBOSE) console.warn('[swarm] failed to freeze system prompt prefix:', err);
226
+ this.frozenPromptPrefix = null;
227
+ }
228
+
213
229
  console.log(`\x1b[36m[swarm] started (pack-weaver v${PACK_VERSION})\x1b[0m`);
214
230
  this.eventLog.emit({ type: 'swarm-started', timestamp: Date.now(), data: { packVersion: PACK_VERSION } });
215
231
 
@@ -793,6 +809,28 @@ export class SwarmController {
793
809
  const task = await this.taskStore.get(taskId);
794
810
  if (!task) throw new Error(`Task not found: ${taskId}`);
795
811
 
812
+ // LLM-based context compaction — produces a structured summary of all runs
813
+ // when the task has enough history. The summary replaces verbose per-run
814
+ // sections in the prompt, preserving semantic signal.
815
+ if (shouldCompact(task, profile.preferences?.costStrategy)) {
816
+ try {
817
+ const { resolveModelTier } = await import('./behavior-defaults.js');
818
+ const compactModel = resolveModelTier('fast', 'anthropic');
819
+ const compactPInfo = {
820
+ type: 'anthropic' as const,
821
+ apiKey: process.env.ANTHROPIC_API_KEY,
822
+ model: compactModel,
823
+ };
824
+ const summary = await compactRunHistory(task, compactPInfo, callAI);
825
+ if (summary) {
826
+ task.context.compactedSummary = summary;
827
+ await this.taskStore.update(taskId, { context: task.context });
828
+ }
829
+ } catch {
830
+ // Compaction failure is non-fatal — prompt builder falls back to context decay
831
+ }
832
+ }
833
+
796
834
  // Build prompt from task context
797
835
  const parentTask = task.parentId ? await this.taskStore.get(task.parentId) : null;
798
836
  const siblingTasks = task.parentId ? await this.taskStore.getSubtasks(task.parentId) : [];
@@ -925,6 +963,11 @@ export class SwarmController {
925
963
 
926
964
  await this.taskStore.release(taskId, releaseStatus, runProgress);
927
965
 
966
+ // Fire-and-forget memory extraction — persists project facts for future runs
967
+ if (task) {
968
+ scheduleMemoryExtraction(this.projectDir, task, runProgress);
969
+ }
970
+
928
971
  // Record token usage
929
972
  this.recordTokenUsage(workerId, taskId, tokensUsed, costUsed);
930
973
 
@@ -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
@@ -71,6 +71,8 @@ export interface TaskContext {
71
71
  stagnationCount: number;
72
72
  budgetExhausted?: boolean;
73
73
  projectBrief?: string;
74
+ /** LLM-generated summary of all runs, replacing verbose run history in prompts. */
75
+ compactedSummary?: string;
74
76
  }
75
77
 
76
78
  // ---------------------------------------------------------------------------
@@ -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 {
@@ -4,14 +4,17 @@ import {
4
4
  createAnthropicProvider,
5
5
  getOrCreateCliSession,
6
6
  killAllCliSessions,
7
+ joinSplitPrompt,
7
8
  type AgentProvider,
8
9
  type AgentMessage,
9
10
  type ToolDefinition,
10
11
  type StreamEvent,
11
12
  type StreamOptions,
12
13
  type ToolEvent,
14
+ type SplitPrompt,
13
15
  } from '@synergenius/flow-weaver/agent';
14
16
  import { WEAVER_TOOLS, createWeaverExecutor } from '../bot/weaver-tools.js';
17
+ import { resolveToolsForTask } from '../bot/tool-registry.js';
15
18
  import { auditEmit } from '../bot/audit-logger.js';
16
19
  import { withRetry, getErrorGuidance } from '../bot/error-classifier.js';
17
20
  import { CostTracker } from '../bot/cost-tracker.js';
@@ -64,15 +67,16 @@ class CliSessionProvider implements AgentProvider {
64
67
 
65
68
  if (!prompt) return;
66
69
 
67
- // Only pass system prompt on the first call
68
- const systemPrompt = this.sentCount <= messages.length ? options?.systemPrompt : undefined;
70
+ // Only pass system prompt on the first call — CLI sessions accept a string
71
+ const splitPrompt = this.sentCount <= messages.length ? options?.systemPrompt : undefined;
72
+ const systemPromptStr = splitPrompt ? joinSplitPrompt(splitPrompt) : undefined;
69
73
 
70
74
  // Forward usage events to the runner's CostTracker via the global callback.
71
75
  // This bridges CLI session usage → runner cost tracking → swarm budget enforcement.
72
76
  const usageCb = (globalThis as Record<string, unknown>).__fw_ai_usage_callback__ as
73
77
  ((model: string, usage: { inputTokens: number; outputTokens: number }) => void) | undefined;
74
78
 
75
- for await (const event of this.session.send(prompt, systemPrompt)) {
79
+ for await (const event of this.session.send(prompt, systemPromptStr)) {
76
80
  if (event.type === 'usage' && usageCb) {
77
81
  usageCb(this.model, {
78
82
  inputTokens: event.promptTokens,
@@ -150,21 +154,24 @@ export async function weaverAgentExecute(
150
154
  return { onSuccess: false, onFailure: true, ctx: JSON.stringify(context) };
151
155
  }
152
156
 
153
- // Build system prompt
154
- let systemPrompt: string;
157
+ // Build system prompt as SplitPrompt — prefix is stable (cacheable),
158
+ // suffix is per-task (contextBundle, project plan).
159
+ // If frozenPromptPrefix is available from the swarm controller, use it
160
+ // to ensure all bot slots share the same cached prefix bytes.
161
+ let systemPrompt: SplitPrompt;
155
162
  try {
156
163
  const mod = await import('../bot/system-prompt.js');
157
- const basePrompt = await mod.buildSystemPrompt();
164
+ const prefix = context.frozenPromptPrefix ?? await mod.buildSystemPrompt();
158
165
  let cliCommands: { name: string; description: string; botCompatible?: boolean; options?: { flags: string; arg?: string; description: string }[] }[] = [];
159
166
  try {
160
167
  const docMeta = await import('@synergenius/flow-weaver/doc-metadata');
161
168
  cliCommands = docMeta.CLI_COMMANDS ?? [];
162
169
  } catch (err) { if (process.env.WEAVER_VERBOSE) console.error('[agent-execute] doc-metadata unavailable (older fw):', err); }
163
- const botPrompt = mod.buildBotSystemPrompt(context.contextBundle, cliCommands, projectDir);
164
- systemPrompt = basePrompt + '\n\n' + botPrompt;
170
+ const suffix = mod.buildBotSystemPrompt(context.contextBundle, cliCommands, projectDir);
171
+ systemPrompt = { prefix, suffix };
165
172
  } catch (err) {
166
173
  if (process.env.WEAVER_VERBOSE) console.error('[agent-execute] system prompt build failed, using fallback:', err);
167
- systemPrompt = 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks.';
174
+ systemPrompt = { prefix: 'You are Weaver, an AI workflow bot. Use the provided tools to complete tasks.', suffix: '' };
168
175
  }
169
176
 
170
177
  const taskPrompt = task.instruction.startsWith('## Task:')
@@ -219,14 +226,17 @@ export async function weaverAgentExecute(
219
226
 
220
227
  const onStreamEvent = (event: StreamEvent) => renderer.onStreamEvent(event);
221
228
 
222
- // Filter tools by profile: only orchestrators get task_create.
223
- // Without this, the AI sees task_create and delegates instead of doing work.
229
+ // Filter tools by task mode and profile capabilities.
230
+ // Mode-based filtering removes tools the task doesn't need (e.g., modify mode
231
+ // excludes write_file). Capability intersection ensures profiles only get their
232
+ // granted tools (e.g., orchestrator gets task_create, developer does not).
224
233
  const behavior = context.behaviorJson ? JSON.parse(context.behaviorJson) : undefined;
225
234
  const caps: string[] = behavior?.capabilities ?? [];
226
- const isOrchestrator = caps.includes('role-orchestrator') || caps.includes('task-mgmt') || caps.includes('decomposition');
227
- const tools = isOrchestrator
228
- ? WEAVER_TOOLS
229
- : WEAVER_TOOLS.filter(t => t.name !== 'task_create');
235
+ const grantedToolNames = resolveToolsForTask(
236
+ { mode: task.mode },
237
+ caps.length > 0 ? caps : undefined,
238
+ );
239
+ const tools = WEAVER_TOOLS.filter(t => grantedToolNames.has(t.name));
230
240
 
231
241
  const result = await withRetry(
232
242
  () => runAgentLoop(
@@ -102,13 +102,29 @@ export function weaverBuildContext(ctx: string): { ctx: string } {
102
102
  }
103
103
  } catch { /* non-fatal — memory is best-effort */ }
104
104
 
105
- // Auto-recall learned knowledge from previous bot runs
105
+ // Auto-recall learned knowledge from previous bot runs (with aging caveats)
106
106
  try {
107
107
  const knowledge = new KnowledgeStore(projectDir);
108
108
  const entries = knowledge.list();
109
109
  if (entries.length > 0) {
110
- const knowledgeLines = entries.map((e: { key: string; value: string }) => `- **${e.key}**: ${e.value}`);
111
- sections.push(`## Learned Knowledge\n\nFacts discovered by previous runs use these instead of re-discovering:\n${knowledgeLines.join('\n')}`);
110
+ const now = Date.now();
111
+ const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
112
+
113
+ // Auto-prune entries older than 90 days
114
+ const staleKeys = entries.filter(e => now - e.createdAt > NINETY_DAYS_MS).map(e => e.key);
115
+ for (const key of staleKeys) knowledge.forget(key);
116
+
117
+ const fresh = entries.filter(e => now - e.createdAt <= NINETY_DAYS_MS);
118
+ if (fresh.length > 0) {
119
+ const knowledgeLines = fresh.map((e: { key: string; value: string; createdAt: number }) => {
120
+ const ageDays = Math.floor((now - e.createdAt) / (24 * 60 * 60 * 1000));
121
+ const caveat = ageDays >= 1
122
+ ? ` _(${ageDays}d ago — may be outdated, verify before asserting)_`
123
+ : '';
124
+ return `- **${e.key}**: ${e.value}${caveat}`;
125
+ });
126
+ sections.push(`## Learned Knowledge\n\nFacts discovered by previous runs — use these instead of re-discovering:\n${knowledgeLines.join('\n')}`);
127
+ }
112
128
  }
113
129
  } catch { /* non-fatal — knowledge recall is best-effort */ }
114
130
 
@@ -10,6 +10,7 @@ import type { WeaverEnv, WeaverContext } from '../bot/types.js';
10
10
  * @color purple
11
11
  * @input env [order:0] - Weaver environment bundle
12
12
  * @input [taskJson] [order:1] - Pre-supplied task (JSON, optional)
13
+ * @input [frozenPromptPrefix] [order:2] [hidden] - Frozen system prompt prefix for cache sharing
13
14
  * @output ctx [order:0] - Weaver context (JSON)
14
15
  * @output onSuccess [order:-2] - On Success
15
16
  * @output onFailure [order:-1] [hidden] - On Failure
@@ -18,11 +19,13 @@ export async function weaverReceiveTask(
18
19
  execute: boolean,
19
20
  env: WeaverEnv,
20
21
  taskJson?: string,
22
+ frozenPromptPrefix?: string,
21
23
  ): Promise<{
22
24
  onSuccess: boolean; onFailure: boolean;
23
25
  ctx: string;
24
26
  }> {
25
27
  const context: WeaverContext = { env, taskJson: '{}', hasTask: false };
28
+ if (frozenPromptPrefix) context.frozenPromptPrefix = frozenPromptPrefix;
26
29
 
27
30
  if (!execute) {
28
31
  return { onSuccess: true, onFailure: false, ctx: JSON.stringify(context) };
@@ -11,6 +11,18 @@ import {
11
11
  } from '@synergenius/flow-weaver/agent';
12
12
  import { createWeaverExecutor } from '../bot/weaver-tools.js';
13
13
 
14
+ /**
15
+ * Strip `<analysis>...</analysis>` scratchpad blocks from LLM response text.
16
+ * The analysis is a reasoning scaffold that improves verdict quality but
17
+ * should not leak into the parsed JSON output.
18
+ */
19
+ export function stripAnalysis(text: string): { cleaned: string; analysis: string | undefined } {
20
+ const match = text.match(/<analysis>([\s\S]*?)<\/analysis>/);
21
+ const analysis = match?.[1]?.trim() || undefined;
22
+ const cleaned = text.replace(/<analysis>[\s\S]*?<\/analysis>/g, '').trim();
23
+ return { cleaned, analysis };
24
+ }
25
+
14
26
  /**
15
27
  * LLM-powered task completion reviewer.
16
28
  * Makes a single judgment call: did the bot accomplish the assigned task?
@@ -95,7 +107,9 @@ Rate each criterion as PASS or FAIL:
95
107
 
96
108
  If you need to verify file contents to judge the RESULT criterion, use the read_file tool. Only read files if the evidence is ambiguous.
97
109
 
98
- Respond with exactly:
110
+ First, write your reasoning inside <analysis> tags. Work through each criterion step by step, examining the evidence.
111
+
112
+ After your <analysis> block, output only the following JSON — no other text outside the tags:
99
113
  {"pass": true/false, "intent": "PASS/FAIL", "execution": "PASS/FAIL", "result": "PASS/FAIL", "completeness": "PASS/FAIL", "reason": "one sentence summary"}`;
100
114
 
101
115
  try {
@@ -129,11 +143,14 @@ Respond with exactly:
129
143
  { maxIterations: 2 },
130
144
  );
131
145
 
146
+ // Strip <analysis> scratchpad before parsing JSON verdict
147
+ const { cleaned: cleanedSummary } = stripAnalysis(result.summary);
148
+
132
149
  // Parse the structured response
133
150
  let pass = true;
134
151
  let reason = 'Review completed';
135
152
  let criteria: Record<string, string> = {};
136
- const jsonMatch = result.summary.match(/\{[\s\S]*"pass"[\s\S]*\}/);
153
+ const jsonMatch = cleanedSummary.match(/\{[\s\S]*"pass"[\s\S]*\}/);
137
154
  if (jsonMatch) {
138
155
  try {
139
156
  const parsed = JSON.parse(jsonMatch[0]);
@@ -146,14 +163,14 @@ Respond with exactly:
146
163
  } catch {
147
164
  if (jsonMatch[0].includes('"pass": false') || jsonMatch[0].includes('"pass":false')) {
148
165
  pass = false;
149
- reason = result.summary.slice(0, 200);
166
+ reason = cleanedSummary.slice(0, 200);
150
167
  }
151
168
  }
152
169
  } else {
153
- const lower = result.summary.toLowerCase();
170
+ const lower = cleanedSummary.toLowerCase();
154
171
  if (lower.includes('"pass": false') || lower.includes('"pass":false')) {
155
172
  pass = false;
156
- reason = result.summary.slice(0, 200);
173
+ reason = cleanedSummary.slice(0, 200);
157
174
  }
158
175
  }
159
176