@synergenius/flow-weaver-pack-weaver 0.9.0 → 0.9.4
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.
- package/dist/bot/ai-client.d.ts +22 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +168 -20
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/ansi.d.ts +13 -0
- package/dist/bot/ansi.d.ts.map +1 -0
- package/dist/bot/ansi.js +13 -0
- package/dist/bot/ansi.js.map +1 -0
- package/dist/bot/assistant-core.d.ts +25 -0
- package/dist/bot/assistant-core.d.ts.map +1 -0
- package/dist/bot/assistant-core.js +272 -0
- package/dist/bot/assistant-core.js.map +1 -0
- package/dist/bot/assistant-tools.d.ts +10 -0
- package/dist/bot/assistant-tools.d.ts.map +1 -0
- package/dist/bot/assistant-tools.js +324 -0
- package/dist/bot/assistant-tools.js.map +1 -0
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +9 -5
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +49 -0
- package/dist/bot/bot-manager.d.ts.map +1 -0
- package/dist/bot/bot-manager.js +279 -0
- package/dist/bot/bot-manager.js.map +1 -0
- package/dist/bot/child-process-tracker.d.ts +6 -0
- package/dist/bot/child-process-tracker.d.ts.map +1 -0
- package/dist/bot/child-process-tracker.js +35 -0
- package/dist/bot/child-process-tracker.js.map +1 -0
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +13 -8
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +40 -0
- package/dist/bot/conversation-store.d.ts.map +1 -0
- package/dist/bot/conversation-store.js +182 -0
- package/dist/bot/conversation-store.js.map +1 -0
- package/dist/bot/error-classifier.d.ts +27 -0
- package/dist/bot/error-classifier.d.ts.map +1 -0
- package/dist/bot/error-classifier.js +71 -0
- package/dist/bot/error-classifier.js.map +1 -0
- package/dist/bot/error-guide.d.ts +5 -0
- package/dist/bot/error-guide.d.ts.map +1 -0
- package/dist/bot/error-guide.js +5 -0
- package/dist/bot/error-guide.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +17 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -0
- package/dist/bot/knowledge-store.js +53 -0
- package/dist/bot/knowledge-store.js.map +1 -0
- package/dist/bot/paths.d.ts +11 -0
- package/dist/bot/paths.d.ts.map +1 -0
- package/dist/bot/paths.js +26 -0
- package/dist/bot/paths.js.map +1 -0
- package/dist/bot/retry-utils.d.ts +5 -0
- package/dist/bot/retry-utils.d.ts.map +1 -0
- package/dist/bot/retry-utils.js +5 -0
- package/dist/bot/retry-utils.js.map +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +12 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -0
- package/dist/bot/safety.d.ts.map +1 -0
- package/dist/bot/safety.js +14 -0
- package/dist/bot/safety.js.map +1 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +3 -1
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/steering.js +2 -2
- package/dist/bot/steering.js.map +1 -1
- package/dist/bot/step-executor.d.ts +10 -5
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +252 -3
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +69 -43
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-decomposer.d.ts +24 -0
- package/dist/bot/task-decomposer.d.ts.map +1 -0
- package/dist/bot/task-decomposer.js +75 -0
- package/dist/bot/task-decomposer.js.map +1 -0
- package/dist/bot/task-queue.d.ts +17 -4
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +83 -5
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/terminal-renderer.d.ts +60 -0
- package/dist/bot/terminal-renderer.d.ts.map +1 -0
- package/dist/bot/terminal-renderer.js +204 -0
- package/dist/bot/terminal-renderer.js.map +1 -0
- package/dist/bot/tool-registry.d.ts +24 -0
- package/dist/bot/tool-registry.d.ts.map +1 -0
- package/dist/bot/tool-registry.js +458 -0
- package/dist/bot/tool-registry.js.map +1 -0
- package/dist/bot/types.d.ts +7 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +18 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -0
- package/dist/bot/weaver-tools.js +124 -0
- package/dist/bot/weaver-tools.js.map +1 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +5 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +13 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +615 -48
- package/dist/cli-handlers.js.map +1 -1
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/abort-task.d.ts.map +1 -1
- package/dist/node-types/abort-task.js +4 -3
- package/dist/node-types/abort-task.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +38 -0
- package/dist/node-types/agent-execute.d.ts.map +1 -0
- package/dist/node-types/agent-execute.js +252 -0
- package/dist/node-types/agent-execute.js.map +1 -0
- package/dist/node-types/bot-report.d.ts +5 -3
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +39 -7
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts +3 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +108 -24
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/detect-provider.d.ts +2 -2
- package/dist/node-types/detect-provider.d.ts.map +1 -1
- package/dist/node-types/detect-provider.js +3 -1
- package/dist/node-types/detect-provider.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +43 -6
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/execute-plan.d.ts.map +1 -1
- package/dist/node-types/execute-plan.js +31 -8
- package/dist/node-types/execute-plan.js.map +1 -1
- package/dist/node-types/execute-target.d.ts.map +1 -1
- package/dist/node-types/execute-target.js +3 -1
- package/dist/node-types/execute-target.js.map +1 -1
- package/dist/node-types/fix-errors.d.ts.map +1 -1
- package/dist/node-types/fix-errors.js +21 -5
- package/dist/node-types/fix-errors.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +3 -1
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-report.js +4 -1
- package/dist/node-types/genesis-report.js.map +1 -1
- package/dist/node-types/git-ops.d.ts.map +1 -1
- package/dist/node-types/git-ops.js +98 -4
- package/dist/node-types/git-ops.js.map +1 -1
- package/dist/node-types/index.d.ts +2 -0
- package/dist/node-types/index.d.ts.map +1 -1
- package/dist/node-types/index.js +2 -0
- package/dist/node-types/index.js.map +1 -1
- package/dist/node-types/load-config.d.ts +2 -2
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +14 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/read-workflow.js +8 -2
- package/dist/node-types/read-workflow.js.map +1 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +35 -26
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/send-notify.js +2 -1
- package/dist/node-types/send-notify.js.map +1 -1
- package/dist/node-types/validate-gate.d.ts +18 -0
- package/dist/node-types/validate-gate.d.ts.map +1 -0
- package/dist/node-types/validate-gate.js +96 -0
- package/dist/node-types/validate-gate.js.map +1 -0
- package/dist/workflows/genesis-task.d.ts +20 -12
- package/dist/workflows/genesis-task.d.ts.map +1 -1
- package/dist/workflows/genesis-task.js +20 -12
- package/dist/workflows/genesis-task.js.map +1 -1
- package/dist/workflows/weaver-agent.d.ts +35 -0
- package/dist/workflows/weaver-agent.d.ts.map +1 -0
- package/dist/workflows/weaver-agent.js +777 -0
- package/dist/workflows/weaver-agent.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.d.ts +19 -26
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +1043 -27
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +21 -35
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +1119 -36
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +21 -1
- package/package.json +5 -2
- package/src/bot/ai-client.ts +180 -19
- package/src/bot/ansi.ts +12 -0
- package/src/bot/assistant-core.ts +312 -0
- package/src/bot/assistant-tools.ts +318 -0
- package/src/bot/audit-logger.ts +6 -5
- package/src/bot/bot-manager.ts +293 -0
- package/src/bot/child-process-tracker.ts +40 -0
- package/src/bot/cli-provider.ts +13 -8
- package/src/bot/conversation-store.ts +222 -0
- package/src/bot/error-classifier.ts +90 -0
- package/src/bot/error-guide.ts +4 -0
- package/src/bot/knowledge-store.ts +59 -0
- package/src/bot/paths.ts +27 -0
- package/src/bot/retry-utils.ts +4 -0
- package/src/bot/runner.ts +12 -1
- package/src/bot/safety.ts +16 -0
- package/src/bot/session-state.ts +2 -1
- package/src/bot/steering.ts +2 -2
- package/src/bot/step-executor.ts +313 -5
- package/src/bot/system-prompt.ts +70 -47
- package/src/bot/task-decomposer.ts +100 -0
- package/src/bot/task-queue.ts +100 -8
- package/src/bot/terminal-renderer.ts +238 -0
- package/src/bot/tool-registry.ts +477 -0
- package/src/bot/types.ts +8 -0
- package/src/bot/weaver-tools.ts +134 -0
- package/src/cli-bridge.ts +7 -1
- package/src/cli-handlers.ts +624 -48
- package/src/mcp-tools.ts +2 -2
- package/src/node-types/abort-task.ts +5 -4
- package/src/node-types/agent-execute.ts +303 -0
- package/src/node-types/bot-report.ts +40 -9
- package/src/node-types/build-context.ts +112 -25
- package/src/node-types/detect-provider.ts +4 -3
- package/src/node-types/exec-validate-retry.ts +47 -8
- package/src/node-types/execute-plan.ts +32 -8
- package/src/node-types/execute-target.ts +2 -1
- package/src/node-types/fix-errors.ts +20 -5
- package/src/node-types/genesis-observe.ts +2 -1
- package/src/node-types/genesis-report.ts +1 -1
- package/src/node-types/git-ops.ts +93 -4
- package/src/node-types/index.ts +2 -0
- package/src/node-types/load-config.ts +3 -3
- package/src/node-types/plan-task.ts +15 -3
- package/src/node-types/read-workflow.ts +2 -2
- package/src/node-types/receive-task.ts +31 -26
- package/src/node-types/send-notify.ts +1 -1
- package/src/node-types/validate-gate.ts +112 -0
- package/src/workflows/genesis-task.ts +20 -12
- package/src/workflows/weaver-agent.ts +799 -0
- package/src/workflows/weaver-bot-batch.ts +1049 -27
- package/src/workflows/weaver-bot.ts +1123 -36
package/src/bot/system-prompt.ts
CHANGED
|
@@ -177,9 +177,38 @@ Genesis is a 17-step self-evolving workflow engine:
|
|
|
177
177
|
|
|
178
178
|
When stabilize mode is active, only fix-up operations are allowed: removeNode, removeConnection, implementNode. No new nodes or connections.
|
|
179
179
|
|
|
180
|
-
##
|
|
180
|
+
## Tool Use
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
You have tools available: validate, read_file, patch_file, run_shell, list_files, write_file.
|
|
183
|
+
|
|
184
|
+
USE TOOLS to complete tasks. Do NOT describe what you would do — actually do it by calling tools. You can see tool results and decide your next action dynamically.
|
|
185
|
+
|
|
186
|
+
Workflow for fixing validation errors:
|
|
187
|
+
1. Call validate(file) to see exact errors
|
|
188
|
+
2. Call read_file(file) to see the code
|
|
189
|
+
3. Call patch_file(file, patches) with exact find/replace strings
|
|
190
|
+
4. Call validate(file) again to confirm fixes
|
|
191
|
+
5. Repeat if errors remain
|
|
192
|
+
|
|
193
|
+
Rules:
|
|
194
|
+
- Always validate BEFORE and AFTER patching
|
|
195
|
+
- Always read a file before patching it (you need exact strings for find/replace)
|
|
196
|
+
- Use patch_file for modifications, write_file only for new files
|
|
197
|
+
- Be concise in your text responses — let tool results speak
|
|
198
|
+
|
|
199
|
+
Flow Weaver workflows are TypeScript. You can also help create supporting files in other formats (JSON configs, shell scripts, Markdown docs).
|
|
200
|
+
|
|
201
|
+
Before starting a task on a file, call recall(filename) to check if there is stored knowledge about known issues or patterns for that file.
|
|
202
|
+
After discovering something important (a pattern, a common fix, a gotcha), call learn(key, value) to store it for future tasks.
|
|
203
|
+
|
|
204
|
+
## Teaching
|
|
205
|
+
|
|
206
|
+
When creating or modifying workflows, briefly explain your decisions:
|
|
207
|
+
- Why you chose a particular template or pattern (1 line)
|
|
208
|
+
- What each node does and why it is @expression vs standard (1 line)
|
|
209
|
+
- What the data flow looks like (1 line)
|
|
210
|
+
Do NOT lecture. Keep explanations short. The user is learning Flow Weaver by watching you work.
|
|
211
|
+
Example: "Using sequential template — best for linear pipelines. The validator is @expression (pure, no side effects). Data flows: input -> validate -> transform -> output."`;
|
|
183
212
|
}
|
|
184
213
|
|
|
185
214
|
export async function buildSystemPrompt(): Promise<string> {
|
|
@@ -205,11 +234,25 @@ export async function buildSystemPrompt(): Promise<string> {
|
|
|
205
234
|
|
|
206
235
|
function formatBotOperations(cliCommands: CliCommandDoc[]): string {
|
|
207
236
|
const packOps = [
|
|
237
|
+
'## File Operations',
|
|
208
238
|
'- create-workflow: Create a new workflow file. args: { file, content }',
|
|
209
239
|
'- implement-node: Write a node type implementation. args: { file, content }',
|
|
210
|
-
'-
|
|
211
|
-
'- read-file: Read a file
|
|
212
|
-
'-
|
|
240
|
+
'- write-file: Write a file. args: { file, content }. Content must be the COMPLETE file.',
|
|
241
|
+
'- read-file: Read a file and return its content. args: { file }',
|
|
242
|
+
'- patch-file: Surgical find-and-replace edits. args: { file, patches: [{ find: "old text", replace: "new text" }] }. PREFERRED for modifying existing files — no need to rewrite the entire file.',
|
|
243
|
+
'- list-files: List files in a directory. args: { directory, pattern? } (pattern is regex)',
|
|
244
|
+
'',
|
|
245
|
+
'## Shell Commands',
|
|
246
|
+
'- run-shell: Execute a shell command and return output. args: { command }. Use for: npx vitest, git status, grep, find, etc.',
|
|
247
|
+
' Examples: { "command": "npx vitest run --reporter verbose" }, { "command": "npx flow-weaver validate src/workflow.ts --json" }',
|
|
248
|
+
' Blocked: rm -rf, git push, npm publish, sudo, curl|sh (safety policy).',
|
|
249
|
+
'',
|
|
250
|
+
'## Best Practices',
|
|
251
|
+
'PREFER patch-file over write-file for modifying existing files (surgical edits, no truncation risk).',
|
|
252
|
+
'Use run-shell for running tests (npx vitest), validation (flow-weaver validate), and inspecting output.',
|
|
253
|
+
'Use read-file to understand a file before modifying it.',
|
|
254
|
+
'Use list-files to discover project structure.',
|
|
255
|
+
'Writes that shrink a file by >50% or write empty content are automatically BLOCKED.',
|
|
213
256
|
];
|
|
214
257
|
|
|
215
258
|
const fwOps = cliCommands
|
|
@@ -229,48 +272,28 @@ function formatBotOperations(cliCommands: CliCommandDoc[]): string {
|
|
|
229
272
|
return [...packOps, ...fwOps].join('\n');
|
|
230
273
|
}
|
|
231
274
|
|
|
232
|
-
export function buildBotSystemPrompt(contextBundle?: string,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
You are operating in autonomous bot mode. Your job is to plan and execute workflow creation or modification tasks.
|
|
256
|
-
|
|
257
|
-
When planning:
|
|
258
|
-
1. Break the task into concrete, ordered steps using the plan schema above
|
|
259
|
-
2. For new workflows, plan: scaffold/create -> implement nodes -> compile -> validate
|
|
260
|
-
3. For modifications, plan: read current state -> modify -> compile -> validate
|
|
261
|
-
4. Each step is executed via the flow-weaver programmatic API
|
|
262
|
-
5. Use templates when they match the task
|
|
263
|
-
6. Prefer @expression nodes for deterministic operations
|
|
264
|
-
7. Use proper JSDoc annotations on all node types and workflows
|
|
265
|
-
8. Include visualization metadata (colors, icons, positions) on workflow nodes
|
|
266
|
-
|
|
267
|
-
When fixing validation errors:
|
|
268
|
-
1. Read the error messages carefully
|
|
269
|
-
2. Map each error to a specific fix operation
|
|
270
|
-
3. Common fixes: add missing connections, fix port names, resolve type mismatches
|
|
271
|
-
4. Return a new plan with only the fix steps`;
|
|
272
|
-
|
|
273
|
-
let prompt = planSchema + '\n\n' + botInstructions;
|
|
275
|
+
export function buildBotSystemPrompt(contextBundle?: string, _cliCommands?: CliCommandDoc[], projectDir?: string): string {
|
|
276
|
+
let prompt = `## Safety Policy
|
|
277
|
+
|
|
278
|
+
Writes that shrink a file by >50% or write empty content are automatically BLOCKED.
|
|
279
|
+
Blocked shell commands: rm -rf, git push, npm publish, sudo, curl|sh.
|
|
280
|
+
Always validate BEFORE and AFTER patching.
|
|
281
|
+
Always read a file before patching it (you need exact strings for find/replace).
|
|
282
|
+
Use patch_file for modifications, write_file only for new files.
|
|
283
|
+
Be concise in your text responses — let tool results speak.`;
|
|
284
|
+
|
|
285
|
+
// Load project plan file if it exists — this is the vision spec that guides all work
|
|
286
|
+
if (projectDir) {
|
|
287
|
+
try {
|
|
288
|
+
const fs = require('node:fs');
|
|
289
|
+
const path = require('node:path');
|
|
290
|
+
const planPath = path.resolve(projectDir, '.weaver-plan.md');
|
|
291
|
+
if (fs.existsSync(planPath)) {
|
|
292
|
+
const plan = fs.readFileSync(planPath, 'utf-8').trim();
|
|
293
|
+
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
|
+
} catch { /* plan file not available */ }
|
|
296
|
+
}
|
|
274
297
|
|
|
275
298
|
if (contextBundle) {
|
|
276
299
|
prompt += '\n\n## Project Context\n\n' + contextBundle;
|
|
@@ -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
|
+
}
|
package/src/bot/task-queue.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import * as os from 'node:os';
|
|
4
3
|
import * as crypto from 'node:crypto';
|
|
5
4
|
import { withFileLock } from './file-lock.js';
|
|
6
5
|
import { parseNdjson } from './safe-json.js';
|
|
6
|
+
import { resolveWeaverDir } from './paths.js';
|
|
7
7
|
|
|
8
8
|
export interface QueuedTask {
|
|
9
9
|
id: string;
|
|
@@ -13,19 +13,53 @@ 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;
|
|
17
19
|
}
|
|
18
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
|
+
|
|
19
31
|
export class TaskQueue {
|
|
20
|
-
|
|
32
|
+
readonly filePath: string;
|
|
21
33
|
|
|
22
34
|
constructor(dir?: string) {
|
|
23
|
-
const base = dir ??
|
|
35
|
+
const base = dir ?? resolveWeaverDir();
|
|
24
36
|
this.filePath = path.join(base, 'task-queue.ndjson');
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
async add(task: Omit<QueuedTask, 'id' | 'addedAt' | 'status'>): Promise<
|
|
39
|
+
async add(task: Omit<QueuedTask, 'id' | 'addedAt' | 'status'>): Promise<AddResult> {
|
|
28
40
|
return withFileLock(this.filePath, () => {
|
|
41
|
+
const existing = this.readAll();
|
|
42
|
+
|
|
43
|
+
// Dedup: skip if a pending task with the same instruction exists
|
|
44
|
+
const pendingDup = existing.find(
|
|
45
|
+
t => t.status === 'pending' && t.instruction === task.instruction,
|
|
46
|
+
);
|
|
47
|
+
if (pendingDup) return { id: pendingDup.id, duplicate: true };
|
|
48
|
+
|
|
49
|
+
// Cycle-aware dedup: skip if same instruction was completed recently
|
|
50
|
+
const recentDup = existing.find(
|
|
51
|
+
t => (t.status === 'completed' || t.status === 'no-op')
|
|
52
|
+
&& t.instruction === task.instruction
|
|
53
|
+
&& Date.now() - t.addedAt < CYCLE_DEDUP_WINDOW,
|
|
54
|
+
);
|
|
55
|
+
if (recentDup) return { id: recentDup.id, duplicate: true };
|
|
56
|
+
|
|
57
|
+
// Queue size cap
|
|
58
|
+
const pendingCount = existing.filter(t => t.status === 'pending').length;
|
|
59
|
+
if (pendingCount >= MAX_PENDING) {
|
|
60
|
+
throw new Error(`Queue full (${MAX_PENDING} pending tasks). Clear or process existing tasks first.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
const entry: QueuedTask = {
|
|
30
64
|
...task,
|
|
31
65
|
id: crypto.randomUUID().slice(0, 8),
|
|
@@ -35,7 +69,7 @@ export class TaskQueue {
|
|
|
35
69
|
const dir = path.dirname(this.filePath);
|
|
36
70
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
37
71
|
fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
38
|
-
return entry.id;
|
|
72
|
+
return { id: entry.id, duplicate: false };
|
|
39
73
|
});
|
|
40
74
|
}
|
|
41
75
|
|
|
@@ -78,8 +112,66 @@ export class TaskQueue {
|
|
|
78
112
|
await this.updateStatus(id, 'completed');
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
async
|
|
82
|
-
await this.updateStatus(id, '
|
|
115
|
+
async markNoOp(id: string): Promise<void> {
|
|
116
|
+
await this.updateStatus(id, 'no-op');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async markFailed(id: string, reason?: string): Promise<void> {
|
|
120
|
+
return withFileLock(this.filePath, () => {
|
|
121
|
+
const tasks = this.readAll();
|
|
122
|
+
const task = tasks.find(t => t.id === id);
|
|
123
|
+
if (task) {
|
|
124
|
+
task.status = 'failed';
|
|
125
|
+
if (reason) task.failureReason = reason.slice(0, 500);
|
|
126
|
+
this.writeAll(tasks);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Reset a failed or running task back to pending. */
|
|
132
|
+
async retry(id: string): Promise<boolean> {
|
|
133
|
+
return withFileLock(this.filePath, () => {
|
|
134
|
+
const tasks = this.readAll();
|
|
135
|
+
const task = tasks.find(t => t.id === id && (t.status === 'failed' || t.status === 'running'));
|
|
136
|
+
if (!task) return false;
|
|
137
|
+
task.status = 'pending';
|
|
138
|
+
task.failureReason = undefined;
|
|
139
|
+
this.writeAll(tasks);
|
|
140
|
+
return true;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Reset ALL failed tasks back to pending. Returns count reset. */
|
|
145
|
+
async retryAll(): Promise<number> {
|
|
146
|
+
return withFileLock(this.filePath, () => {
|
|
147
|
+
const tasks = this.readAll();
|
|
148
|
+
let count = 0;
|
|
149
|
+
for (const t of tasks) {
|
|
150
|
+
if (t.status === 'failed') {
|
|
151
|
+
t.status = 'pending';
|
|
152
|
+
t.failureReason = undefined;
|
|
153
|
+
count++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (count > 0) this.writeAll(tasks);
|
|
157
|
+
return count;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Reset orphaned "running" tasks to pending (crash recovery). */
|
|
162
|
+
async recoverOrphans(): Promise<number> {
|
|
163
|
+
return withFileLock(this.filePath, () => {
|
|
164
|
+
const tasks = this.readAll();
|
|
165
|
+
let count = 0;
|
|
166
|
+
for (const t of tasks) {
|
|
167
|
+
if (t.status === 'running') {
|
|
168
|
+
t.status = 'pending';
|
|
169
|
+
count++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (count > 0) this.writeAll(tasks);
|
|
173
|
+
return count;
|
|
174
|
+
});
|
|
83
175
|
}
|
|
84
176
|
|
|
85
177
|
private async updateStatus(id: string, status: QueuedTask['status']): Promise<void> {
|
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
import { c } from './ansi.js';
|
|
11
|
+
import { VERBOSE_TOOL_NAMES } from './tool-registry.js';
|
|
12
|
+
|
|
13
|
+
export interface RendererOptions {
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
quiet?: boolean;
|
|
16
|
+
noColor?: boolean;
|
|
17
|
+
/** Override stderr writer (for testing) */
|
|
18
|
+
write?: (s: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TaskEndStats {
|
|
22
|
+
toolCalls: number;
|
|
23
|
+
inputTokens: number;
|
|
24
|
+
outputTokens: number;
|
|
25
|
+
estimatedCost: number;
|
|
26
|
+
filesModified: number;
|
|
27
|
+
elapsed: number;
|
|
28
|
+
gitMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SessionEndStats {
|
|
32
|
+
tasks: number;
|
|
33
|
+
completed: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
totalInputTokens: number;
|
|
36
|
+
totalOutputTokens: number;
|
|
37
|
+
totalCost: number;
|
|
38
|
+
elapsed: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class TerminalRenderer {
|
|
42
|
+
private verbose: boolean;
|
|
43
|
+
private quiet: boolean;
|
|
44
|
+
private out: (s: string) => void;
|
|
45
|
+
private taskStartTime = 0;
|
|
46
|
+
private lastToolStartTime = 0;
|
|
47
|
+
private textBuffer = '';
|
|
48
|
+
private hasActiveText = false;
|
|
49
|
+
|
|
50
|
+
constructor(opts: RendererOptions = {}) {
|
|
51
|
+
this.verbose = opts.verbose ?? false;
|
|
52
|
+
this.quiet = opts.quiet ?? false;
|
|
53
|
+
if (opts.noColor) {
|
|
54
|
+
// Strip all color functions
|
|
55
|
+
for (const key of Object.keys(c) as (keyof typeof c)[]) {
|
|
56
|
+
(c as Record<string, (s: string) => string>)[key] = (s: string) => s;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.out = opts.write ?? ((s: string) => process.stderr.write(s));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Session lifecycle ---
|
|
63
|
+
|
|
64
|
+
sessionStart(info: { provider: string; parallel?: number; deadline?: string }): void {
|
|
65
|
+
if (this.quiet) return;
|
|
66
|
+
this.out(`${c.bold('[weaver]')} Session started ${c.dim('(Ctrl+C to stop)')}\n`);
|
|
67
|
+
const parts = [`Provider: ${info.provider}`];
|
|
68
|
+
if (info.parallel && info.parallel > 1) parts.push(`Parallel: ${info.parallel}`);
|
|
69
|
+
if (info.deadline) parts.push(`Deadline: ${info.deadline}`);
|
|
70
|
+
this.out(`${c.bold('[weaver]')} ${c.dim(parts.join(' · '))}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sessionEnd(stats: SessionEndStats): void {
|
|
74
|
+
if (this.quiet) return;
|
|
75
|
+
this.out('\n');
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
parts.push(`${stats.tasks} task${stats.tasks === 1 ? '' : 's'}`);
|
|
78
|
+
if (stats.completed > 0) parts.push(c.green(`${stats.completed} completed`));
|
|
79
|
+
if (stats.failed > 0) parts.push(c.red(`${stats.failed} failed`));
|
|
80
|
+
const skipped = stats.tasks - stats.completed - stats.failed;
|
|
81
|
+
if (skipped > 0) parts.push(c.yellow(`${skipped} skipped`));
|
|
82
|
+
this.out(`${c.bold('[weaver]')} Session complete: ${parts.join(' · ')}\n`);
|
|
83
|
+
|
|
84
|
+
const totalTokens = stats.totalInputTokens + stats.totalOutputTokens;
|
|
85
|
+
if (totalTokens > 0) {
|
|
86
|
+
this.out(`${c.bold('[weaver]')} ${c.dim(`Total: ${formatTokens(totalTokens)} tokens · $${stats.totalCost.toFixed(3)} · ${formatElapsed(stats.elapsed)}`)}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Task lifecycle ---
|
|
91
|
+
|
|
92
|
+
taskStart(index: number, instruction: string): void {
|
|
93
|
+
if (this.quiet) return;
|
|
94
|
+
this.taskStartTime = Date.now();
|
|
95
|
+
this.hasActiveText = false;
|
|
96
|
+
this.textBuffer = '';
|
|
97
|
+
const label = instruction.length > 70 ? instruction.slice(0, 67) + '...' : instruction;
|
|
98
|
+
this.out(`\n${c.cyan('◆')} ${c.bold(`Task ${index}:`)} ${label}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
taskEnd(success: boolean, stats: TaskEndStats): void {
|
|
102
|
+
if (this.quiet) return;
|
|
103
|
+
// Flush any remaining text
|
|
104
|
+
this.flushText();
|
|
105
|
+
|
|
106
|
+
const elapsed = formatElapsed(stats.elapsed);
|
|
107
|
+
const icon = success ? c.green('✓') : c.red('✗');
|
|
108
|
+
const status = success ? 'completed' : 'failed';
|
|
109
|
+
this.out(`${icon} Task ${status} ${c.dim(elapsed)}\n`);
|
|
110
|
+
|
|
111
|
+
// Summary line
|
|
112
|
+
const parts: string[] = [];
|
|
113
|
+
if (stats.toolCalls > 0) parts.push(`${stats.toolCalls} tool calls`);
|
|
114
|
+
const totalTokens = stats.inputTokens + stats.outputTokens;
|
|
115
|
+
if (totalTokens > 0) parts.push(`${formatTokens(totalTokens)} tokens`);
|
|
116
|
+
if (stats.estimatedCost > 0) parts.push(`$${stats.estimatedCost.toFixed(3)}`);
|
|
117
|
+
if (stats.filesModified > 0) parts.push(`${stats.filesModified} file${stats.filesModified === 1 ? '' : 's'} modified`);
|
|
118
|
+
if (parts.length > 0) {
|
|
119
|
+
this.out(` ${c.dim(parts.join(' · '))}\n`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (stats.gitMessage) {
|
|
123
|
+
this.out(` ${c.dim('→ Git: ' + stats.gitMessage)}\n`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Stream event handling ---
|
|
128
|
+
|
|
129
|
+
onStreamEvent(event: StreamEvent): void {
|
|
130
|
+
if (this.quiet) return;
|
|
131
|
+
|
|
132
|
+
switch (event.type) {
|
|
133
|
+
case 'thinking_delta':
|
|
134
|
+
if (this.verbose) {
|
|
135
|
+
// In verbose mode, stream thinking as dim text
|
|
136
|
+
this.flushText();
|
|
137
|
+
this.out(` ${c.dim(event.text.replace(/\n/g, '\n '))}`);
|
|
138
|
+
}
|
|
139
|
+
// In normal mode, thinking is completely hidden
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'text_delta':
|
|
143
|
+
if (this.verbose) {
|
|
144
|
+
this.flushText();
|
|
145
|
+
this.out(event.text);
|
|
146
|
+
this.hasActiveText = true;
|
|
147
|
+
}
|
|
148
|
+
// In normal mode, AI text is hidden (tool calls tell the story)
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'tool_result':
|
|
152
|
+
// CLI internal tool result — show as result line
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onToolEvent(event: ToolEvent): void {
|
|
161
|
+
if (this.quiet) return;
|
|
162
|
+
|
|
163
|
+
if (event.type === 'tool_call_start') {
|
|
164
|
+
this.flushText();
|
|
165
|
+
this.lastToolStartTime = Date.now();
|
|
166
|
+
const args = event.args ?? {};
|
|
167
|
+
const preview = toolPreview(event.name, args);
|
|
168
|
+
this.out(` ${c.cyan('◆')} ${event.name}${preview ? c.dim(`(${preview})`) : ''}\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (event.type === 'tool_call_result') {
|
|
172
|
+
const elapsed = Date.now() - this.lastToolStartTime;
|
|
173
|
+
const raw = event.result ?? '';
|
|
174
|
+
const icon = event.isError ? c.red('✗') : c.dim('→');
|
|
175
|
+
// Show full multiline output for verbose tools; one-line summary for others
|
|
176
|
+
const isVerboseTool = VERBOSE_TOOL_NAMES.has(event.name);
|
|
177
|
+
if (isVerboseTool && raw.includes('\n') && raw.length > 120) {
|
|
178
|
+
this.out(` ${icon} ${c.dim(formatElapsed(elapsed))}\n${raw}\n`);
|
|
179
|
+
} else {
|
|
180
|
+
const result = raw.replace(/\n/g, ' ').slice(0, 200);
|
|
181
|
+
this.out(` ${icon} ${result} ${c.dim(formatElapsed(elapsed))}\n`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- Direct messages ---
|
|
187
|
+
|
|
188
|
+
info(msg: string): void {
|
|
189
|
+
if (!this.quiet) this.out(`${c.bold('[weaver]')} ${msg}\n`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
warn(msg: string): void {
|
|
193
|
+
if (!this.quiet) this.out(`${c.yellow('⚠')} ${msg}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
error(title: string, detail?: string): void {
|
|
197
|
+
this.out(`${c.redBold('✗')} ${c.red(title)}\n`);
|
|
198
|
+
if (detail) this.out(` ${c.red(detail)}\n`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- Private ---
|
|
202
|
+
|
|
203
|
+
private flushText(): void {
|
|
204
|
+
if (this.hasActiveText) {
|
|
205
|
+
this.out('\n');
|
|
206
|
+
this.hasActiveText = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Formatting helpers ---
|
|
212
|
+
|
|
213
|
+
export function formatTokens(n: number): string {
|
|
214
|
+
if (n < 1000) return String(n);
|
|
215
|
+
if (n < 1_000_000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
|
216
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function formatElapsed(ms: number): string {
|
|
220
|
+
if (ms < 1000) return `${ms}ms`;
|
|
221
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
222
|
+
const m = Math.floor(ms / 60_000);
|
|
223
|
+
const s = Math.round((ms % 60_000) / 1000);
|
|
224
|
+
return s > 0 ? `${m}m ${s}s` : `${m}m`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function toolPreview(name: string, args: Record<string, unknown>): string {
|
|
228
|
+
if (args.file) return String(args.file).split('/').pop() ?? '';
|
|
229
|
+
if (args.command) return String(args.command).slice(0, 50);
|
|
230
|
+
if (args.directory) return String(args.directory).split('/').pop() ?? '';
|
|
231
|
+
// For patch_file, show file + patch count
|
|
232
|
+
if (name === 'patch_file' && args.patches) {
|
|
233
|
+
const file = String(args.file ?? '').split('/').pop() ?? '';
|
|
234
|
+
const count = Array.isArray(args.patches) ? args.patches.length : '?';
|
|
235
|
+
return `${file}, ${count} patches`;
|
|
236
|
+
}
|
|
237
|
+
return '';
|
|
238
|
+
}
|