@synergenius/flow-weaver-pack-weaver 0.9.0 → 0.9.3

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 (210) hide show
  1. package/dist/bot/ai-client.d.ts +22 -2
  2. package/dist/bot/ai-client.d.ts.map +1 -1
  3. package/dist/bot/ai-client.js +168 -20
  4. package/dist/bot/ai-client.js.map +1 -1
  5. package/dist/bot/assistant-core.d.ts +25 -0
  6. package/dist/bot/assistant-core.d.ts.map +1 -0
  7. package/dist/bot/assistant-core.js +265 -0
  8. package/dist/bot/assistant-core.js.map +1 -0
  9. package/dist/bot/assistant-tools.d.ts +9 -0
  10. package/dist/bot/assistant-tools.d.ts.map +1 -0
  11. package/dist/bot/assistant-tools.js +602 -0
  12. package/dist/bot/assistant-tools.js.map +1 -0
  13. package/dist/bot/audit-logger.d.ts.map +1 -1
  14. package/dist/bot/audit-logger.js +9 -5
  15. package/dist/bot/audit-logger.js.map +1 -1
  16. package/dist/bot/bot-manager.d.ts +49 -0
  17. package/dist/bot/bot-manager.d.ts.map +1 -0
  18. package/dist/bot/bot-manager.js +279 -0
  19. package/dist/bot/bot-manager.js.map +1 -0
  20. package/dist/bot/child-process-tracker.d.ts +6 -0
  21. package/dist/bot/child-process-tracker.d.ts.map +1 -0
  22. package/dist/bot/child-process-tracker.js +35 -0
  23. package/dist/bot/child-process-tracker.js.map +1 -0
  24. package/dist/bot/cli-provider.d.ts.map +1 -1
  25. package/dist/bot/cli-provider.js +13 -8
  26. package/dist/bot/cli-provider.js.map +1 -1
  27. package/dist/bot/conversation-store.d.ts +40 -0
  28. package/dist/bot/conversation-store.d.ts.map +1 -0
  29. package/dist/bot/conversation-store.js +182 -0
  30. package/dist/bot/conversation-store.js.map +1 -0
  31. package/dist/bot/error-guide.d.ts +10 -0
  32. package/dist/bot/error-guide.d.ts.map +1 -0
  33. package/dist/bot/error-guide.js +34 -0
  34. package/dist/bot/error-guide.js.map +1 -0
  35. package/dist/bot/knowledge-store.d.ts +17 -0
  36. package/dist/bot/knowledge-store.d.ts.map +1 -0
  37. package/dist/bot/knowledge-store.js +53 -0
  38. package/dist/bot/knowledge-store.js.map +1 -0
  39. package/dist/bot/retry-utils.d.ts +19 -0
  40. package/dist/bot/retry-utils.d.ts.map +1 -0
  41. package/dist/bot/retry-utils.js +64 -0
  42. package/dist/bot/retry-utils.js.map +1 -0
  43. package/dist/bot/runner.d.ts.map +1 -1
  44. package/dist/bot/runner.js +12 -1
  45. package/dist/bot/runner.js.map +1 -1
  46. package/dist/bot/session-state.d.ts.map +1 -1
  47. package/dist/bot/session-state.js +3 -1
  48. package/dist/bot/session-state.js.map +1 -1
  49. package/dist/bot/steering.js +1 -1
  50. package/dist/bot/steering.js.map +1 -1
  51. package/dist/bot/step-executor.d.ts +10 -5
  52. package/dist/bot/step-executor.d.ts.map +1 -1
  53. package/dist/bot/step-executor.js +252 -3
  54. package/dist/bot/step-executor.js.map +1 -1
  55. package/dist/bot/system-prompt.d.ts +1 -1
  56. package/dist/bot/system-prompt.d.ts.map +1 -1
  57. package/dist/bot/system-prompt.js +69 -43
  58. package/dist/bot/system-prompt.js.map +1 -1
  59. package/dist/bot/task-decomposer.d.ts +24 -0
  60. package/dist/bot/task-decomposer.d.ts.map +1 -0
  61. package/dist/bot/task-decomposer.js +75 -0
  62. package/dist/bot/task-decomposer.js.map +1 -0
  63. package/dist/bot/task-queue.d.ts +17 -4
  64. package/dist/bot/task-queue.d.ts.map +1 -1
  65. package/dist/bot/task-queue.js +95 -4
  66. package/dist/bot/task-queue.js.map +1 -1
  67. package/dist/bot/terminal-renderer.d.ts +60 -0
  68. package/dist/bot/terminal-renderer.d.ts.map +1 -0
  69. package/dist/bot/terminal-renderer.js +205 -0
  70. package/dist/bot/terminal-renderer.js.map +1 -0
  71. package/dist/bot/types.d.ts +7 -0
  72. package/dist/bot/types.d.ts.map +1 -1
  73. package/dist/bot/weaver-tools.d.ts +18 -0
  74. package/dist/bot/weaver-tools.d.ts.map +1 -0
  75. package/dist/bot/weaver-tools.js +215 -0
  76. package/dist/bot/weaver-tools.js.map +1 -0
  77. package/dist/cli-bridge.d.ts.map +1 -1
  78. package/dist/cli-bridge.js +5 -1
  79. package/dist/cli-bridge.js.map +1 -1
  80. package/dist/cli-handlers.d.ts +13 -1
  81. package/dist/cli-handlers.d.ts.map +1 -1
  82. package/dist/cli-handlers.js +616 -48
  83. package/dist/cli-handlers.js.map +1 -1
  84. package/dist/mcp-tools.js +2 -2
  85. package/dist/mcp-tools.js.map +1 -1
  86. package/dist/node-types/abort-task.d.ts.map +1 -1
  87. package/dist/node-types/abort-task.js +4 -3
  88. package/dist/node-types/abort-task.js.map +1 -1
  89. package/dist/node-types/agent-execute.d.ts +38 -0
  90. package/dist/node-types/agent-execute.d.ts.map +1 -0
  91. package/dist/node-types/agent-execute.js +256 -0
  92. package/dist/node-types/agent-execute.js.map +1 -0
  93. package/dist/node-types/bot-report.d.ts +5 -3
  94. package/dist/node-types/bot-report.d.ts.map +1 -1
  95. package/dist/node-types/bot-report.js +39 -7
  96. package/dist/node-types/bot-report.js.map +1 -1
  97. package/dist/node-types/build-context.d.ts +3 -3
  98. package/dist/node-types/build-context.d.ts.map +1 -1
  99. package/dist/node-types/build-context.js +108 -24
  100. package/dist/node-types/build-context.js.map +1 -1
  101. package/dist/node-types/detect-provider.d.ts +2 -2
  102. package/dist/node-types/detect-provider.d.ts.map +1 -1
  103. package/dist/node-types/detect-provider.js +3 -1
  104. package/dist/node-types/detect-provider.js.map +1 -1
  105. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  106. package/dist/node-types/exec-validate-retry.js +43 -6
  107. package/dist/node-types/exec-validate-retry.js.map +1 -1
  108. package/dist/node-types/execute-plan.d.ts.map +1 -1
  109. package/dist/node-types/execute-plan.js +31 -8
  110. package/dist/node-types/execute-plan.js.map +1 -1
  111. package/dist/node-types/execute-target.d.ts.map +1 -1
  112. package/dist/node-types/execute-target.js +3 -1
  113. package/dist/node-types/execute-target.js.map +1 -1
  114. package/dist/node-types/fix-errors.d.ts.map +1 -1
  115. package/dist/node-types/fix-errors.js +21 -5
  116. package/dist/node-types/fix-errors.js.map +1 -1
  117. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  118. package/dist/node-types/genesis-observe.js +3 -1
  119. package/dist/node-types/genesis-observe.js.map +1 -1
  120. package/dist/node-types/genesis-report.js +4 -1
  121. package/dist/node-types/genesis-report.js.map +1 -1
  122. package/dist/node-types/git-ops.d.ts.map +1 -1
  123. package/dist/node-types/git-ops.js +98 -4
  124. package/dist/node-types/git-ops.js.map +1 -1
  125. package/dist/node-types/index.d.ts +2 -0
  126. package/dist/node-types/index.d.ts.map +1 -1
  127. package/dist/node-types/index.js +2 -0
  128. package/dist/node-types/index.js.map +1 -1
  129. package/dist/node-types/load-config.d.ts +2 -2
  130. package/dist/node-types/load-config.d.ts.map +1 -1
  131. package/dist/node-types/load-config.js.map +1 -1
  132. package/dist/node-types/plan-task.d.ts.map +1 -1
  133. package/dist/node-types/plan-task.js +14 -2
  134. package/dist/node-types/plan-task.js.map +1 -1
  135. package/dist/node-types/read-workflow.js +8 -2
  136. package/dist/node-types/read-workflow.js.map +1 -1
  137. package/dist/node-types/receive-task.d.ts.map +1 -1
  138. package/dist/node-types/receive-task.js +35 -26
  139. package/dist/node-types/receive-task.js.map +1 -1
  140. package/dist/node-types/send-notify.js +2 -1
  141. package/dist/node-types/send-notify.js.map +1 -1
  142. package/dist/node-types/validate-gate.d.ts +18 -0
  143. package/dist/node-types/validate-gate.d.ts.map +1 -0
  144. package/dist/node-types/validate-gate.js +96 -0
  145. package/dist/node-types/validate-gate.js.map +1 -0
  146. package/dist/workflows/genesis-task.d.ts +20 -12
  147. package/dist/workflows/genesis-task.d.ts.map +1 -1
  148. package/dist/workflows/genesis-task.js +20 -12
  149. package/dist/workflows/genesis-task.js.map +1 -1
  150. package/dist/workflows/weaver-agent.d.ts +35 -0
  151. package/dist/workflows/weaver-agent.d.ts.map +1 -0
  152. package/dist/workflows/weaver-agent.js +777 -0
  153. package/dist/workflows/weaver-agent.js.map +1 -0
  154. package/dist/workflows/weaver-bot-batch.d.ts +19 -26
  155. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  156. package/dist/workflows/weaver-bot-batch.js +1043 -27
  157. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  158. package/dist/workflows/weaver-bot.d.ts +21 -35
  159. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  160. package/dist/workflows/weaver-bot.js +1119 -36
  161. package/dist/workflows/weaver-bot.js.map +1 -1
  162. package/flowweaver.manifest.json +21 -1
  163. package/package.json +5 -2
  164. package/src/bot/ai-client.ts +180 -19
  165. package/src/bot/assistant-core.ts +306 -0
  166. package/src/bot/assistant-tools.ts +605 -0
  167. package/src/bot/audit-logger.ts +6 -5
  168. package/src/bot/bot-manager.ts +293 -0
  169. package/src/bot/child-process-tracker.ts +40 -0
  170. package/src/bot/cli-provider.ts +13 -8
  171. package/src/bot/conversation-store.ts +222 -0
  172. package/src/bot/error-guide.ts +34 -0
  173. package/src/bot/knowledge-store.ts +59 -0
  174. package/src/bot/retry-utils.ts +76 -0
  175. package/src/bot/runner.ts +12 -1
  176. package/src/bot/session-state.ts +2 -1
  177. package/src/bot/steering.ts +1 -1
  178. package/src/bot/step-executor.ts +313 -5
  179. package/src/bot/system-prompt.ts +70 -47
  180. package/src/bot/task-decomposer.ts +100 -0
  181. package/src/bot/task-queue.ts +113 -7
  182. package/src/bot/terminal-renderer.ts +241 -0
  183. package/src/bot/types.ts +8 -0
  184. package/src/bot/weaver-tools.ts +225 -0
  185. package/src/cli-bridge.ts +7 -1
  186. package/src/cli-handlers.ts +625 -48
  187. package/src/mcp-tools.ts +2 -2
  188. package/src/node-types/abort-task.ts +5 -4
  189. package/src/node-types/agent-execute.ts +306 -0
  190. package/src/node-types/bot-report.ts +40 -9
  191. package/src/node-types/build-context.ts +112 -25
  192. package/src/node-types/detect-provider.ts +4 -3
  193. package/src/node-types/exec-validate-retry.ts +47 -8
  194. package/src/node-types/execute-plan.ts +32 -8
  195. package/src/node-types/execute-target.ts +2 -1
  196. package/src/node-types/fix-errors.ts +20 -5
  197. package/src/node-types/genesis-observe.ts +2 -1
  198. package/src/node-types/genesis-report.ts +1 -1
  199. package/src/node-types/git-ops.ts +93 -4
  200. package/src/node-types/index.ts +2 -0
  201. package/src/node-types/load-config.ts +3 -3
  202. package/src/node-types/plan-task.ts +15 -3
  203. package/src/node-types/read-workflow.ts +2 -2
  204. package/src/node-types/receive-task.ts +31 -26
  205. package/src/node-types/send-notify.ts +1 -1
  206. package/src/node-types/validate-gate.ts +112 -0
  207. package/src/workflows/genesis-task.ts +20 -12
  208. package/src/workflows/weaver-agent.ts +799 -0
  209. package/src/workflows/weaver-bot-batch.ts +1049 -27
  210. package/src/workflows/weaver-bot.ts +1123 -36
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Auto task decomposition — splits broad tasks into per-file tasks.
3
+ *
4
+ * When a task says "fix all templates" or "validate everything",
5
+ * decompose it into one task per file. This gives the AI focused
6
+ * context and prevents one failure from blocking all files.
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ export interface DecomposableTask {
13
+ id: string;
14
+ instruction: string;
15
+ mode?: string;
16
+ targets?: string[];
17
+ priority?: number;
18
+ }
19
+
20
+ export interface DecomposedResult {
21
+ decomposed: boolean;
22
+ tasks: DecomposableTask[];
23
+ }
24
+
25
+ // Patterns that suggest a broad task targeting multiple files
26
+ const BROAD_PATTERNS = [
27
+ /\b(all|every|each)\b.*\b(template|workflow|file|node.?type)s?\b/i,
28
+ /\bfix\b.*\bin\s+src\/(templates|node-types|workflows)\/?$/i,
29
+ /\bvalidat(e|ion)\b.*\b(all|every|each|src\/)\b/i,
30
+ ];
31
+
32
+ /**
33
+ * Check if a task should be decomposed into per-file tasks.
34
+ * Returns the original task unchanged if no decomposition is needed.
35
+ */
36
+ export function decomposeTask(
37
+ task: DecomposableTask,
38
+ projectDir: string,
39
+ ): DecomposedResult {
40
+ const instruction = task.instruction;
41
+
42
+ // Already has specific targets — don't decompose
43
+ if (task.targets && task.targets.length === 1) {
44
+ return { decomposed: false, tasks: [task] };
45
+ }
46
+
47
+ // Check if instruction matches broad patterns
48
+ const isBroad = BROAD_PATTERNS.some(p => p.test(instruction));
49
+ if (!isBroad) {
50
+ return { decomposed: false, tasks: [task] };
51
+ }
52
+
53
+ // Determine which directory to scan
54
+ let targetDir: string | undefined;
55
+ if (instruction.match(/template/i)) targetDir = 'src/templates';
56
+ else if (instruction.match(/node.?type/i)) targetDir = 'src/node-types';
57
+ else if (instruction.match(/workflow/i)) targetDir = 'src/workflows';
58
+
59
+ if (!targetDir) {
60
+ return { decomposed: false, tasks: [task] };
61
+ }
62
+
63
+ const absDir = path.resolve(projectDir, targetDir);
64
+ if (!fs.existsSync(absDir)) {
65
+ return { decomposed: false, tasks: [task] };
66
+ }
67
+
68
+ // List .ts files in the directory — only files that actually exist
69
+ let files: string[];
70
+ try {
71
+ files = fs.readdirSync(absDir)
72
+ .filter(f => f.endsWith('.ts') && !f.startsWith('index'))
73
+ .filter(f => fs.existsSync(path.resolve(absDir, f))) // verify file exists
74
+ .sort();
75
+ } catch {
76
+ return { decomposed: false, tasks: [task] };
77
+ }
78
+
79
+ if (files.length === 0 || files.length > 50) {
80
+ return { decomposed: false, tasks: [task] };
81
+ }
82
+
83
+ // Extract the verb from the original instruction for clean per-file instructions
84
+ const verb = instruction.match(/^(Fix|Validate|Add|Update|Review|Run|Check|Test|Improve)/i)?.[0] ?? 'Process';
85
+
86
+ // Create per-file tasks with clean, grammatical instructions
87
+ const tasks: DecomposableTask[] = files.map((file, i) => {
88
+ const filePath = path.join(targetDir!, file);
89
+
90
+ return {
91
+ id: `${task.id}-${i + 1}`,
92
+ instruction: `${verb} ${filePath}`,
93
+ mode: task.mode ?? 'modify',
94
+ targets: [filePath],
95
+ priority: task.priority ?? 0,
96
+ };
97
+ });
98
+
99
+ return { decomposed: true, tasks };
100
+ }
@@ -13,19 +13,67 @@ export interface QueuedTask {
13
13
  options?: Record<string, unknown>;
14
14
  priority: number;
15
15
  addedAt: number;
16
- status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
16
+ status: 'pending' | 'running' | 'completed' | 'no-op' | 'failed' | 'cancelled';
17
+ /** Error reason (set on failure) */
18
+ failureReason?: string;
19
+ }
20
+
21
+ export interface AddResult {
22
+ id: string;
23
+ duplicate: boolean;
24
+ }
25
+
26
+ /** Max pending tasks before queue rejects new additions. */
27
+ const MAX_PENDING = 200;
28
+ /** Don't re-queue tasks completed within this window (ms). */
29
+ const CYCLE_DEDUP_WINDOW = 3600_000; // 1 hour
30
+
31
+ /**
32
+ * Hash a project directory path into a short filesystem-safe string.
33
+ * Used for per-project queue isolation.
34
+ */
35
+ function hashDir(dir: string): string {
36
+ return crypto.createHash('sha256').update(dir).digest('hex').slice(0, 8);
17
37
  }
18
38
 
19
39
  export class TaskQueue {
20
- private filePath: string;
40
+ readonly filePath: string;
21
41
 
22
42
  constructor(dir?: string) {
23
- const base = dir ?? path.join(os.homedir(), '.weaver');
43
+ // Priority: explicit dir > env var > project-scoped > global fallback
44
+ const projectDir = process.env.WEAVER_PROJECT_DIR;
45
+ const base = dir
46
+ ?? process.env.WEAVER_QUEUE_DIR
47
+ ?? (projectDir
48
+ ? path.join(os.homedir(), '.weaver', 'projects', hashDir(projectDir))
49
+ : path.join(os.homedir(), '.weaver'));
24
50
  this.filePath = path.join(base, 'task-queue.ndjson');
25
51
  }
26
52
 
27
- async add(task: Omit<QueuedTask, 'id' | 'addedAt' | 'status'>): Promise<string> {
53
+ async add(task: Omit<QueuedTask, 'id' | 'addedAt' | 'status'>): Promise<AddResult> {
28
54
  return withFileLock(this.filePath, () => {
55
+ const existing = this.readAll();
56
+
57
+ // Dedup: skip if a pending task with the same instruction exists
58
+ const pendingDup = existing.find(
59
+ t => t.status === 'pending' && t.instruction === task.instruction,
60
+ );
61
+ if (pendingDup) return { id: pendingDup.id, duplicate: true };
62
+
63
+ // Cycle-aware dedup: skip if same instruction was completed recently
64
+ const recentDup = existing.find(
65
+ t => (t.status === 'completed' || t.status === 'no-op')
66
+ && t.instruction === task.instruction
67
+ && Date.now() - t.addedAt < CYCLE_DEDUP_WINDOW,
68
+ );
69
+ if (recentDup) return { id: recentDup.id, duplicate: true };
70
+
71
+ // Queue size cap
72
+ const pendingCount = existing.filter(t => t.status === 'pending').length;
73
+ if (pendingCount >= MAX_PENDING) {
74
+ throw new Error(`Queue full (${MAX_PENDING} pending tasks). Clear or process existing tasks first.`);
75
+ }
76
+
29
77
  const entry: QueuedTask = {
30
78
  ...task,
31
79
  id: crypto.randomUUID().slice(0, 8),
@@ -35,7 +83,7 @@ export class TaskQueue {
35
83
  const dir = path.dirname(this.filePath);
36
84
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
37
85
  fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf-8');
38
- return entry.id;
86
+ return { id: entry.id, duplicate: false };
39
87
  });
40
88
  }
41
89
 
@@ -78,8 +126,66 @@ export class TaskQueue {
78
126
  await this.updateStatus(id, 'completed');
79
127
  }
80
128
 
81
- async markFailed(id: string): Promise<void> {
82
- await this.updateStatus(id, 'failed');
129
+ async markNoOp(id: string): Promise<void> {
130
+ await this.updateStatus(id, 'no-op');
131
+ }
132
+
133
+ async markFailed(id: string, reason?: string): Promise<void> {
134
+ return withFileLock(this.filePath, () => {
135
+ const tasks = this.readAll();
136
+ const task = tasks.find(t => t.id === id);
137
+ if (task) {
138
+ task.status = 'failed';
139
+ if (reason) task.failureReason = reason.slice(0, 500);
140
+ this.writeAll(tasks);
141
+ }
142
+ });
143
+ }
144
+
145
+ /** Reset a failed or running task back to pending. */
146
+ async retry(id: string): Promise<boolean> {
147
+ return withFileLock(this.filePath, () => {
148
+ const tasks = this.readAll();
149
+ const task = tasks.find(t => t.id === id && (t.status === 'failed' || t.status === 'running'));
150
+ if (!task) return false;
151
+ task.status = 'pending';
152
+ task.failureReason = undefined;
153
+ this.writeAll(tasks);
154
+ return true;
155
+ });
156
+ }
157
+
158
+ /** Reset ALL failed tasks back to pending. Returns count reset. */
159
+ async retryAll(): Promise<number> {
160
+ return withFileLock(this.filePath, () => {
161
+ const tasks = this.readAll();
162
+ let count = 0;
163
+ for (const t of tasks) {
164
+ if (t.status === 'failed') {
165
+ t.status = 'pending';
166
+ t.failureReason = undefined;
167
+ count++;
168
+ }
169
+ }
170
+ if (count > 0) this.writeAll(tasks);
171
+ return count;
172
+ });
173
+ }
174
+
175
+ /** Reset orphaned "running" tasks to pending (crash recovery). */
176
+ async recoverOrphans(): Promise<number> {
177
+ return withFileLock(this.filePath, () => {
178
+ const tasks = this.readAll();
179
+ let count = 0;
180
+ for (const t of tasks) {
181
+ if (t.status === 'running') {
182
+ t.status = 'pending';
183
+ count++;
184
+ }
185
+ }
186
+ if (count > 0) this.writeAll(tasks);
187
+ return count;
188
+ });
83
189
  }
84
190
 
85
191
  private async updateStatus(id: string, status: QueuedTask['status']): Promise<void> {
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Terminal renderer — centralizes all weaver CLI output through
3
+ * a consistent visual grammar. Pipe-safe (stderr only for decoration).
4
+ *
5
+ * Icons: ✓ success · ✗ error · ⚠ warning · ◆ action · ● running
6
+ * Colors: green/red/yellow/cyan/dim/bold — strict assignments
7
+ */
8
+
9
+ import type { StreamEvent, ToolEvent } from '@synergenius/flow-weaver/agent';
10
+
11
+ // ANSI helpers
12
+ const isTTY = process.stderr.isTTY ?? false;
13
+ const c = {
14
+ green: (s: string) => `\x1b[32m${s}\x1b[0m`,
15
+ red: (s: string) => `\x1b[31m${s}\x1b[0m`,
16
+ yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
17
+ cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
18
+ dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
19
+ bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
20
+ redBold: (s: string) => `\x1b[1;31m${s}\x1b[0m`,
21
+ };
22
+
23
+ export interface RendererOptions {
24
+ verbose?: boolean;
25
+ quiet?: boolean;
26
+ noColor?: boolean;
27
+ /** Override stderr writer (for testing) */
28
+ write?: (s: string) => void;
29
+ }
30
+
31
+ export interface TaskEndStats {
32
+ toolCalls: number;
33
+ inputTokens: number;
34
+ outputTokens: number;
35
+ estimatedCost: number;
36
+ filesModified: number;
37
+ elapsed: number;
38
+ gitMessage?: string;
39
+ }
40
+
41
+ export interface SessionEndStats {
42
+ tasks: number;
43
+ completed: number;
44
+ failed: number;
45
+ totalInputTokens: number;
46
+ totalOutputTokens: number;
47
+ totalCost: number;
48
+ elapsed: number;
49
+ }
50
+
51
+ export class TerminalRenderer {
52
+ private verbose: boolean;
53
+ private quiet: boolean;
54
+ private out: (s: string) => void;
55
+ private taskStartTime = 0;
56
+ private lastToolStartTime = 0;
57
+ private textBuffer = '';
58
+ private hasActiveText = false;
59
+
60
+ constructor(opts: RendererOptions = {}) {
61
+ this.verbose = opts.verbose ?? false;
62
+ this.quiet = opts.quiet ?? false;
63
+ if (opts.noColor) {
64
+ // Strip all color functions
65
+ for (const key of Object.keys(c) as (keyof typeof c)[]) {
66
+ (c as Record<string, (s: string) => string>)[key] = (s: string) => s;
67
+ }
68
+ }
69
+ this.out = opts.write ?? ((s: string) => process.stderr.write(s));
70
+ }
71
+
72
+ // --- Session lifecycle ---
73
+
74
+ sessionStart(info: { provider: string; parallel?: number; deadline?: string }): void {
75
+ if (this.quiet) return;
76
+ this.out(`${c.bold('[weaver]')} Session started ${c.dim('(Ctrl+C to stop)')}\n`);
77
+ const parts = [`Provider: ${info.provider}`];
78
+ if (info.parallel && info.parallel > 1) parts.push(`Parallel: ${info.parallel}`);
79
+ if (info.deadline) parts.push(`Deadline: ${info.deadline}`);
80
+ this.out(`${c.bold('[weaver]')} ${c.dim(parts.join(' · '))}\n`);
81
+ }
82
+
83
+ sessionEnd(stats: SessionEndStats): void {
84
+ if (this.quiet) return;
85
+ this.out('\n');
86
+ const parts: string[] = [];
87
+ parts.push(`${stats.tasks} task${stats.tasks === 1 ? '' : 's'}`);
88
+ if (stats.completed > 0) parts.push(c.green(`${stats.completed} completed`));
89
+ if (stats.failed > 0) parts.push(c.red(`${stats.failed} failed`));
90
+ const skipped = stats.tasks - stats.completed - stats.failed;
91
+ if (skipped > 0) parts.push(c.yellow(`${skipped} skipped`));
92
+ this.out(`${c.bold('[weaver]')} Session complete: ${parts.join(' · ')}\n`);
93
+
94
+ const totalTokens = stats.totalInputTokens + stats.totalOutputTokens;
95
+ if (totalTokens > 0) {
96
+ this.out(`${c.bold('[weaver]')} ${c.dim(`Total: ${formatTokens(totalTokens)} tokens · $${stats.totalCost.toFixed(3)} · ${formatElapsed(stats.elapsed)}`)}\n`);
97
+ }
98
+ }
99
+
100
+ // --- Task lifecycle ---
101
+
102
+ taskStart(index: number, instruction: string): void {
103
+ if (this.quiet) return;
104
+ this.taskStartTime = Date.now();
105
+ this.hasActiveText = false;
106
+ this.textBuffer = '';
107
+ const label = instruction.length > 70 ? instruction.slice(0, 67) + '...' : instruction;
108
+ this.out(`\n${c.cyan('◆')} ${c.bold(`Task ${index}:`)} ${label}\n`);
109
+ }
110
+
111
+ taskEnd(success: boolean, stats: TaskEndStats): void {
112
+ if (this.quiet) return;
113
+ // Flush any remaining text
114
+ this.flushText();
115
+
116
+ const elapsed = formatElapsed(stats.elapsed);
117
+ const icon = success ? c.green('✓') : c.red('✗');
118
+ const status = success ? 'completed' : 'failed';
119
+ this.out(`${icon} Task ${status} ${c.dim(elapsed)}\n`);
120
+
121
+ // Summary line
122
+ const parts: string[] = [];
123
+ if (stats.toolCalls > 0) parts.push(`${stats.toolCalls} tool calls`);
124
+ const totalTokens = stats.inputTokens + stats.outputTokens;
125
+ if (totalTokens > 0) parts.push(`${formatTokens(totalTokens)} tokens`);
126
+ if (stats.estimatedCost > 0) parts.push(`$${stats.estimatedCost.toFixed(3)}`);
127
+ if (stats.filesModified > 0) parts.push(`${stats.filesModified} file${stats.filesModified === 1 ? '' : 's'} modified`);
128
+ if (parts.length > 0) {
129
+ this.out(` ${c.dim(parts.join(' · '))}\n`);
130
+ }
131
+
132
+ if (stats.gitMessage) {
133
+ this.out(` ${c.dim('→ Git: ' + stats.gitMessage)}\n`);
134
+ }
135
+ }
136
+
137
+ // --- Stream event handling ---
138
+
139
+ onStreamEvent(event: StreamEvent): void {
140
+ if (this.quiet) return;
141
+
142
+ switch (event.type) {
143
+ case 'thinking_delta':
144
+ if (this.verbose) {
145
+ // In verbose mode, stream thinking as dim text
146
+ this.flushText();
147
+ this.out(` ${c.dim(event.text.replace(/\n/g, '\n '))}`);
148
+ }
149
+ // In normal mode, thinking is completely hidden
150
+ break;
151
+
152
+ case 'text_delta':
153
+ if (this.verbose) {
154
+ this.flushText();
155
+ this.out(event.text);
156
+ this.hasActiveText = true;
157
+ }
158
+ // In normal mode, AI text is hidden (tool calls tell the story)
159
+ break;
160
+
161
+ case 'tool_result':
162
+ // CLI internal tool result — show as result line
163
+ break;
164
+
165
+ default:
166
+ break;
167
+ }
168
+ }
169
+
170
+ onToolEvent(event: ToolEvent): void {
171
+ if (this.quiet) return;
172
+
173
+ if (event.type === 'tool_call_start') {
174
+ this.flushText();
175
+ this.lastToolStartTime = Date.now();
176
+ const args = event.args ?? {};
177
+ const preview = toolPreview(event.name, args);
178
+ this.out(` ${c.cyan('◆')} ${event.name}${preview ? c.dim(`(${preview})`) : ''}\n`);
179
+ }
180
+
181
+ if (event.type === 'tool_call_result') {
182
+ const elapsed = Date.now() - this.lastToolStartTime;
183
+ const result = (event.result ?? '').replace(/\n/g, ' ').slice(0, 120);
184
+ const icon = event.isError ? c.red('✗') : c.dim('→');
185
+ this.out(` ${icon} ${result} ${c.dim(formatElapsed(elapsed))}\n`);
186
+ }
187
+ }
188
+
189
+ // --- Direct messages ---
190
+
191
+ info(msg: string): void {
192
+ if (!this.quiet) this.out(`${c.bold('[weaver]')} ${msg}\n`);
193
+ }
194
+
195
+ warn(msg: string): void {
196
+ if (!this.quiet) this.out(`${c.yellow('⚠')} ${msg}\n`);
197
+ }
198
+
199
+ error(title: string, detail?: string): void {
200
+ this.out(`${c.redBold('✗')} ${c.red(title)}\n`);
201
+ if (detail) this.out(` ${c.red(detail)}\n`);
202
+ }
203
+
204
+ // --- Private ---
205
+
206
+ private flushText(): void {
207
+ if (this.hasActiveText) {
208
+ this.out('\n');
209
+ this.hasActiveText = false;
210
+ }
211
+ }
212
+ }
213
+
214
+ // --- Formatting helpers ---
215
+
216
+ export function formatTokens(n: number): string {
217
+ if (n < 1000) return String(n);
218
+ if (n < 1_000_000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
219
+ return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
220
+ }
221
+
222
+ export function formatElapsed(ms: number): string {
223
+ if (ms < 1000) return `${ms}ms`;
224
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
225
+ const m = Math.floor(ms / 60_000);
226
+ const s = Math.round((ms % 60_000) / 1000);
227
+ return s > 0 ? `${m}m ${s}s` : `${m}m`;
228
+ }
229
+
230
+ function toolPreview(name: string, args: Record<string, unknown>): string {
231
+ if (args.file) return String(args.file).split('/').pop() ?? '';
232
+ if (args.command) return String(args.command).slice(0, 50);
233
+ if (args.directory) return String(args.directory).split('/').pop() ?? '';
234
+ // For patch_file, show file + patch count
235
+ if (name === 'patch_file' && args.patches) {
236
+ const file = String(args.file ?? '').split('/').pop() ?? '';
237
+ const count = Array.isArray(args.patches) ? args.patches.length : '?';
238
+ return `${file}, ${count} patches`;
239
+ }
240
+ return '';
241
+ }
package/src/bot/types.ts CHANGED
@@ -171,6 +171,12 @@ export interface WorkflowResult {
171
171
 
172
172
  export type RunOutcome = 'completed' | 'failed' | 'error' | 'skipped';
173
173
 
174
+ export interface StepLogEntry {
175
+ step: string;
176
+ status: 'ok' | 'blocked' | 'error';
177
+ detail?: string;
178
+ }
179
+
174
180
  export interface RunRecord {
175
181
  id: string;
176
182
  workflowFile: string;
@@ -187,6 +193,7 @@ export interface RunRecord {
187
193
  provider?: string;
188
194
  pipelineName?: string;
189
195
  stageName?: string;
196
+ stepLog?: StepLogEntry[];
190
197
  }
191
198
 
192
199
  export interface RunFilter {
@@ -494,6 +501,7 @@ export interface WeaverContext {
494
501
  resultJson?: string;
495
502
  validationResultJson?: string;
496
503
  filesModified?: string;
504
+ stepLogJson?: string;
497
505
  allValid?: boolean;
498
506
  gitResultJson?: string;
499
507
  }