@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9

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 (224) hide show
  1. package/dist/bot/agent-loop.d.ts +20 -0
  2. package/dist/bot/agent-loop.d.ts.map +1 -0
  3. package/dist/bot/agent-loop.js +331 -0
  4. package/dist/bot/agent-loop.js.map +1 -0
  5. package/dist/bot/agent-provider.d.ts.map +1 -1
  6. package/dist/bot/agent-provider.js +3 -2
  7. package/dist/bot/agent-provider.js.map +1 -1
  8. package/dist/bot/approvals.js +17 -8
  9. package/dist/bot/approvals.js.map +1 -1
  10. package/dist/bot/assistant-core.d.ts +17 -0
  11. package/dist/bot/assistant-core.d.ts.map +1 -1
  12. package/dist/bot/assistant-core.js +418 -60
  13. package/dist/bot/assistant-core.js.map +1 -1
  14. package/dist/bot/assistant-tools.d.ts +1 -1
  15. package/dist/bot/assistant-tools.d.ts.map +1 -1
  16. package/dist/bot/assistant-tools.js +283 -9
  17. package/dist/bot/assistant-tools.js.map +1 -1
  18. package/dist/bot/bot-agent-channel.d.ts.map +1 -1
  19. package/dist/bot/bot-agent-channel.js +2 -0
  20. package/dist/bot/bot-agent-channel.js.map +1 -1
  21. package/dist/bot/bot-manager.d.ts +4 -0
  22. package/dist/bot/bot-manager.d.ts.map +1 -1
  23. package/dist/bot/bot-manager.js +72 -27
  24. package/dist/bot/bot-manager.js.map +1 -1
  25. package/dist/bot/conversation-store.d.ts +6 -5
  26. package/dist/bot/conversation-store.d.ts.map +1 -1
  27. package/dist/bot/conversation-store.js +98 -42
  28. package/dist/bot/conversation-store.js.map +1 -1
  29. package/dist/bot/cost-store.d.ts +3 -0
  30. package/dist/bot/cost-store.d.ts.map +1 -1
  31. package/dist/bot/cost-store.js +21 -10
  32. package/dist/bot/cost-store.js.map +1 -1
  33. package/dist/bot/cost-tracker.d.ts.map +1 -1
  34. package/dist/bot/cost-tracker.js +14 -1
  35. package/dist/bot/cost-tracker.js.map +1 -1
  36. package/dist/bot/cron-parser.d.ts.map +1 -1
  37. package/dist/bot/cron-parser.js +2 -0
  38. package/dist/bot/cron-parser.js.map +1 -1
  39. package/dist/bot/cron-scheduler.d.ts.map +1 -1
  40. package/dist/bot/cron-scheduler.js +1 -0
  41. package/dist/bot/cron-scheduler.js.map +1 -1
  42. package/dist/bot/device-connection.d.ts +13 -0
  43. package/dist/bot/device-connection.d.ts.map +1 -0
  44. package/dist/bot/device-connection.js +102 -0
  45. package/dist/bot/device-connection.js.map +1 -0
  46. package/dist/bot/error-classifier.d.ts.map +1 -1
  47. package/dist/bot/error-classifier.js +5 -0
  48. package/dist/bot/error-classifier.js.map +1 -1
  49. package/dist/bot/file-lock.d.ts.map +1 -1
  50. package/dist/bot/file-lock.js +13 -3
  51. package/dist/bot/file-lock.js.map +1 -1
  52. package/dist/bot/file-watcher.d.ts.map +1 -1
  53. package/dist/bot/file-watcher.js +1 -0
  54. package/dist/bot/file-watcher.js.map +1 -1
  55. package/dist/bot/genesis-prompt-context.d.ts +5 -0
  56. package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
  57. package/dist/bot/genesis-prompt-context.js +55 -0
  58. package/dist/bot/genesis-prompt-context.js.map +1 -1
  59. package/dist/bot/genesis-store.d.ts +4 -0
  60. package/dist/bot/genesis-store.d.ts.map +1 -1
  61. package/dist/bot/genesis-store.js +79 -12
  62. package/dist/bot/genesis-store.js.map +1 -1
  63. package/dist/bot/improve-loop.d.ts +46 -0
  64. package/dist/bot/improve-loop.d.ts.map +1 -0
  65. package/dist/bot/improve-loop.js +592 -0
  66. package/dist/bot/improve-loop.js.map +1 -0
  67. package/dist/bot/insight-engine.d.ts +12 -0
  68. package/dist/bot/insight-engine.d.ts.map +1 -0
  69. package/dist/bot/insight-engine.js +256 -0
  70. package/dist/bot/insight-engine.js.map +1 -0
  71. package/dist/bot/knowledge-store.d.ts.map +1 -1
  72. package/dist/bot/knowledge-store.js +4 -1
  73. package/dist/bot/knowledge-store.js.map +1 -1
  74. package/dist/bot/pipeline-runner.d.ts.map +1 -1
  75. package/dist/bot/pipeline-runner.js +12 -4
  76. package/dist/bot/pipeline-runner.js.map +1 -1
  77. package/dist/bot/project-model.d.ts +25 -0
  78. package/dist/bot/project-model.d.ts.map +1 -0
  79. package/dist/bot/project-model.js +372 -0
  80. package/dist/bot/project-model.js.map +1 -0
  81. package/dist/bot/response-formatter.js +2 -3
  82. package/dist/bot/response-formatter.js.map +1 -1
  83. package/dist/bot/run-store.d.ts.map +1 -1
  84. package/dist/bot/run-store.js +10 -2
  85. package/dist/bot/run-store.js.map +1 -1
  86. package/dist/bot/safe-path.d.ts +1 -1
  87. package/dist/bot/safe-path.d.ts.map +1 -1
  88. package/dist/bot/safe-path.js +20 -1
  89. package/dist/bot/safe-path.js.map +1 -1
  90. package/dist/bot/safety.d.ts +10 -2
  91. package/dist/bot/safety.d.ts.map +1 -1
  92. package/dist/bot/safety.js +45 -2
  93. package/dist/bot/safety.js.map +1 -1
  94. package/dist/bot/session-state.d.ts +4 -0
  95. package/dist/bot/session-state.d.ts.map +1 -1
  96. package/dist/bot/session-state.js +52 -9
  97. package/dist/bot/session-state.js.map +1 -1
  98. package/dist/bot/slash-commands.d.ts.map +1 -1
  99. package/dist/bot/slash-commands.js +109 -3
  100. package/dist/bot/slash-commands.js.map +1 -1
  101. package/dist/bot/steering-engine.d.ts +67 -0
  102. package/dist/bot/steering-engine.d.ts.map +1 -0
  103. package/dist/bot/steering-engine.js +198 -0
  104. package/dist/bot/steering-engine.js.map +1 -0
  105. package/dist/bot/step-executor.d.ts.map +1 -1
  106. package/dist/bot/step-executor.js +62 -25
  107. package/dist/bot/step-executor.js.map +1 -1
  108. package/dist/bot/system-prompt.d.ts.map +1 -1
  109. package/dist/bot/system-prompt.js +5 -2
  110. package/dist/bot/system-prompt.js.map +1 -1
  111. package/dist/bot/task-queue.d.ts +6 -1
  112. package/dist/bot/task-queue.d.ts.map +1 -1
  113. package/dist/bot/task-queue.js +43 -4
  114. package/dist/bot/task-queue.js.map +1 -1
  115. package/dist/bot/tool-registry.d.ts +1 -1
  116. package/dist/bot/tool-registry.d.ts.map +1 -1
  117. package/dist/bot/tool-registry.js +65 -4
  118. package/dist/bot/tool-registry.js.map +1 -1
  119. package/dist/bot/trust-calculator.d.ts +34 -0
  120. package/dist/bot/trust-calculator.d.ts.map +1 -0
  121. package/dist/bot/trust-calculator.js +67 -0
  122. package/dist/bot/trust-calculator.js.map +1 -0
  123. package/dist/bot/types.d.ts +97 -0
  124. package/dist/bot/types.d.ts.map +1 -1
  125. package/dist/bot/update-checker.d.ts +21 -0
  126. package/dist/bot/update-checker.d.ts.map +1 -0
  127. package/dist/bot/update-checker.js +129 -0
  128. package/dist/bot/update-checker.js.map +1 -0
  129. package/dist/bot/weaver-tools.d.ts.map +1 -1
  130. package/dist/bot/weaver-tools.js +11 -4
  131. package/dist/bot/weaver-tools.js.map +1 -1
  132. package/dist/cli-bridge.d.ts +2 -0
  133. package/dist/cli-bridge.d.ts.map +1 -1
  134. package/dist/cli-bridge.js +3 -1
  135. package/dist/cli-bridge.js.map +1 -1
  136. package/dist/cli-handlers.d.ts +10 -1
  137. package/dist/cli-handlers.d.ts.map +1 -1
  138. package/dist/cli-handlers.js +141 -24
  139. package/dist/cli-handlers.js.map +1 -1
  140. package/dist/cli.d.ts +3 -0
  141. package/dist/cli.d.ts.map +1 -0
  142. package/dist/cli.js +749 -0
  143. package/dist/cli.js.map +1 -0
  144. package/dist/docs/weaver-config.md +15 -9
  145. package/dist/handlers/on-execution-completed.d.ts +11 -0
  146. package/dist/handlers/on-execution-completed.d.ts.map +1 -0
  147. package/dist/handlers/on-execution-completed.js +25 -0
  148. package/dist/handlers/on-execution-completed.js.map +1 -0
  149. package/dist/mcp-tools.d.ts.map +1 -1
  150. package/dist/mcp-tools.js +33 -0
  151. package/dist/mcp-tools.js.map +1 -1
  152. package/dist/node-types/genesis-approve.d.ts.map +1 -1
  153. package/dist/node-types/genesis-approve.js +28 -3
  154. package/dist/node-types/genesis-approve.js.map +1 -1
  155. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  156. package/dist/node-types/genesis-observe.js +23 -13
  157. package/dist/node-types/genesis-observe.js.map +1 -1
  158. package/dist/node-types/genesis-propose.d.ts.map +1 -1
  159. package/dist/node-types/genesis-propose.js +8 -0
  160. package/dist/node-types/genesis-propose.js.map +1 -1
  161. package/dist/node-types/genesis-update-history.d.ts.map +1 -1
  162. package/dist/node-types/genesis-update-history.js +13 -0
  163. package/dist/node-types/genesis-update-history.js.map +1 -1
  164. package/dist/templates/weaver-template.d.ts +11 -0
  165. package/dist/templates/weaver-template.d.ts.map +1 -0
  166. package/dist/templates/weaver-template.js +53 -0
  167. package/dist/templates/weaver-template.js.map +1 -0
  168. package/dist/workflows/weaver-bot-session.d.ts +65 -0
  169. package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
  170. package/dist/workflows/weaver-bot-session.js +68 -0
  171. package/dist/workflows/weaver-bot-session.js.map +1 -0
  172. package/dist/workflows/weaver.d.ts +24 -0
  173. package/dist/workflows/weaver.d.ts.map +1 -0
  174. package/dist/workflows/weaver.js +28 -0
  175. package/dist/workflows/weaver.js.map +1 -0
  176. package/flowweaver.manifest.json +28 -1
  177. package/package.json +6 -3
  178. package/src/bot/agent-provider.ts +3 -2
  179. package/src/bot/approvals.ts +16 -8
  180. package/src/bot/assistant-core.ts +420 -63
  181. package/src/bot/assistant-tools.ts +291 -9
  182. package/src/bot/bot-agent-channel.ts +2 -0
  183. package/src/bot/bot-manager.ts +70 -29
  184. package/src/bot/conversation-store.ts +87 -42
  185. package/src/bot/cost-store.ts +20 -9
  186. package/src/bot/cost-tracker.ts +13 -1
  187. package/src/bot/cron-parser.ts +1 -0
  188. package/src/bot/cron-scheduler.ts +1 -0
  189. package/src/bot/device-connection.ts +102 -0
  190. package/src/bot/error-classifier.ts +5 -0
  191. package/src/bot/file-lock.ts +12 -2
  192. package/src/bot/file-watcher.ts +1 -0
  193. package/src/bot/genesis-prompt-context.ts +61 -0
  194. package/src/bot/genesis-store.ts +68 -16
  195. package/src/bot/improve-loop.ts +651 -0
  196. package/src/bot/insight-engine.ts +273 -0
  197. package/src/bot/knowledge-store.ts +4 -1
  198. package/src/bot/pipeline-runner.ts +11 -6
  199. package/src/bot/project-model.ts +404 -0
  200. package/src/bot/response-formatter.ts +2 -3
  201. package/src/bot/run-store.ts +5 -2
  202. package/src/bot/safe-path.ts +20 -1
  203. package/src/bot/safety.ts +57 -3
  204. package/src/bot/session-state.ts +47 -7
  205. package/src/bot/slash-commands.ts +111 -3
  206. package/src/bot/steering-engine.ts +233 -0
  207. package/src/bot/step-executor.ts +66 -26
  208. package/src/bot/system-prompt.ts +5 -2
  209. package/src/bot/task-queue.ts +40 -4
  210. package/src/bot/tool-registry.ts +67 -5
  211. package/src/bot/trust-calculator.ts +87 -0
  212. package/src/bot/types.ts +104 -0
  213. package/src/bot/update-checker.ts +138 -0
  214. package/src/bot/weaver-tools.ts +10 -4
  215. package/src/cli-bridge.ts +4 -1
  216. package/src/cli-handlers.ts +150 -29
  217. package/src/handlers/on-execution-completed.ts +30 -0
  218. package/src/mcp-tools.ts +38 -0
  219. package/src/node-types/genesis-approve.ts +28 -3
  220. package/src/node-types/genesis-observe.ts +23 -12
  221. package/src/node-types/genesis-propose.ts +8 -0
  222. package/src/node-types/genesis-update-history.ts +12 -0
  223. package/src/ui/evolution-panel.tsx +96 -0
  224. package/src/ui/insights-widget.tsx +77 -0
@@ -21,10 +21,40 @@ export interface SlashCommand {
21
21
  export const SLASH_COMMANDS: SlashCommand[] = [
22
22
  {
23
23
  name: '/help',
24
- description: 'Show available commands',
24
+ description: 'Show available commands and capabilities',
25
25
  handler: async (ctx) => {
26
- ctx.out('\n Available commands:\n');
26
+ // Check if this is a new project (no workflows)
27
+ let hasWorkflows = false;
28
+ try {
29
+ const fsMod = await import('node:fs');
30
+ const pathMod = await import('node:path');
31
+ const srcDir = pathMod.join(ctx.projectDir, 'src');
32
+ if (fsMod.existsSync(srcDir)) {
33
+ const files = fsMod.readdirSync(srcDir, { recursive: true }) as string[];
34
+ hasWorkflows = files.some(f => f.endsWith('.ts'));
35
+ }
36
+ } catch { /* scan failed */ }
37
+
38
+ ctx.out(`\n ${c.bold('Weaver Assistant')}\n`);
39
+ ctx.out(` Tell me what to build, fix, or explore. I use tools to do the work.\n\n`);
40
+
41
+ if (!hasWorkflows) {
42
+ ctx.out(` ${c.bold('Getting Started:')}\n`);
43
+ ctx.out(` ${c.cyan('1.')} "create a hello world workflow"\n`);
44
+ ctx.out(` ${c.cyan('2.')} "create a data pipeline that reads an API and transforms the result"\n`);
45
+ ctx.out(` ${c.cyan('3.')} "what is Flow Weaver and how does it work?"\n\n`);
46
+ }
47
+
48
+ ctx.out(` ${c.bold('Capabilities:')}\n`);
49
+ ctx.out(` Workflows Create, validate, compile, diagram, describe, and modify\n`);
50
+ ctx.out(` Bots Spawn background workers that execute tasks autonomously\n`);
51
+ ctx.out(` Code Read, write, and patch files in your project\n`);
52
+ ctx.out(` Health Track project health, insights, cost, and trust level\n`);
53
+ ctx.out(` Evolution Propose and apply genesis improvements to bot workflows\n`);
54
+ ctx.out(` Shell Run commands, check TypeScript, run tests\n\n`);
55
+ ctx.out(` ${c.bold('Commands:')}\n`);
27
56
  for (const cmd of SLASH_COMMANDS) {
57
+ if (cmd.name === '/help') continue;
28
58
  ctx.out(` ${c.cyan(cmd.name.padEnd(12))} ${c.dim(cmd.description)}\n`);
29
59
  }
30
60
  ctx.out('\n');
@@ -36,7 +66,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
36
66
  handler: async (ctx) => {
37
67
  const bots = await ctx.executor('bot_list', {});
38
68
  const summary = await ctx.executor('conversation_summary', {});
39
- ctx.out(`\n ${bots.result}\n ${summary.result}\n\n`);
69
+ ctx.out(`\n Bots:\n ${bots.result}\n\n Conversation:\n ${summary.result}\n\n`);
40
70
  },
41
71
  },
42
72
  {
@@ -53,6 +83,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
53
83
  handler: async (ctx) => {
54
84
  process.stderr.write('\x1b[2J\x1b[H');
55
85
  ctx.onClear?.();
86
+ ctx.out(` ${c.dim('Screen cleared. New conversation started.')}\n\n`);
56
87
  },
57
88
  },
58
89
  {
@@ -85,6 +116,22 @@ export const SLASH_COMMANDS: SlashCommand[] = [
85
116
  ctx.onVerbose?.();
86
117
  },
87
118
  },
119
+ {
120
+ name: '/insights',
121
+ description: 'Show project insights and recommendations',
122
+ handler: async (ctx: SlashContext) => {
123
+ const result = await ctx.executor('project_insights', {});
124
+ ctx.out(`\n${result.result}\n\n`);
125
+ },
126
+ },
127
+ {
128
+ name: '/health',
129
+ description: 'Show project health summary',
130
+ handler: async (ctx: SlashContext) => {
131
+ const result = await ctx.executor('project_health', {});
132
+ ctx.out(`\n${result.result}\n\n`);
133
+ },
134
+ },
88
135
  {
89
136
  name: '/history',
90
137
  description: 'Show conversation history summary',
@@ -93,6 +140,67 @@ export const SLASH_COMMANDS: SlashCommand[] = [
93
140
  ctx.out(`\n ${result.result}\n\n`);
94
141
  },
95
142
  },
143
+ {
144
+ name: '/improve',
145
+ description: 'Show status of the current or last improve run',
146
+ handler: async (ctx) => {
147
+ try {
148
+ const fsMod = await import('node:fs');
149
+ const pathMod = await import('node:path');
150
+ const osMod = await import('node:os');
151
+ const summaryDir = pathMod.join(osMod.homedir(), '.weaver', 'improve');
152
+ if (!fsMod.existsSync(summaryDir)) {
153
+ ctx.out(`\n No improve runs found. Start one with: weaver improve\n\n`);
154
+ return;
155
+ }
156
+ const files = fsMod.readdirSync(summaryDir).filter((f: string) => f.endsWith('.json')).sort().reverse();
157
+ if (files.length === 0) {
158
+ ctx.out(`\n No improve runs found.\n\n`);
159
+ return;
160
+ }
161
+ const latest = JSON.parse(fsMod.readFileSync(pathMod.join(summaryDir, files[0]!), 'utf-8'));
162
+ const duration = Math.round((new Date(latest.finishedAt).getTime() - new Date(latest.startedAt).getTime()) / 1000);
163
+
164
+ ctx.out(`\n Improve Run (${latest.reason})\n`);
165
+ ctx.out(` Branch: ${latest.branch}\n`);
166
+ ctx.out(` Duration: ${duration}s\n`);
167
+ ctx.out(` Successes: ${latest.successes} Failures: ${latest.failures} Skips: ${latest.skips} Blocked: ${latest.blocked}\n\n`);
168
+ for (const cy of latest.cycles) {
169
+ const icon = cy.outcome === 'success' ? '✓' : cy.outcome === 'failure' ? '✗' : '○';
170
+ ctx.out(` ${icon} Cycle ${cy.cycle}: [${cy.outcome}] ${cy.description.slice(0, 70)}\n`);
171
+ }
172
+
173
+ // Check if a run is currently active
174
+ try {
175
+ const { execFileSync } = await import('node:child_process');
176
+ const worktrees = execFileSync('git', ['worktree', 'list'], { encoding: 'utf-8', cwd: ctx.projectDir });
177
+ if (worktrees.includes('weaver-improve')) {
178
+ ctx.out(`\n LIVE: improve worktree active — run is in progress\n`);
179
+ }
180
+ } catch { /* git not available */ }
181
+
182
+ ctx.out('\n');
183
+ } catch (err) {
184
+ ctx.out(`\n Error reading improve status: ${err instanceof Error ? err.message : err}\n\n`);
185
+ }
186
+ },
187
+ },
188
+ {
189
+ name: '/genesis',
190
+ description: 'Propose a workflow evolution based on project insights',
191
+ handler: async (ctx: SlashContext) => {
192
+ const result = await ctx.executor('genesis_propose', {});
193
+ ctx.out(`\n${result.result}\n\n`);
194
+ },
195
+ },
196
+ {
197
+ name: '/trust',
198
+ description: 'Show current trust level and factors',
199
+ handler: async (ctx: SlashContext) => {
200
+ const result = await ctx.executor('project_health', {});
201
+ ctx.out(`\n${result.result}\n\n`);
202
+ },
203
+ },
96
204
  ];
97
205
 
98
206
  export function getSlashCompletions(partial: string): string[] {
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Steering Engine — time-based and event-based nudges that guide the
3
+ * assistant's behavior during long-running operations.
4
+ *
5
+ * Steers are injected into the system prompt when conditions are met.
6
+ * They're guidance, not hard stops (except urgent + "STOP" which triggers hasHardStop).
7
+ */
8
+
9
+ export interface TimeTrigger {
10
+ type: 'time';
11
+ afterMs: number;
12
+ }
13
+
14
+ export interface EventTrigger {
15
+ type: 'event';
16
+ event: string;
17
+ count?: number; // default 1
18
+ }
19
+
20
+ export interface Steer {
21
+ id: string;
22
+ trigger: TimeTrigger | EventTrigger;
23
+ message: string;
24
+ intensity: 'gentle' | 'firm' | 'urgent';
25
+ once: boolean;
26
+ }
27
+
28
+ const INTENSITY_PREFIX: Record<string, string> = {
29
+ gentle: '[CONTEXT NOTE]',
30
+ firm: '[STEER]',
31
+ urgent: '[URGENT STEER]',
32
+ };
33
+
34
+ export class SteeringEngine {
35
+ private readonly steers: Steer[];
36
+ private startTime: number;
37
+ private eventCounts = new Map<string, number>();
38
+ private firedOnce = new Set<string>();
39
+ private pendingMessages: string[] = [];
40
+ private hardStopTriggered = false;
41
+ // Track last event count when an event steer fired (for once: false repeat logic)
42
+ private eventSteerLastFired = new Map<string, number>();
43
+
44
+ constructor(steers: Steer[]) {
45
+ this.steers = steers;
46
+ this.startTime = Date.now();
47
+ }
48
+
49
+ /**
50
+ * Check all steers and return the highest-priority triggered message,
51
+ * or null if nothing fired.
52
+ */
53
+ check(): string | null {
54
+ const triggered: string[] = [];
55
+ const elapsed = Date.now() - this.startTime;
56
+
57
+ for (const steer of this.steers) {
58
+ if (steer.once && this.firedOnce.has(steer.id)) continue;
59
+
60
+ let shouldFire = false;
61
+
62
+ if (steer.trigger.type === 'time') {
63
+ shouldFire = elapsed >= steer.trigger.afterMs;
64
+ } else if (steer.trigger.type === 'event') {
65
+ const count = this.eventCounts.get(steer.trigger.event) ?? 0;
66
+ const threshold = steer.trigger.count ?? 1;
67
+
68
+ if (steer.once) {
69
+ shouldFire = count >= threshold;
70
+ } else {
71
+ // For repeat steers, fire every time we cross another threshold
72
+ const lastFired = this.eventSteerLastFired.get(steer.id) ?? 0;
73
+ shouldFire = count >= threshold && count > lastFired;
74
+ if (shouldFire) {
75
+ this.eventSteerLastFired.set(steer.id, count);
76
+ }
77
+ }
78
+ }
79
+
80
+ if (shouldFire) {
81
+ const prefix = INTENSITY_PREFIX[steer.intensity] ?? '[STEER]';
82
+ const formatted = `${prefix} ${steer.message}`;
83
+ triggered.push(formatted);
84
+
85
+ if (steer.once) this.firedOnce.add(steer.id);
86
+
87
+ if (steer.intensity === 'urgent' && steer.message.includes('STOP')) {
88
+ this.hardStopTriggered = true;
89
+ }
90
+ }
91
+ }
92
+
93
+ if (triggered.length === 0) return null;
94
+
95
+ // Add each triggered message individually to pending
96
+ this.pendingMessages.push(...triggered);
97
+
98
+ // Return combined for inline use
99
+ return triggered.join('\n\n');
100
+ }
101
+
102
+ /**
103
+ * Record an event occurrence. May trigger event-based steers on next check().
104
+ */
105
+ recordEvent(event: string): void {
106
+ this.eventCounts.set(event, (this.eventCounts.get(event) ?? 0) + 1);
107
+ }
108
+
109
+ /**
110
+ * Get all pending messages since last call and clear the queue.
111
+ */
112
+ getPendingMessages(): string[] {
113
+ const msgs = [...this.pendingMessages];
114
+ this.pendingMessages = [];
115
+ return msgs;
116
+ }
117
+
118
+ /**
119
+ * Reset all state for a new cycle. Restarts the timer.
120
+ */
121
+ reset(): void {
122
+ this.startTime = Date.now();
123
+ this.eventCounts.clear();
124
+ this.firedOnce.clear();
125
+ this.pendingMessages = [];
126
+ this.hardStopTriggered = false;
127
+ this.eventSteerLastFired.clear();
128
+ }
129
+
130
+ /**
131
+ * Returns true if an urgent steer with "STOP" in the message has fired.
132
+ */
133
+ hasHardStop(): boolean {
134
+ return this.hardStopTriggered;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Default steers that ship with every assistant session.
140
+ */
141
+ export const DEFAULT_STEERS: Steer[] = [
142
+ {
143
+ id: 'direction-check',
144
+ trigger: { type: 'time', afterMs: 300_000 },
145
+ message: "You've been working for 5 minutes. Take a step back — are you sure this is the right approach? If not, pivot now.",
146
+ intensity: 'gentle',
147
+ once: true,
148
+ },
149
+ {
150
+ id: 'wrap-up',
151
+ trigger: { type: 'time', afterMs: 1_200_000 },
152
+ message: '20 minutes in. Focus on finishing your current change. If tests pass, commit. If not, fix only what\'s broken.',
153
+ intensity: 'firm',
154
+ once: true,
155
+ },
156
+ {
157
+ id: 'final-warning',
158
+ trigger: { type: 'time', afterMs: 2_700_000 },
159
+ message: "45 minutes. Commit what works now. Revert anything that doesn't pass tests. Time is almost up.",
160
+ intensity: 'urgent',
161
+ once: true,
162
+ },
163
+ {
164
+ id: 'hard-stop',
165
+ trigger: { type: 'time', afterMs: 3_600_000 },
166
+ message: 'STOP. 60 minutes reached. Commit passing work or revert everything.',
167
+ intensity: 'urgent',
168
+ once: true,
169
+ },
170
+ {
171
+ id: 'repeated-errors',
172
+ trigger: { type: 'event', event: 'tool_error', count: 3 },
173
+ message: "You've hit 3 tool errors. Stop and reconsider your approach. Read the error messages carefully.",
174
+ intensity: 'firm',
175
+ once: false,
176
+ },
177
+ {
178
+ id: 'test-failure-reflect',
179
+ trigger: { type: 'event', event: 'test_fail' },
180
+ message: "Tests failed. Don't just retry — think about WHY they failed. Read the error output carefully before making changes.",
181
+ intensity: 'gentle',
182
+ once: false,
183
+ },
184
+ {
185
+ id: 'write-blocked',
186
+ trigger: { type: 'event', event: 'file_write_blocked', count: 2 },
187
+ message: "File writes are being blocked. You're probably using the wrong path. Use paths relative to the project directory, not absolute paths.",
188
+ intensity: 'firm',
189
+ once: true,
190
+ },
191
+ ];
192
+
193
+ /**
194
+ * Additional steers for the improve loop.
195
+ */
196
+ export const IMPROVE_STEERS: Steer[] = [
197
+ {
198
+ id: 'improve-focus',
199
+ trigger: { type: 'time', afterMs: 120_000 },
200
+ message: "2 minutes in. If you haven't started writing code yet, you're over-analyzing. Pick the simplest fix and start writing.",
201
+ intensity: 'gentle',
202
+ once: true,
203
+ },
204
+ {
205
+ id: 'improve-commit-ready',
206
+ trigger: { type: 'event', event: 'test_pass' },
207
+ message: 'Tests pass. Commit your changes now unless you have a specific reason to keep going.',
208
+ intensity: 'firm',
209
+ once: false,
210
+ },
211
+ ];
212
+
213
+ /**
214
+ * Load steers from .weaver-steers.json, merged with defaults.
215
+ */
216
+ export function loadSteers(projectDir: string, extras: Steer[] = []): Steer[] {
217
+ const base = [...DEFAULT_STEERS, ...extras];
218
+
219
+ try {
220
+ const fs = require('node:fs') as typeof import('node:fs');
221
+ const path = require('node:path') as typeof import('node:path');
222
+ const configPath = path.join(projectDir, '.weaver-steers.json');
223
+ if (fs.existsSync(configPath)) {
224
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
225
+ const userSteers: Steer[] = config.steers ?? [];
226
+ // User steers override defaults by ID
227
+ const userIds = new Set(userSteers.map(s => s.id));
228
+ return [...base.filter(s => !userIds.has(s.id)), ...userSteers];
229
+ }
230
+ } catch { /* config not available */ }
231
+
232
+ return base;
233
+ }
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
4
  import { runCommand } from '@synergenius/flow-weaver';
5
+ import { BLOCKED_SHELL_PATTERNS } from './safety.js';
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Safety thresholds
@@ -25,21 +26,8 @@ const MAX_READ_SIZE = 1_048_576;
25
26
  /** Maximum files returned by list-files. */
26
27
  const MAX_LIST_FILES = 1000;
27
28
 
28
- /** Shell commands that are NEVER allowed (destructive operations). */
29
- const BLOCKED_SHELL_PATTERNS = [
30
- /\brm\s+(-[a-z]*r|-[a-z]*f)[a-z]*\s/i, // rm with -r or -f flags
31
- /\bgit\s+push\b/i, // git push (no remote ops)
32
- /\bgit\s+reset\s+--hard\b/i, // git reset --hard
33
- /\bnpm\s+publish\b/i, // npm publish
34
- /\bcurl\b.*\|\s*(sh|bash)\b/i, // curl | sh/bash
35
- /\bwget\b.*\|\s*(sh|bash)\b/i, // wget | sh/bash
36
- /\bsudo\b/i, // sudo
37
- /\bchmod\s+777\b/i, // chmod 777
38
- /\bkill\s+-9\b/i, // kill -9
39
- /\bmkfs\b/i, // format disk
40
- /\bdd\s+if=/i, // dd (disk destroyer)
41
- />\s*\/dev\/sd/i, // write to raw disk
42
- ];
29
+ // Shell blocklist imported from safety.ts single source of truth.
30
+ // Do NOT add patterns here; add them in safety.ts instead.
43
31
 
44
32
  /** Track files written in this process to enforce the per-plan cap. */
45
33
  let filesWrittenThisPlan = 0;
@@ -55,11 +43,37 @@ export function resetPlanFileCounter(): void {
55
43
 
56
44
  function assertSafePath(filePath: string, projectDir: string): void {
57
45
  const resolved = path.resolve(projectDir, filePath);
58
- if (!resolved.startsWith(path.resolve(projectDir))) {
46
+ const resolvedBase = path.resolve(projectDir);
47
+ if (!resolved.startsWith(resolvedBase + path.sep) && resolved !== resolvedBase) {
59
48
  throw new Error(
60
49
  `Path traversal blocked: "${filePath}" resolves outside project directory.`,
61
50
  );
62
51
  }
52
+
53
+ // Symlink protection: resolve real paths for any existing segments.
54
+ // Walk up from the resolved path to find the deepest existing ancestor,
55
+ // then verify its real path is still inside the project directory.
56
+ let checkPath = resolved;
57
+ while (!fs.existsSync(checkPath) && checkPath !== resolvedBase) {
58
+ checkPath = path.dirname(checkPath);
59
+ }
60
+ if (fs.existsSync(checkPath)) {
61
+ try {
62
+ const realPath = fs.realpathSync(checkPath);
63
+ const realBase = fs.realpathSync(resolvedBase);
64
+ if (!realPath.startsWith(realBase + path.sep) && realPath !== realBase) {
65
+ throw new Error(
66
+ `Path traversal blocked: "${filePath}" resolves outside project directory via symlink.`,
67
+ );
68
+ }
69
+ } catch (err) {
70
+ if (err instanceof Error && err.message.includes('Path traversal')) throw err;
71
+ // If realpath fails (broken symlink etc), block it
72
+ throw new Error(
73
+ `Path traversal blocked: "${filePath}" could not verify path safety.`,
74
+ );
75
+ }
76
+ }
63
77
  }
64
78
 
65
79
  // ---------------------------------------------------------------------------
@@ -222,6 +236,21 @@ export async function executeStep(
222
236
  };
223
237
  }
224
238
 
239
+ // Shrink guard: block patches that would reduce file size by >50%
240
+ const originalSize = fileSize;
241
+ const newSize = Buffer.byteLength(content, 'utf-8');
242
+ if (originalSize > SHRINK_GUARD_MIN_SIZE && newSize < originalSize * MAX_SHRINK_RATIO) {
243
+ const shrinkPct = Math.round((1 - newSize / originalSize) * 100);
244
+ return {
245
+ file: filePath,
246
+ blocked: true,
247
+ blockReason:
248
+ `Refusing to patch ${path.basename(filePath)}: result (${newSize}B) ` +
249
+ `is ${shrinkPct}% smaller than original (${originalSize}B). ` +
250
+ `This looks like accidental content removal. Review patches carefully.`,
251
+ };
252
+ }
253
+
225
254
  fs.writeFileSync(filePath, content, 'utf-8');
226
255
  filesWrittenThisPlan++;
227
256
 
@@ -242,16 +271,21 @@ export async function executeStep(
242
271
  if (!fs.existsSync(filePath)) {
243
272
  return { output: `File not found: ${file}` };
244
273
  }
245
- if (fs.statSync(filePath).isDirectory()) {
246
- const entries = fs.readdirSync(filePath, { encoding: 'utf-8' });
247
- return { output: `"${file}" is a directory. Contents:\n${entries.join('\n')}` };
248
- }
249
- const fileSize = fs.statSync(filePath).size;
250
- if (fileSize > MAX_READ_SIZE) {
251
- return { output: `File too large to read (${fileSize} bytes, max ${MAX_READ_SIZE}). Use run-shell with head/tail instead.` };
274
+ try {
275
+ const stat = fs.statSync(filePath);
276
+ if (stat.isDirectory()) {
277
+ const entries = fs.readdirSync(filePath, { encoding: 'utf-8' });
278
+ return { output: `"${file}" is a directory. Contents:\n${entries.join('\n')}` };
279
+ }
280
+ if (stat.size > MAX_READ_SIZE) {
281
+ return { output: `File too large to read (${stat.size} bytes, max ${MAX_READ_SIZE}). Use run-shell with head/tail instead.` };
282
+ }
283
+ const content = fs.readFileSync(filePath, 'utf-8');
284
+ return { file: filePath, output: content };
285
+ } catch (err: unknown) {
286
+ const msg = err instanceof Error ? err.message : String(err);
287
+ return { output: `Error reading file "${file}": ${msg}` };
252
288
  }
253
- const content = fs.readFileSync(filePath, 'utf-8');
254
- return { file: filePath, output: content };
255
289
  }
256
290
 
257
291
  // -----------------------------------------------------------------
@@ -302,7 +336,13 @@ export async function executeStep(
302
336
  return { output: `Directory not found: ${dir}` };
303
337
  }
304
338
 
305
- const entries = fs.readdirSync(targetDir, { recursive: true, encoding: 'utf-8' }) as string[];
339
+ let entries: string[];
340
+ try {
341
+ entries = fs.readdirSync(targetDir, { recursive: true, encoding: 'utf-8' }) as string[];
342
+ } catch (err: unknown) {
343
+ const msg = err instanceof Error ? err.message : String(err);
344
+ return { output: `Error listing directory "${dir}": ${msg}` };
345
+ }
306
346
  let files = entries
307
347
  .filter(e => {
308
348
  if (e.includes('node_modules') || e.includes('.git')) return false;
@@ -224,8 +224,9 @@ export async function buildSystemPrompt(): Promise<string> {
224
224
  docMeta.VALIDATION_CODES ?? [],
225
225
  docMeta.CLI_COMMANDS ?? [],
226
226
  );
227
- } catch {
227
+ } catch (err) {
228
228
  // Fallback if doc-metadata not available (e.g., older flow-weaver version)
229
+ console.warn('Failed to load doc-metadata for system prompt; using empty fallback:', err);
229
230
  cachedPrompt = buildPromptFromMetadata([], [], [], [], []);
230
231
  }
231
232
 
@@ -292,7 +293,9 @@ Be concise in your text responses — let tool results speak.`;
292
293
  const plan = fs.readFileSync(planPath, 'utf-8').trim();
293
294
  prompt += '\n\n## Project Plan & Vision\n\nIMPORTANT: All work MUST align with this plan. If a task contradicts the plan, skip it and explain why.\n\n' + plan;
294
295
  }
295
- } catch { /* plan file not available */ }
296
+ } catch (err) {
297
+ console.warn('Failed to load project plan file:', err);
298
+ }
296
299
  }
297
300
 
298
301
  if (contextBundle) {
@@ -14,6 +14,8 @@ export interface QueuedTask {
14
14
  priority: number;
15
15
  addedAt: number;
16
16
  status: 'pending' | 'running' | 'completed' | 'no-op' | 'failed' | 'cancelled';
17
+ /** PID of the process running this task (set when status becomes 'running'). */
18
+ runnerId?: number;
17
19
  /** Error reason (set on failure) */
18
20
  failureReason?: string;
19
21
  }
@@ -57,7 +59,7 @@ export class TaskQueue {
57
59
  // Queue size cap
58
60
  const pendingCount = existing.filter(t => t.status === 'pending').length;
59
61
  if (pendingCount >= MAX_PENDING) {
60
- throw new Error(`Queue full (${MAX_PENDING} pending tasks). Clear or process existing tasks first.`);
62
+ throw new Error(`Queue full (${MAX_PENDING} pending tasks). Use queue_retry to resume failed tasks, or queue_list to review.`);
61
63
  }
62
64
 
63
65
  const entry: QueuedTask = {
@@ -105,7 +107,15 @@ export class TaskQueue {
105
107
  }
106
108
 
107
109
  async markRunning(id: string): Promise<void> {
108
- await this.updateStatus(id, 'running');
110
+ return withFileLock(this.filePath, () => {
111
+ const tasks = this.readAll();
112
+ const task = tasks.find(t => t.id === id);
113
+ if (task) {
114
+ task.status = 'running';
115
+ task.runnerId = process.pid;
116
+ this.writeAll(tasks);
117
+ }
118
+ });
109
119
  }
110
120
 
111
121
  async markComplete(id: string): Promise<void> {
@@ -158,14 +168,22 @@ export class TaskQueue {
158
168
  });
159
169
  }
160
170
 
161
- /** Reset orphaned "running" tasks to pending (crash recovery). */
171
+ /** Reset orphaned "running" tasks to pending (crash recovery).
172
+ * Only resets tasks whose runner PID is no longer alive. */
162
173
  async recoverOrphans(): Promise<number> {
163
174
  return withFileLock(this.filePath, () => {
164
175
  const tasks = this.readAll();
165
176
  let count = 0;
166
177
  for (const t of tasks) {
167
178
  if (t.status === 'running') {
179
+ // Check if the runner process is still alive
180
+ if (t.runnerId != null) {
181
+ let alive = false;
182
+ try { process.kill(t.runnerId, 0); alive = true; } catch { /* process gone */ }
183
+ if (alive) continue; // skip — process is still working on this task
184
+ }
168
185
  t.status = 'pending';
186
+ t.runnerId = undefined;
169
187
  count++;
170
188
  }
171
189
  }
@@ -193,9 +211,27 @@ export class TaskQueue {
193
211
  return records;
194
212
  }
195
213
 
214
+ /** Atomically claim the next pending task: selects highest-priority and marks it running in one lock. */
215
+ async claimNext(): Promise<QueuedTask | null> {
216
+ return withFileLock(this.filePath, () => {
217
+ const tasks = this.readAll();
218
+ const pending = tasks.filter(t => t.status === 'pending');
219
+ pending.sort((a, b) => b.priority - a.priority || a.addedAt - b.addedAt);
220
+ const chosen = pending[0];
221
+ if (!chosen) return null;
222
+ chosen.status = 'running';
223
+ chosen.runnerId = process.pid;
224
+ this.writeAll(tasks);
225
+ return chosen;
226
+ });
227
+ }
228
+
196
229
  private writeAll(tasks: QueuedTask[]): void {
197
230
  const dir = path.dirname(this.filePath);
198
231
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
199
- fs.writeFileSync(this.filePath, tasks.map(t => JSON.stringify(t)).join('\n') + (tasks.length > 0 ? '\n' : ''), 'utf-8');
232
+ // Atomic write: write to temp file then rename to avoid partial reads on crash
233
+ const tmpPath = this.filePath + '.tmp.' + process.pid;
234
+ fs.writeFileSync(tmpPath, tasks.map(t => JSON.stringify(t)).join('\n') + (tasks.length > 0 ? '\n' : ''), 'utf-8');
235
+ fs.renameSync(tmpPath, this.filePath);
200
236
  }
201
237
  }