@synergenius/flow-weaver-pack-weaver 0.9.8 → 0.9.10
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/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/agent-provider.d.ts.map +1 -1
- package/dist/bot/agent-provider.js +3 -2
- package/dist/bot/agent-provider.js.map +1 -1
- package/dist/bot/approvals.js +17 -8
- package/dist/bot/approvals.js.map +1 -1
- package/dist/bot/assistant-core.d.ts +17 -0
- package/dist/bot/assistant-core.d.ts.map +1 -1
- package/dist/bot/assistant-core.js +366 -45
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/assistant-tools.d.ts +1 -1
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +283 -9
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/bot-agent-channel.d.ts.map +1 -1
- package/dist/bot/bot-agent-channel.js +2 -0
- package/dist/bot/bot-agent-channel.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +4 -0
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +72 -27
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +6 -5
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js +98 -42
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/cost-store.d.ts +3 -0
- package/dist/bot/cost-store.d.ts.map +1 -1
- package/dist/bot/cost-store.js +21 -10
- package/dist/bot/cost-store.js.map +1 -1
- package/dist/bot/cost-tracker.d.ts.map +1 -1
- package/dist/bot/cost-tracker.js +14 -1
- package/dist/bot/cost-tracker.js.map +1 -1
- package/dist/bot/cron-parser.d.ts.map +1 -1
- package/dist/bot/cron-parser.js +2 -0
- package/dist/bot/cron-parser.js.map +1 -1
- package/dist/bot/cron-scheduler.d.ts.map +1 -1
- package/dist/bot/cron-scheduler.js +1 -0
- package/dist/bot/cron-scheduler.js.map +1 -1
- package/dist/bot/device-connection.d.ts +13 -0
- package/dist/bot/device-connection.d.ts.map +1 -0
- package/dist/bot/device-connection.js +102 -0
- package/dist/bot/device-connection.js.map +1 -0
- package/dist/bot/error-classifier.d.ts.map +1 -1
- package/dist/bot/error-classifier.js +5 -0
- package/dist/bot/error-classifier.js.map +1 -1
- package/dist/bot/file-lock.d.ts.map +1 -1
- package/dist/bot/file-lock.js +13 -3
- package/dist/bot/file-lock.js.map +1 -1
- package/dist/bot/file-watcher.d.ts.map +1 -1
- package/dist/bot/file-watcher.js +1 -0
- package/dist/bot/file-watcher.js.map +1 -1
- package/dist/bot/genesis-prompt-context.d.ts +5 -0
- package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
- package/dist/bot/genesis-prompt-context.js +55 -0
- package/dist/bot/genesis-prompt-context.js.map +1 -1
- package/dist/bot/genesis-store.d.ts +4 -0
- package/dist/bot/genesis-store.d.ts.map +1 -1
- package/dist/bot/genesis-store.js +79 -12
- package/dist/bot/genesis-store.js.map +1 -1
- package/dist/bot/improve-loop.d.ts +46 -0
- package/dist/bot/improve-loop.d.ts.map +1 -0
- package/dist/bot/improve-loop.js +592 -0
- package/dist/bot/improve-loop.js.map +1 -0
- package/dist/bot/index.d.ts +1 -0
- package/dist/bot/index.d.ts.map +1 -1
- package/dist/bot/index.js +2 -0
- package/dist/bot/index.js.map +1 -1
- package/dist/bot/insight-engine.d.ts +12 -0
- package/dist/bot/insight-engine.d.ts.map +1 -0
- package/dist/bot/insight-engine.js +256 -0
- package/dist/bot/insight-engine.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +4 -1
- package/dist/bot/knowledge-store.js.map +1 -1
- package/dist/bot/pipeline-runner.d.ts.map +1 -1
- package/dist/bot/pipeline-runner.js +12 -4
- package/dist/bot/pipeline-runner.js.map +1 -1
- package/dist/bot/project-model.d.ts +25 -0
- package/dist/bot/project-model.d.ts.map +1 -0
- package/dist/bot/project-model.js +372 -0
- package/dist/bot/project-model.js.map +1 -0
- package/dist/bot/response-formatter.js +2 -3
- package/dist/bot/response-formatter.js.map +1 -1
- package/dist/bot/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +10 -2
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/safe-path.d.ts +1 -1
- package/dist/bot/safe-path.d.ts.map +1 -1
- package/dist/bot/safe-path.js +20 -1
- package/dist/bot/safe-path.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -2
- package/dist/bot/safety.d.ts.map +1 -1
- package/dist/bot/safety.js +45 -2
- package/dist/bot/safety.js.map +1 -1
- package/dist/bot/session-state.d.ts +4 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +52 -9
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/slash-commands.d.ts.map +1 -1
- package/dist/bot/slash-commands.js +100 -5
- package/dist/bot/slash-commands.js.map +1 -1
- package/dist/bot/steering-engine.d.ts +67 -0
- package/dist/bot/steering-engine.d.ts.map +1 -0
- package/dist/bot/steering-engine.js +198 -0
- package/dist/bot/steering-engine.js.map +1 -0
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +62 -25
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +5 -2
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-queue.d.ts +6 -1
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +43 -4
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/tool-registry.d.ts +1 -1
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +65 -4
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/trust-calculator.d.ts +34 -0
- package/dist/bot/trust-calculator.d.ts.map +1 -0
- package/dist/bot/trust-calculator.js +67 -0
- package/dist/bot/trust-calculator.js.map +1 -0
- package/dist/bot/types.d.ts +97 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/update-checker.d.ts +21 -0
- package/dist/bot/update-checker.d.ts.map +1 -0
- package/dist/bot/update-checker.js +129 -0
- package/dist/bot/update-checker.js.map +1 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +11 -4
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +2 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +9 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +77 -8
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/weaver-config.md +15 -9
- package/dist/handlers/on-execution-completed.d.ts +11 -0
- package/dist/handlers/on-execution-completed.d.ts.map +1 -0
- package/dist/handlers/on-execution-completed.js +25 -0
- package/dist/handlers/on-execution-completed.js.map +1 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +33 -0
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/genesis-approve.d.ts.map +1 -1
- package/dist/node-types/genesis-approve.js +28 -3
- package/dist/node-types/genesis-approve.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +23 -13
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-propose.d.ts.map +1 -1
- package/dist/node-types/genesis-propose.js +8 -0
- package/dist/node-types/genesis-propose.js.map +1 -1
- package/dist/node-types/genesis-update-history.d.ts.map +1 -1
- package/dist/node-types/genesis-update-history.js +13 -0
- package/dist/node-types/genesis-update-history.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +27 -0
- package/package.json +5 -2
- package/src/bot/agent-provider.ts +3 -2
- package/src/bot/approvals.ts +16 -8
- package/src/bot/assistant-core.ts +366 -44
- package/src/bot/assistant-tools.ts +291 -9
- package/src/bot/bot-agent-channel.ts +2 -0
- package/src/bot/bot-manager.ts +70 -29
- package/src/bot/conversation-store.ts +87 -42
- package/src/bot/cost-store.ts +20 -9
- package/src/bot/cost-tracker.ts +13 -1
- package/src/bot/cron-parser.ts +1 -0
- package/src/bot/cron-scheduler.ts +1 -0
- package/src/bot/device-connection.ts +102 -0
- package/src/bot/error-classifier.ts +5 -0
- package/src/bot/file-lock.ts +12 -2
- package/src/bot/file-watcher.ts +1 -0
- package/src/bot/genesis-prompt-context.ts +61 -0
- package/src/bot/genesis-store.ts +68 -16
- package/src/bot/improve-loop.ts +651 -0
- package/src/bot/index.ts +3 -0
- package/src/bot/insight-engine.ts +273 -0
- package/src/bot/knowledge-store.ts +4 -1
- package/src/bot/pipeline-runner.ts +11 -6
- package/src/bot/project-model.ts +404 -0
- package/src/bot/response-formatter.ts +2 -3
- package/src/bot/run-store.ts +5 -2
- package/src/bot/safe-path.ts +20 -1
- package/src/bot/safety.ts +57 -3
- package/src/bot/session-state.ts +47 -7
- package/src/bot/slash-commands.ts +103 -5
- package/src/bot/steering-engine.ts +233 -0
- package/src/bot/step-executor.ts +66 -26
- package/src/bot/system-prompt.ts +5 -2
- package/src/bot/task-queue.ts +40 -4
- package/src/bot/tool-registry.ts +67 -5
- package/src/bot/trust-calculator.ts +87 -0
- package/src/bot/types.ts +104 -0
- package/src/bot/update-checker.ts +138 -0
- package/src/bot/weaver-tools.ts +10 -4
- package/src/cli-bridge.ts +2 -1
- package/src/cli-handlers.ts +84 -9
- package/src/handlers/on-execution-completed.ts +30 -0
- package/src/mcp-tools.ts +38 -0
- package/src/node-types/genesis-approve.ts +28 -3
- package/src/node-types/genesis-observe.ts +23 -12
- package/src/node-types/genesis-propose.ts +8 -0
- package/src/node-types/genesis-update-history.ts +12 -0
- package/src/ui/evolution-panel.tsx +96 -0
- package/src/ui/insights-widget.tsx +77 -0
|
@@ -23,6 +23,17 @@ import { VERBOSE_TOOL_NAMES } from './tool-registry.js';
|
|
|
23
23
|
import { generateToolPromptSection, generateVerboseToolList } from './tool-registry.js';
|
|
24
24
|
import { CHARS_PER_TOKEN } from './safety.js';
|
|
25
25
|
|
|
26
|
+
export interface AssistantDebugTurn {
|
|
27
|
+
turn: number;
|
|
28
|
+
input: string;
|
|
29
|
+
toolCalls: Array<{ name: string; args: Record<string, unknown>; result: string; isError: boolean }>;
|
|
30
|
+
response: string;
|
|
31
|
+
tokensUsed: number;
|
|
32
|
+
systemPromptLength: number;
|
|
33
|
+
insightNudge?: string;
|
|
34
|
+
conversationId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export interface AssistantOptions {
|
|
27
38
|
provider: AgentProvider;
|
|
28
39
|
tools: ToolDefinition[];
|
|
@@ -37,6 +48,8 @@ export interface AssistantOptions {
|
|
|
37
48
|
resumeId?: string;
|
|
38
49
|
/** Always start a fresh conversation */
|
|
39
50
|
newConversation?: boolean;
|
|
51
|
+
/** Debug mode: output structured NDJSON per turn, no ANSI, full conversation loop */
|
|
52
|
+
debug?: boolean;
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
const DEFAULT_SYSTEM_PROMPT = `You are Weaver — a hands-on AI assistant for Flow Weaver projects.
|
|
@@ -49,7 +62,9 @@ Tell me what you want to build or fix. I will:
|
|
|
49
62
|
1. Break it into steps
|
|
50
63
|
2. Use tools to read, write, validate, and test code
|
|
51
64
|
3. Spawn bots for longer tasks that run in the background
|
|
52
|
-
4.
|
|
65
|
+
4. Track project health and surface insights proactively
|
|
66
|
+
5. Propose bot workflow improvements based on execution patterns
|
|
67
|
+
6. Report results — not plans
|
|
53
68
|
|
|
54
69
|
## How to respond
|
|
55
70
|
|
|
@@ -73,22 +88,35 @@ You are running in a terminal. Plain text only.
|
|
|
73
88
|
|
|
74
89
|
## Tool output handling
|
|
75
90
|
|
|
76
|
-
These tools display their FULL output directly to the user:
|
|
91
|
+
CRITICAL: These tools display their FULL output directly to the user:
|
|
77
92
|
${generateVerboseToolList()}
|
|
78
|
-
|
|
93
|
+
The user ALREADY SEES the complete output from these tools. After calling them:
|
|
94
|
+
- Do NOT list, enumerate, or walk through the output
|
|
95
|
+
- Do NOT restate what the diagram/description shows
|
|
96
|
+
- ONLY add a brief insight the user cannot see (e.g. "Notice the fan-out at step 5 — that's where parallelism happens")
|
|
97
|
+
- If the output speaks for itself, say nothing or just "There it is."
|
|
79
98
|
For all other tools, you may explain the result briefly.
|
|
80
99
|
|
|
81
100
|
## Personality
|
|
82
101
|
|
|
83
102
|
- Helpful, practical, no fluff
|
|
84
|
-
- Lead with
|
|
103
|
+
- Lead with the RESULT, not narration of what you did. Never start with "Let me...", "Found it.", "Now let me...", "Good, I have..."
|
|
85
104
|
- If something fails, say what went wrong and what you'll try next
|
|
86
105
|
- Never apologize for tool usage — tools are how you work
|
|
87
|
-
- When you don't know something, say so
|
|
106
|
+
- When you don't know something, say so
|
|
107
|
+
|
|
108
|
+
## Project intelligence
|
|
109
|
+
|
|
110
|
+
You have access to project health, bot performance, failure patterns, cost trends, and evolution history.
|
|
111
|
+
Be proactive: when you see something relevant to what the user is doing, mention it.
|
|
112
|
+
At session start, briefly acknowledge the project state if there's something worth noting.
|
|
113
|
+
You can propose workflow improvements with genesis_propose when patterns suggest a structural fix.
|
|
114
|
+
If a bot workflow needs modification and isn't ejected yet, auto-eject it first.`;
|
|
88
115
|
|
|
89
116
|
|
|
90
117
|
export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
91
118
|
const { provider, tools, executor, projectDir } = opts;
|
|
119
|
+
const isDebug = !!opts.debug;
|
|
92
120
|
const out = (s: string) => process.stderr.write(s);
|
|
93
121
|
|
|
94
122
|
// Pipe mode: if stdin is not a TTY, read all input as one message
|
|
@@ -98,7 +126,10 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
98
126
|
chunks.push(typeof chunk === 'string' ? chunk : chunk.toString());
|
|
99
127
|
}
|
|
100
128
|
const pipeInput = chunks.join('').trim();
|
|
101
|
-
if (!pipeInput)
|
|
129
|
+
if (!pipeInput) {
|
|
130
|
+
process.stderr.write(' No input provided. Pipe a message: echo "describe my workflows" | flow-weaver weaver assistant\n');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
102
133
|
|
|
103
134
|
// Build system prompt for pipe mode
|
|
104
135
|
let systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
@@ -112,13 +143,23 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
112
143
|
}
|
|
113
144
|
} catch { /* plan not available */ }
|
|
114
145
|
|
|
146
|
+
// Inject project intelligence (ambient awareness)
|
|
147
|
+
try {
|
|
148
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
149
|
+
const pms = new ProjectModelStore(projectDir);
|
|
150
|
+
const model = await pms.getOrBuild();
|
|
151
|
+
if (model && (model.health.workflows.length > 0 || model.bots.length > 0)) {
|
|
152
|
+
systemPrompt += '\n\n## Project Intelligence\n\n' + pms.formatSummary(model);
|
|
153
|
+
}
|
|
154
|
+
} catch { /* project model not available yet */ }
|
|
155
|
+
|
|
115
156
|
// Run single message, print result, exit
|
|
116
157
|
await runAgentLoop(provider, tools, executor, [{ role: 'user', content: pipeInput }], {
|
|
117
158
|
systemPrompt, maxIterations: 20,
|
|
118
159
|
onStreamEvent: (e) => { if (e.type === 'text_delta') out(e.text); },
|
|
119
160
|
onToolEvent: (e) => {
|
|
120
161
|
if (e.type === 'tool_call_start') out(`\n ${c.cyan('◆')} ${e.name}\n`);
|
|
121
|
-
if (e.type === 'tool_call_result') out(` ${c.dim('→')} ${(e.result ?? '').slice(0,
|
|
162
|
+
if (e.type === 'tool_call_result') out(` ${c.dim('→')} ${(e.result ?? '').slice(0, 500)}\n`);
|
|
122
163
|
},
|
|
123
164
|
});
|
|
124
165
|
out('\n');
|
|
@@ -137,12 +178,87 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
137
178
|
}
|
|
138
179
|
} catch { /* plan not available */ }
|
|
139
180
|
|
|
181
|
+
// Load steering configuration
|
|
182
|
+
let steeringEngine: import('./steering-engine.js').SteeringEngine | undefined;
|
|
183
|
+
try {
|
|
184
|
+
const { SteeringEngine, loadSteers } = await import('./steering-engine.js');
|
|
185
|
+
const steers = loadSteers(projectDir);
|
|
186
|
+
steeringEngine = new SteeringEngine(steers);
|
|
187
|
+
} catch { /* steering not available */ }
|
|
188
|
+
|
|
189
|
+
// Inject project intelligence (ambient awareness)
|
|
190
|
+
try {
|
|
191
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
192
|
+
const pms = new ProjectModelStore(projectDir);
|
|
193
|
+
const model = await pms.getOrBuild();
|
|
194
|
+
if (model && (model.health.workflows.length > 0 || model.bots.length > 0)) {
|
|
195
|
+
systemPrompt += '\n\n## Project Intelligence\n\n' + pms.formatSummary(model);
|
|
196
|
+
}
|
|
197
|
+
} catch { /* project model not available yet */ }
|
|
198
|
+
|
|
199
|
+
// Auto-context scan: on first-ever use, scan project and persist context in knowledge store
|
|
200
|
+
try {
|
|
201
|
+
const { KnowledgeStore } = await import('./knowledge-store.js');
|
|
202
|
+
const knowledge = new KnowledgeStore(projectDir);
|
|
203
|
+
const existing = knowledge.recall('project:context');
|
|
204
|
+
if (existing.length === 0) {
|
|
205
|
+
// First-ever scan — build project context
|
|
206
|
+
const fsMod = await import('node:fs');
|
|
207
|
+
const parts: string[] = [];
|
|
208
|
+
|
|
209
|
+
// Package info
|
|
210
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
211
|
+
if (fsMod.existsSync(pkgPath)) {
|
|
212
|
+
try {
|
|
213
|
+
const pkg = JSON.parse(fsMod.readFileSync(pkgPath, 'utf-8'));
|
|
214
|
+
parts.push(`Project: ${pkg.name ?? 'unknown'} v${pkg.version ?? '0.0.0'}`);
|
|
215
|
+
if (pkg.description) parts.push(`Description: ${pkg.description}`);
|
|
216
|
+
const deps = Object.keys(pkg.dependencies ?? {}).slice(0, 10);
|
|
217
|
+
if (deps.length > 0) parts.push(`Key deps: ${deps.join(', ')}`);
|
|
218
|
+
} catch { /* parse failed */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Workflow scan
|
|
222
|
+
try {
|
|
223
|
+
const { execFileSync: scanExec } = await import('node:child_process');
|
|
224
|
+
const found = scanExec('npx', ['flow-weaver', 'find-workflows', '--json'], {
|
|
225
|
+
encoding: 'utf-8', cwd: projectDir, timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
226
|
+
});
|
|
227
|
+
const workflows = JSON.parse(found);
|
|
228
|
+
if (Array.isArray(workflows) && workflows.length > 0) {
|
|
229
|
+
parts.push(`Workflows (${workflows.length}): ${workflows.map((w: { name?: string; file?: string }) => w.name ?? w.file ?? 'unknown').join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
} catch { /* find-workflows failed */ }
|
|
232
|
+
|
|
233
|
+
// TypeScript config
|
|
234
|
+
const tsConfigPath = path.join(projectDir, 'tsconfig.json');
|
|
235
|
+
if (fsMod.existsSync(tsConfigPath)) parts.push('TypeScript: yes');
|
|
236
|
+
|
|
237
|
+
// Weaver config
|
|
238
|
+
const weaverConfigPath = path.join(projectDir, '.weaver.json');
|
|
239
|
+
if (fsMod.existsSync(weaverConfigPath)) {
|
|
240
|
+
try {
|
|
241
|
+
const wc = JSON.parse(fsMod.readFileSync(weaverConfigPath, 'utf-8'));
|
|
242
|
+
parts.push(`Weaver provider: ${typeof wc.provider === 'string' ? wc.provider : wc.provider?.name ?? 'auto'}`);
|
|
243
|
+
} catch { /* parse failed */ }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (parts.length > 0) {
|
|
247
|
+
knowledge.learn('project:context', parts.join('\n'), 'auto-scan');
|
|
248
|
+
systemPrompt += '\n\n## Project Context (auto-scanned)\n\n' + parts.join('\n');
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
// Context already scanned — inject from knowledge store
|
|
252
|
+
systemPrompt += '\n\n## Project Context\n\n' + existing[0]!.value;
|
|
253
|
+
}
|
|
254
|
+
} catch { /* knowledge store not available */ }
|
|
255
|
+
|
|
140
256
|
// Persistent conversation store
|
|
141
257
|
const { ConversationStore } = await import('./conversation-store.js');
|
|
142
258
|
const store = new ConversationStore();
|
|
143
259
|
|
|
144
260
|
// Resolve conversation: resume, new, or auto
|
|
145
|
-
let conversation: { id: string; title: string; messageCount: number };
|
|
261
|
+
let conversation: { id: string; title: string; messageCount: number; lastMessageAt: number };
|
|
146
262
|
const history: AgentMessage[] = [];
|
|
147
263
|
|
|
148
264
|
if (opts.resumeId) {
|
|
@@ -155,7 +271,7 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
155
271
|
history.push(...store.loadMessages(existing.id));
|
|
156
272
|
compressHistory(history);
|
|
157
273
|
} else if (opts.newConversation) {
|
|
158
|
-
conversation = store.create(projectDir);
|
|
274
|
+
conversation = await store.create(projectDir);
|
|
159
275
|
} else {
|
|
160
276
|
// Auto-resume most recent if within 1 hour, else create new
|
|
161
277
|
const recent = store.getMostRecent();
|
|
@@ -164,7 +280,7 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
164
280
|
history.push(...store.loadMessages(recent.id));
|
|
165
281
|
compressHistory(history);
|
|
166
282
|
} else {
|
|
167
|
-
conversation = store.create(projectDir);
|
|
283
|
+
conversation = await store.create(projectDir);
|
|
168
284
|
}
|
|
169
285
|
}
|
|
170
286
|
|
|
@@ -193,23 +309,92 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
193
309
|
if (creds.token && creds.expiresAt > Date.now()) {
|
|
194
310
|
cloudPlan = creds.plan ?? 'connected';
|
|
195
311
|
cloudStatus = `Cloud: ${cloudPlan}`;
|
|
312
|
+
} else if (creds.token) {
|
|
313
|
+
cloudStatus = 'Cloud: expired (run "fw login" to refresh)';
|
|
196
314
|
}
|
|
197
315
|
}
|
|
198
316
|
} catch { /* credentials not available */ }
|
|
199
317
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
318
|
+
// Detect first-run: scan for existing workflows and conversation count
|
|
319
|
+
let workflowCount = 0;
|
|
320
|
+
let projectName = path.basename(projectDir);
|
|
321
|
+
let packageDesc = '';
|
|
322
|
+
let isFirstRun = !conversation.title;
|
|
323
|
+
try {
|
|
324
|
+
const fsMod = await import('node:fs');
|
|
325
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
326
|
+
if (fsMod.existsSync(pkgPath)) {
|
|
327
|
+
const pkg = JSON.parse(fsMod.readFileSync(pkgPath, 'utf-8'));
|
|
328
|
+
projectName = pkg.name ?? projectName;
|
|
329
|
+
packageDesc = pkg.description ?? '';
|
|
330
|
+
}
|
|
331
|
+
const { execFileSync: fwExec } = await import('node:child_process');
|
|
332
|
+
try {
|
|
333
|
+
const found = fwExec('npx', ['flow-weaver', 'find-workflows', '--json'], {
|
|
334
|
+
encoding: 'utf-8', cwd: projectDir, timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
335
|
+
});
|
|
336
|
+
const parsed = JSON.parse(found);
|
|
337
|
+
workflowCount = Array.isArray(parsed) ? parsed.length : (parsed.count ?? 0);
|
|
338
|
+
} catch { /* find-workflows not available */ }
|
|
339
|
+
} catch { /* scan failed */ }
|
|
340
|
+
|
|
341
|
+
if (!isDebug) {
|
|
342
|
+
const header = [`weaver assistant v${weaverVersion}`, `flow-weaver v${fwVersion}`];
|
|
343
|
+
if (cloudStatus) header.push(cloudStatus);
|
|
344
|
+
out(`\n ${c.bold(header[0])} ${c.dim(`· ${header.slice(1).join(' · ')}`)}\n`);
|
|
345
|
+
out(` ${c.dim(`Project: ${projectName}`)}\n`);
|
|
346
|
+
if (!cloudStatus) {
|
|
347
|
+
out(` ${c.dim('AI: Local (set ANTHROPIC_API_KEY or run "fw login" to connect)')}\n`);
|
|
348
|
+
}
|
|
349
|
+
if (conversation.title) {
|
|
350
|
+
const ago = Math.round((Date.now() - conversation.lastMessageAt) / 60000);
|
|
351
|
+
out(` ${c.dim(`Resuming: "${conversation.title}" (${conversation.messageCount} messages, ${ago}m ago). /new to start fresh`)}\n`);
|
|
352
|
+
} else if (workflowCount === 0 && isFirstRun) {
|
|
353
|
+
out(` ${c.dim('Welcome! No workflows found yet.')}\n`);
|
|
354
|
+
out(` ${c.dim('Try: "create a hello world workflow" or "what can you do?"')}\n`);
|
|
355
|
+
} else if (workflowCount > 0 && isFirstRun) {
|
|
356
|
+
out(` ${c.dim(`Found ${workflowCount} workflow${workflowCount !== 1 ? 's' : ''}. New conversation.`)}\n`);
|
|
357
|
+
out(` ${c.dim('Try: "validate my workflows" or "show project health"')}\n`);
|
|
358
|
+
} else {
|
|
359
|
+
out(` ${c.dim('Type your request. /help for commands.')}\n`);
|
|
360
|
+
}
|
|
361
|
+
out('\n');
|
|
206
362
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
363
|
+
|
|
364
|
+
// Proactive session greeting with project status (for returning users with data)
|
|
365
|
+
if (!isDebug && !isFirstRun) {
|
|
366
|
+
try {
|
|
367
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
368
|
+
const pms = new ProjectModelStore(projectDir);
|
|
369
|
+
const model = await pms.getOrBuild();
|
|
370
|
+
if (model && (model.health.workflows.length > 0 || model.bots.length > 0)) {
|
|
371
|
+
out(` ${c.dim(pms.formatSessionGreeting(model))}\n`);
|
|
372
|
+
}
|
|
373
|
+
} catch { /* project model not available yet */ }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Check for updates (cached, max once per 24h, non-blocking)
|
|
377
|
+
if (!isDebug) {
|
|
378
|
+
try {
|
|
379
|
+
const { checkForUpdates, formatUpdateNotification } = await import('./update-checker.js');
|
|
380
|
+
const updates = await checkForUpdates(projectDir);
|
|
381
|
+
const notification = formatUpdateNotification(updates);
|
|
382
|
+
if (notification) {
|
|
383
|
+
out(` ${c.yellow('⬆')} ${c.dim(notification.split('\n')[0]!)}\n`);
|
|
384
|
+
systemPrompt += `\n\n## Update Available\n\n${notification}\nMention this to the user if relevant.`;
|
|
385
|
+
}
|
|
386
|
+
} catch { /* update check failed — non-fatal */ }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Inject first-run context into system prompt so the assistant adapts
|
|
390
|
+
if (workflowCount === 0 && isFirstRun) {
|
|
391
|
+
systemPrompt += '\n\n## First-Run Context\n\nThis is a NEW user with NO workflows yet. Be welcoming. Suggest creating their first workflow. If they describe what they want to build, offer to create it immediately. Do NOT assume they know Flow Weaver concepts — explain briefly as you go.';
|
|
392
|
+
if (packageDesc) {
|
|
393
|
+
systemPrompt += `\nProject description: "${packageDesc}". Use this to suggest a relevant first workflow.`;
|
|
394
|
+
}
|
|
395
|
+
} else if (workflowCount > 0 && isFirstRun) {
|
|
396
|
+
systemPrompt += `\n\n## First Conversation Context\n\nThis user has ${workflowCount} existing workflow${workflowCount !== 1 ? 's' : ''} but this is their first conversation with you. Offer to validate, describe, or improve their workflows. Be helpful but don't assume they're a beginner.`;
|
|
211
397
|
}
|
|
212
|
-
out(` ${c.dim('Type your request. Ctrl+C to exit. /help for commands.')}\n\n`);
|
|
213
398
|
|
|
214
399
|
// Rich input with history, arrows, tab completion, slash commands
|
|
215
400
|
const { RichInput } = await import('./rich-input.js');
|
|
@@ -232,32 +417,70 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
232
417
|
})()
|
|
233
418
|
: (): Promise<string | null> => richInput!.getInput();
|
|
234
419
|
|
|
420
|
+
// Debug mode: collect tool calls and response text per turn
|
|
421
|
+
let debugToolCalls: Array<{ name: string; args: Record<string, unknown>; result: string; isError: boolean }> = [];
|
|
422
|
+
const debugStreamToolNames = new Map<string, string>(); // id -> name for CLI provider tool tracking
|
|
423
|
+
let debugResponseText = '';
|
|
424
|
+
let debugInsightNudge: string | undefined;
|
|
425
|
+
let debugTurnCount = 0;
|
|
426
|
+
|
|
235
427
|
let lastStreamType = '';
|
|
236
428
|
const onStreamEvent = (event: StreamEvent) => {
|
|
237
429
|
if (event.type === 'text_delta') {
|
|
238
|
-
if (lastStreamType === 'thinking_delta') out('\n\n');
|
|
239
|
-
|
|
430
|
+
if (lastStreamType === 'thinking_delta' && !isDebug) out('\n\n');
|
|
431
|
+
if (isDebug) { debugResponseText += event.text; }
|
|
432
|
+
else { out(event.text); }
|
|
240
433
|
} else if (event.type === 'thinking_delta') {
|
|
241
|
-
out(c.dim(event.text));
|
|
434
|
+
if (!isDebug) out(c.dim(event.text));
|
|
435
|
+
}
|
|
436
|
+
// Capture tool events from stream (CLI provider handles tools via MCP,
|
|
437
|
+
// so onToolEvent never fires — we catch them here instead)
|
|
438
|
+
if (isDebug) {
|
|
439
|
+
const e = event as Record<string, unknown>;
|
|
440
|
+
if (event.type === 'tool_use_start') {
|
|
441
|
+
debugStreamToolNames.set(String(e.id), String(e.name));
|
|
442
|
+
}
|
|
443
|
+
if (event.type === 'tool_result') {
|
|
444
|
+
debugToolCalls.push({
|
|
445
|
+
name: debugStreamToolNames.get(String(e.id)) ?? 'unknown',
|
|
446
|
+
args: {},
|
|
447
|
+
result: String(e.result ?? '').slice(0, 2000),
|
|
448
|
+
isError: !!e.isError,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
242
451
|
}
|
|
243
452
|
lastStreamType = event.type;
|
|
244
453
|
};
|
|
245
454
|
|
|
455
|
+
let currentToolName = '';
|
|
456
|
+
let currentToolArgs: Record<string, unknown> = {};
|
|
246
457
|
const onToolEvent = (event: { type: string; name: string; args?: Record<string, unknown>; result?: string; isError?: boolean }) => {
|
|
247
458
|
if (event.type === 'tool_call_start') {
|
|
248
|
-
|
|
249
|
-
|
|
459
|
+
currentToolName = event.name;
|
|
460
|
+
currentToolArgs = event.args ?? {};
|
|
461
|
+
if (!isDebug) {
|
|
462
|
+
const preview = toolPreview(event.name, event.args ?? {});
|
|
463
|
+
out(`\n ${c.cyan('◆')} ${event.name}${preview ? c.dim(`(${preview})`) : ''}\n`);
|
|
464
|
+
}
|
|
250
465
|
}
|
|
251
466
|
if (event.type === 'tool_call_result') {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
467
|
+
if (isDebug) {
|
|
468
|
+
debugToolCalls.push({
|
|
469
|
+
name: currentToolName,
|
|
470
|
+
args: currentToolArgs,
|
|
471
|
+
result: (event.result ?? '').slice(0, 2000),
|
|
472
|
+
isError: !!event.isError,
|
|
473
|
+
});
|
|
258
474
|
} else {
|
|
259
|
-
const
|
|
260
|
-
|
|
475
|
+
const icon = event.isError ? c.red('✗') : c.dim('→');
|
|
476
|
+
const raw = event.result ?? '';
|
|
477
|
+
const isVerboseTool = VERBOSE_TOOL_NAMES.has(event.name);
|
|
478
|
+
if (isVerboseTool && raw.length > 150) {
|
|
479
|
+
out(` ${icon}\n${raw}\n`);
|
|
480
|
+
} else {
|
|
481
|
+
const result = raw.replace(/\n/g, ' ').slice(0, 200);
|
|
482
|
+
out(` ${icon} ${result}\n`);
|
|
483
|
+
}
|
|
261
484
|
}
|
|
262
485
|
}
|
|
263
486
|
};
|
|
@@ -271,7 +494,7 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
271
494
|
conversationId: conversation.id,
|
|
272
495
|
onClear: () => { history.length = 0; },
|
|
273
496
|
onExit: () => { shouldExit = true; },
|
|
274
|
-
onNew: () => { history.length = 0; conversation = store.create(projectDir); },
|
|
497
|
+
onNew: async () => { history.length = 0; conversation = await store.create(projectDir); },
|
|
275
498
|
onVerbose: () => { out(` ${c.dim('Verbose toggling not yet wired to streaming.')}\n`); },
|
|
276
499
|
};
|
|
277
500
|
|
|
@@ -291,7 +514,7 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
291
514
|
const parsed = JSON.parse(result);
|
|
292
515
|
const errorCount = parsed.errorCount ?? parsed.errors?.length ?? 0;
|
|
293
516
|
if (errorCount > 0) {
|
|
294
|
-
out(`\n ${c.yellow('⚠')} ${filename}
|
|
517
|
+
out(`\n ${c.yellow('⚠')} ${opts.watchDir}/${filename}: ${errorCount} validation error(s). Ask me to fix them.\n`);
|
|
295
518
|
out(` ${c.dim('Type a message to fix, or ignore.')}\n`);
|
|
296
519
|
}
|
|
297
520
|
} catch { /* validation failed or not a workflow — ignore */ }
|
|
@@ -300,6 +523,9 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
300
523
|
} catch { /* watch not available */ }
|
|
301
524
|
}
|
|
302
525
|
|
|
526
|
+
// Track which insights have been nudged (by ID) to avoid repetition
|
|
527
|
+
const nudgedInsightIds = new Set<string>();
|
|
528
|
+
|
|
303
529
|
// Main conversation loop
|
|
304
530
|
while (!shouldExit) {
|
|
305
531
|
const input = await getNextInput();
|
|
@@ -308,10 +534,29 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
308
534
|
|
|
309
535
|
// Handle slash commands
|
|
310
536
|
if (input.startsWith('/')) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
537
|
+
if (isDebug) {
|
|
538
|
+
// Capture slash command output for debug JSON
|
|
539
|
+
let slashOutput = '';
|
|
540
|
+
const debugSlashCtx = { ...slashCtx, out: (s: string) => { slashOutput += s; } };
|
|
541
|
+
const handled = await handleSlashCommand(input, debugSlashCtx);
|
|
542
|
+
if (handled) {
|
|
543
|
+
debugTurnCount++;
|
|
544
|
+
// Strip ANSI codes for clean debug output
|
|
545
|
+
const clean = slashOutput.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
546
|
+
process.stdout.write(JSON.stringify({
|
|
547
|
+
turn: debugTurnCount,
|
|
548
|
+
input,
|
|
549
|
+
slashCommand: true,
|
|
550
|
+
response: clean,
|
|
551
|
+
conversationId: conversation.id,
|
|
552
|
+
}) + '\n');
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
const handled = await handleSlashCommand(input, slashCtx);
|
|
557
|
+
if (handled) continue;
|
|
558
|
+
}
|
|
559
|
+
if (!isDebug) out(` ${c.dim('Unknown command. Type /help for available commands.')}\n\n`);
|
|
315
560
|
continue;
|
|
316
561
|
}
|
|
317
562
|
|
|
@@ -369,20 +614,97 @@ export async function runAssistant(opts: AssistantOptions): Promise<void> {
|
|
|
369
614
|
// Token-aware compression
|
|
370
615
|
compressHistory(history);
|
|
371
616
|
|
|
617
|
+
// Proactive insight surfacing — update system prompt for next turn
|
|
618
|
+
// Max 3 nudges per session to avoid being annoying
|
|
619
|
+
if (nudgedInsightIds.size < 3) {
|
|
620
|
+
try {
|
|
621
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
622
|
+
const { InsightEngine } = await import('./insight-engine.js');
|
|
623
|
+
const pms = new ProjectModelStore(projectDir);
|
|
624
|
+
const model = await pms.getOrBuild();
|
|
625
|
+
const engine = new InsightEngine();
|
|
626
|
+
const insights = engine.analyze(model).filter(i => i.confidence >= 0.6);
|
|
627
|
+
|
|
628
|
+
if (insights.length > 0) {
|
|
629
|
+
// Skip insights already nudged (by ID) or mentioned in conversation
|
|
630
|
+
const unsurfaced = insights.filter(insight =>
|
|
631
|
+
!nudgedInsightIds.has(insight.id) &&
|
|
632
|
+
!history.some(m =>
|
|
633
|
+
m.role === 'assistant' &&
|
|
634
|
+
typeof m.content === 'string' &&
|
|
635
|
+
m.content.includes(insight.title)
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
if (unsurfaced.length > 0) {
|
|
639
|
+
const top = unsurfaced[0]!;
|
|
640
|
+
nudgedInsightIds.add(top.id);
|
|
641
|
+
const nudge = `\n\n[PROACTIVE CONTEXT: ${top.title}. ${top.description}${top.suggestion ? ` Suggestion: ${top.suggestion}` : ''}. Mention this naturally if relevant, or bring it up if there's a lull.]`;
|
|
642
|
+
if (isDebug) debugInsightNudge = nudge;
|
|
643
|
+
if (!systemPrompt.includes('[PROACTIVE CONTEXT:')) {
|
|
644
|
+
systemPrompt += nudge;
|
|
645
|
+
} else {
|
|
646
|
+
systemPrompt = systemPrompt.replace(/\n\n\[PROACTIVE CONTEXT:.*\]/s, nudge);
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
// Remove stale nudge from system prompt
|
|
650
|
+
systemPrompt = systemPrompt.replace(/\n\n\[PROACTIVE CONTEXT:.*\]/s, '');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
} catch { /* insights not available */ }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Check steering engine for time/event-based nudges
|
|
657
|
+
if (steeringEngine) {
|
|
658
|
+
const steerMsg = steeringEngine.check();
|
|
659
|
+
if (steerMsg) {
|
|
660
|
+
// Append to system prompt for next turn
|
|
661
|
+
if (systemPrompt.includes('[STEER') || systemPrompt.includes('[CONTEXT NOTE]') || systemPrompt.includes('[URGENT STEER]')) {
|
|
662
|
+
systemPrompt = systemPrompt.replace(/\n\n\[(CONTEXT NOTE|STEER|URGENT STEER)\].*$/s, '\n\n' + steerMsg);
|
|
663
|
+
} else {
|
|
664
|
+
systemPrompt += '\n\n' + steerMsg;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Debug mode: emit structured NDJSON per turn
|
|
670
|
+
if (isDebug) {
|
|
671
|
+
debugTurnCount++;
|
|
672
|
+
const debugOutput: AssistantDebugTurn = {
|
|
673
|
+
turn: debugTurnCount,
|
|
674
|
+
input,
|
|
675
|
+
toolCalls: debugToolCalls,
|
|
676
|
+
response: debugResponseText,
|
|
677
|
+
tokensUsed,
|
|
678
|
+
systemPromptLength: systemPrompt.length,
|
|
679
|
+
insightNudge: debugInsightNudge,
|
|
680
|
+
conversationId: conversation.id,
|
|
681
|
+
};
|
|
682
|
+
process.stdout.write(JSON.stringify(debugOutput) + '\n');
|
|
683
|
+
// Reset for next turn
|
|
684
|
+
debugToolCalls = [];
|
|
685
|
+
debugStreamToolNames.clear();
|
|
686
|
+
debugResponseText = '';
|
|
687
|
+
debugInsightNudge = undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
372
690
|
if (!result.success && result.summary) {
|
|
373
|
-
out(`\n ${c.red(result.summary)}\n`);
|
|
691
|
+
if (!isDebug) out(`\n ${c.red(result.summary)}\n`);
|
|
374
692
|
}
|
|
375
693
|
} catch (err: unknown) {
|
|
376
694
|
const msg = err instanceof Error ? err.message : String(err);
|
|
377
|
-
|
|
695
|
+
if (isDebug) {
|
|
696
|
+
process.stdout.write(JSON.stringify({ turn: ++debugTurnCount, error: msg, conversationId: conversation.id }) + '\n');
|
|
697
|
+
} else {
|
|
698
|
+
out(`\n ${c.red('Error:')} ${msg}\n`);
|
|
699
|
+
}
|
|
378
700
|
}
|
|
379
701
|
|
|
380
|
-
out('\n');
|
|
702
|
+
if (!isDebug) out('\n');
|
|
381
703
|
}
|
|
382
704
|
|
|
383
705
|
watcher?.close();
|
|
384
706
|
richInput?.destroy();
|
|
385
|
-
out(`\n ${c.dim('Goodbye.')}\n\n`);
|
|
707
|
+
if (!isDebug) out(`\n ${c.dim('Goodbye.')}\n\n`);
|
|
386
708
|
}
|
|
387
709
|
|
|
388
710
|
function toolPreview(name: string, args: Record<string, unknown>): string {
|