@opencode_weave/weave 0.6.4 → 0.7.0-preview.1
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/config/schema.d.ts +9 -0
- package/dist/features/analytics/adherence.d.ts +10 -0
- package/dist/features/analytics/format-metrics.d.ts +10 -0
- package/dist/features/analytics/generate-metrics-report.d.ts +17 -0
- package/dist/features/analytics/git-diff.d.ts +7 -0
- package/dist/features/analytics/index.d.ts +13 -6
- package/dist/features/analytics/plan-parser.d.ts +7 -0
- package/dist/features/analytics/plan-token-aggregator.d.ts +11 -0
- package/dist/features/analytics/session-tracker.d.ts +20 -0
- package/dist/features/analytics/storage.d.ts +13 -1
- package/dist/features/analytics/token-report.d.ts +14 -0
- package/dist/features/analytics/types.d.ts +89 -1
- package/dist/features/builtin-commands/templates/metrics.d.ts +1 -0
- package/dist/features/builtin-commands/templates/run-workflow.d.ts +1 -0
- package/dist/features/builtin-commands/types.d.ts +1 -1
- package/dist/features/workflow/commands.d.ts +17 -0
- package/dist/features/workflow/completion.d.ts +31 -0
- package/dist/features/workflow/constants.d.ts +12 -0
- package/dist/features/workflow/context.d.ts +16 -0
- package/dist/features/workflow/discovery.d.ts +19 -0
- package/dist/features/workflow/engine.d.ts +49 -0
- package/dist/features/workflow/hook.d.ts +47 -0
- package/dist/features/workflow/index.d.ts +15 -0
- package/dist/features/workflow/schema.d.ts +118 -0
- package/dist/features/workflow/storage.d.ts +51 -0
- package/dist/features/workflow/types.d.ts +142 -0
- package/dist/hooks/create-hooks.d.ts +6 -0
- package/dist/index.js +2143 -422
- package/dist/plugin/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { join as
|
|
2
|
+
import { join as join13 } from "path";
|
|
3
3
|
|
|
4
4
|
// src/config/loader.ts
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
6
6
|
import { join as join2 } from "node:path";
|
|
7
|
-
import { homedir } from "node:os";
|
|
7
|
+
import { homedir as homedir2 } from "node:os";
|
|
8
8
|
import { parse } from "jsonc-parser";
|
|
9
9
|
|
|
10
10
|
// src/config/schema.ts
|
|
@@ -77,7 +77,11 @@ var CustomAgentConfigSchema = z.object({
|
|
|
77
77
|
});
|
|
78
78
|
var CustomAgentsConfigSchema = z.record(z.string(), CustomAgentConfigSchema);
|
|
79
79
|
var AnalyticsConfigSchema = z.object({
|
|
80
|
-
enabled: z.boolean().optional()
|
|
80
|
+
enabled: z.boolean().optional(),
|
|
81
|
+
use_fingerprint: z.boolean().optional()
|
|
82
|
+
});
|
|
83
|
+
var WorkflowConfigSchema = z.object({
|
|
84
|
+
disabled_workflows: z.array(z.string()).optional()
|
|
81
85
|
});
|
|
82
86
|
var WeaveConfigSchema = z.object({
|
|
83
87
|
$schema: z.string().optional(),
|
|
@@ -91,7 +95,8 @@ var WeaveConfigSchema = z.object({
|
|
|
91
95
|
background: BackgroundConfigSchema.optional(),
|
|
92
96
|
analytics: AnalyticsConfigSchema.optional(),
|
|
93
97
|
tmux: TmuxConfigSchema.optional(),
|
|
94
|
-
experimental: ExperimentalConfigSchema.optional()
|
|
98
|
+
experimental: ExperimentalConfigSchema.optional(),
|
|
99
|
+
workflows: WorkflowConfigSchema.optional()
|
|
95
100
|
});
|
|
96
101
|
|
|
97
102
|
// src/config/merge.ts
|
|
@@ -134,7 +139,20 @@ function mergeConfigs(user, project) {
|
|
|
134
139
|
import * as fs from "fs";
|
|
135
140
|
import * as path from "path";
|
|
136
141
|
import * as os from "os";
|
|
137
|
-
|
|
142
|
+
function getLogDir() {
|
|
143
|
+
const home = os.homedir();
|
|
144
|
+
return path.join(home, ".opencode", "logs");
|
|
145
|
+
}
|
|
146
|
+
function resolveLogFile() {
|
|
147
|
+
const dir = getLogDir();
|
|
148
|
+
try {
|
|
149
|
+
if (!fs.existsSync(dir)) {
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
return path.join(dir, "weave-opencode.log");
|
|
154
|
+
}
|
|
155
|
+
var LOG_FILE = resolveLogFile();
|
|
138
156
|
function log(message, data) {
|
|
139
157
|
try {
|
|
140
158
|
const timestamp = new Date().toISOString();
|
|
@@ -170,15 +188,15 @@ function readJsoncFile(filePath) {
|
|
|
170
188
|
}
|
|
171
189
|
function detectConfigFile(basePath) {
|
|
172
190
|
const jsoncPath = basePath + ".jsonc";
|
|
173
|
-
if (
|
|
191
|
+
if (existsSync2(jsoncPath))
|
|
174
192
|
return jsoncPath;
|
|
175
193
|
const jsonPath = basePath + ".json";
|
|
176
|
-
if (
|
|
194
|
+
if (existsSync2(jsonPath))
|
|
177
195
|
return jsonPath;
|
|
178
196
|
return null;
|
|
179
197
|
}
|
|
180
198
|
function loadWeaveConfig(directory, _ctx, _homeDir) {
|
|
181
|
-
const userBasePath = join2(_homeDir ??
|
|
199
|
+
const userBasePath = join2(_homeDir ?? homedir2(), ".config", "opencode", "weave-opencode");
|
|
182
200
|
const projectBasePath = join2(directory, ".opencode", "weave-opencode");
|
|
183
201
|
const userConfigPath = detectConfigFile(userBasePath);
|
|
184
202
|
const projectConfigPath = detectConfigFile(projectBasePath);
|
|
@@ -267,6 +285,38 @@ For each unchecked \`- [ ]\` task in the plan:
|
|
|
267
285
|
- Do NOT stop until all checkboxes are checked or you are explicitly told to stop
|
|
268
286
|
- After all tasks are complete, report a final summary`;
|
|
269
287
|
|
|
288
|
+
// src/features/builtin-commands/templates/metrics.ts
|
|
289
|
+
var METRICS_TEMPLATE = `You are being activated by the /metrics command to present Weave analytics data to the user.
|
|
290
|
+
|
|
291
|
+
## Your Mission
|
|
292
|
+
Present the injected metrics data in a clear, readable format. The data has already been loaded and formatted by the command hook — simply relay it to the user.
|
|
293
|
+
|
|
294
|
+
## Instructions
|
|
295
|
+
|
|
296
|
+
1. **Read the injected context below** — it contains pre-formatted metrics markdown
|
|
297
|
+
2. **Present it to the user** as-is — do NOT re-fetch or recalculate anything
|
|
298
|
+
3. **Answer follow-up questions** about the data if the user asks
|
|
299
|
+
4. If the data indicates analytics is disabled or no data exists, relay that message directly`;
|
|
300
|
+
|
|
301
|
+
// src/features/builtin-commands/templates/run-workflow.ts
|
|
302
|
+
var RUN_WORKFLOW_TEMPLATE = `You are being activated by the /run-workflow command to execute a multi-step workflow.
|
|
303
|
+
|
|
304
|
+
## Your Mission
|
|
305
|
+
The workflow engine will inject context below with:
|
|
306
|
+
- The workflow definition to use
|
|
307
|
+
- The user's goal for this workflow instance
|
|
308
|
+
- The current step and its prompt
|
|
309
|
+
- Context from any previously completed steps
|
|
310
|
+
|
|
311
|
+
Follow the injected step prompt. When the step is complete, the workflow engine will
|
|
312
|
+
automatically advance you to the next step.
|
|
313
|
+
|
|
314
|
+
## Rules
|
|
315
|
+
- Focus on the current step's task only
|
|
316
|
+
- Signal completion clearly (the workflow engine detects it)
|
|
317
|
+
- Do NOT skip ahead to future steps
|
|
318
|
+
- If you need user input, ask for it and wait`;
|
|
319
|
+
|
|
270
320
|
// src/features/builtin-commands/commands.ts
|
|
271
321
|
var BUILTIN_COMMANDS = {
|
|
272
322
|
"start-work": {
|
|
@@ -279,6 +329,36 @@ ${START_WORK_TEMPLATE}
|
|
|
279
329
|
<session-context>Session ID: $SESSION_ID Timestamp: $TIMESTAMP</session-context>
|
|
280
330
|
<user-request>$ARGUMENTS</user-request>`,
|
|
281
331
|
argumentHint: "[plan-name]"
|
|
332
|
+
},
|
|
333
|
+
"token-report": {
|
|
334
|
+
name: "token-report",
|
|
335
|
+
description: "Show token usage and cost report across sessions",
|
|
336
|
+
agent: "loom",
|
|
337
|
+
template: `<command-instruction>
|
|
338
|
+
Display the token usage report that has been injected below. Present it clearly to the user.
|
|
339
|
+
</command-instruction>
|
|
340
|
+
<token-report>$ARGUMENTS</token-report>`
|
|
341
|
+
},
|
|
342
|
+
metrics: {
|
|
343
|
+
name: "metrics",
|
|
344
|
+
description: "Show Weave analytics and plan metrics reports",
|
|
345
|
+
agent: "loom",
|
|
346
|
+
template: `<command-instruction>
|
|
347
|
+
${METRICS_TEMPLATE}
|
|
348
|
+
</command-instruction>
|
|
349
|
+
<metrics-data>$ARGUMENTS</metrics-data>`,
|
|
350
|
+
argumentHint: "[plan-name|all]"
|
|
351
|
+
},
|
|
352
|
+
"run-workflow": {
|
|
353
|
+
name: "run-workflow",
|
|
354
|
+
description: "Run a multi-step workflow",
|
|
355
|
+
agent: "loom",
|
|
356
|
+
template: `<command-instruction>
|
|
357
|
+
${RUN_WORKFLOW_TEMPLATE}
|
|
358
|
+
</command-instruction>
|
|
359
|
+
<session-context>Session ID: $SESSION_ID Timestamp: $TIMESTAMP</session-context>
|
|
360
|
+
<user-request>$ARGUMENTS</user-request>`,
|
|
361
|
+
argumentHint: '<workflow-name> ["goal"]'
|
|
282
362
|
}
|
|
283
363
|
};
|
|
284
364
|
// src/managers/config-handler.ts
|
|
@@ -1768,18 +1848,18 @@ function createBuiltinAgents(options = {}) {
|
|
|
1768
1848
|
}
|
|
1769
1849
|
|
|
1770
1850
|
// src/agents/prompt-loader.ts
|
|
1771
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
1772
|
-
import { resolve, isAbsolute, normalize } from "path";
|
|
1851
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
1852
|
+
import { resolve, isAbsolute, normalize, sep } from "path";
|
|
1773
1853
|
function loadPromptFile(promptFilePath, basePath) {
|
|
1774
1854
|
if (isAbsolute(promptFilePath)) {
|
|
1775
1855
|
return null;
|
|
1776
1856
|
}
|
|
1777
1857
|
const base = resolve(basePath ?? process.cwd());
|
|
1778
1858
|
const resolvedPath = normalize(resolve(base, promptFilePath));
|
|
1779
|
-
if (!resolvedPath.startsWith(base +
|
|
1859
|
+
if (!resolvedPath.startsWith(base + sep) && resolvedPath !== base) {
|
|
1780
1860
|
return null;
|
|
1781
1861
|
}
|
|
1782
|
-
if (!
|
|
1862
|
+
if (!existsSync3(resolvedPath)) {
|
|
1783
1863
|
return null;
|
|
1784
1864
|
}
|
|
1785
1865
|
return readFileSync2(resolvedPath, "utf-8").trim();
|
|
@@ -2359,7 +2439,7 @@ var WORK_STATE_FILE = "state.json";
|
|
|
2359
2439
|
var WORK_STATE_PATH = `${WEAVE_DIR}/${WORK_STATE_FILE}`;
|
|
2360
2440
|
var PLANS_DIR = `${WEAVE_DIR}/plans`;
|
|
2361
2441
|
// src/features/work-state/storage.ts
|
|
2362
|
-
import { existsSync as
|
|
2442
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync, unlinkSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, statSync } from "fs";
|
|
2363
2443
|
import { join as join6, basename } from "path";
|
|
2364
2444
|
import { execSync } from "child_process";
|
|
2365
2445
|
var UNCHECKED_RE = /^[-*]\s*\[\s*\]/gm;
|
|
@@ -2367,7 +2447,7 @@ var CHECKED_RE = /^[-*]\s*\[[xX]\]/gm;
|
|
|
2367
2447
|
function readWorkState(directory) {
|
|
2368
2448
|
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
2369
2449
|
try {
|
|
2370
|
-
if (!
|
|
2450
|
+
if (!existsSync7(filePath))
|
|
2371
2451
|
return null;
|
|
2372
2452
|
const raw = readFileSync5(filePath, "utf-8");
|
|
2373
2453
|
const parsed = JSON.parse(raw);
|
|
@@ -2386,8 +2466,8 @@ function readWorkState(directory) {
|
|
|
2386
2466
|
function writeWorkState(directory, state) {
|
|
2387
2467
|
try {
|
|
2388
2468
|
const dir = join6(directory, WEAVE_DIR);
|
|
2389
|
-
if (!
|
|
2390
|
-
|
|
2469
|
+
if (!existsSync7(dir)) {
|
|
2470
|
+
mkdirSync2(dir, { recursive: true });
|
|
2391
2471
|
}
|
|
2392
2472
|
writeFileSync(join6(dir, WORK_STATE_FILE), JSON.stringify(state, null, 2), "utf-8");
|
|
2393
2473
|
return true;
|
|
@@ -2398,7 +2478,7 @@ function writeWorkState(directory, state) {
|
|
|
2398
2478
|
function clearWorkState(directory) {
|
|
2399
2479
|
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
2400
2480
|
try {
|
|
2401
|
-
if (
|
|
2481
|
+
if (existsSync7(filePath)) {
|
|
2402
2482
|
unlinkSync(filePath);
|
|
2403
2483
|
}
|
|
2404
2484
|
return true;
|
|
@@ -2442,7 +2522,7 @@ function getHeadSha(directory) {
|
|
|
2442
2522
|
function findPlans(directory) {
|
|
2443
2523
|
const plansDir = join6(directory, PLANS_DIR);
|
|
2444
2524
|
try {
|
|
2445
|
-
if (!
|
|
2525
|
+
if (!existsSync7(plansDir))
|
|
2446
2526
|
return [];
|
|
2447
2527
|
const files = readdirSync2(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
2448
2528
|
const fullPath = join6(plansDir, f);
|
|
@@ -2455,7 +2535,7 @@ function findPlans(directory) {
|
|
|
2455
2535
|
}
|
|
2456
2536
|
}
|
|
2457
2537
|
function getPlanProgress(planPath) {
|
|
2458
|
-
if (!
|
|
2538
|
+
if (!existsSync7(planPath)) {
|
|
2459
2539
|
return { total: 0, completed: 0, isComplete: true };
|
|
2460
2540
|
}
|
|
2461
2541
|
try {
|
|
@@ -2491,14 +2571,14 @@ function resumeWork(directory) {
|
|
|
2491
2571
|
return writeWorkState(directory, state);
|
|
2492
2572
|
}
|
|
2493
2573
|
// src/features/work-state/validation.ts
|
|
2494
|
-
import { readFileSync as readFileSync6, existsSync as
|
|
2495
|
-
import { resolve as resolve3, sep } from "path";
|
|
2574
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
|
|
2575
|
+
import { resolve as resolve3, sep as sep2 } from "path";
|
|
2496
2576
|
function validatePlan(planPath, projectDir) {
|
|
2497
2577
|
const errors = [];
|
|
2498
2578
|
const warnings = [];
|
|
2499
2579
|
const resolvedPlanPath = resolve3(planPath);
|
|
2500
2580
|
const allowedDir = resolve3(projectDir, PLANS_DIR);
|
|
2501
|
-
if (!resolvedPlanPath.startsWith(allowedDir +
|
|
2581
|
+
if (!resolvedPlanPath.startsWith(allowedDir + sep2) && resolvedPlanPath !== allowedDir) {
|
|
2502
2582
|
errors.push({
|
|
2503
2583
|
severity: "error",
|
|
2504
2584
|
category: "structure",
|
|
@@ -2506,7 +2586,7 @@ function validatePlan(planPath, projectDir) {
|
|
|
2506
2586
|
});
|
|
2507
2587
|
return { valid: false, errors, warnings };
|
|
2508
2588
|
}
|
|
2509
|
-
if (!
|
|
2589
|
+
if (!existsSync8(resolvedPlanPath)) {
|
|
2510
2590
|
errors.push({
|
|
2511
2591
|
severity: "error",
|
|
2512
2592
|
category: "structure",
|
|
@@ -2678,7 +2758,7 @@ function validateFileReferences(content, projectDir, warnings) {
|
|
|
2678
2758
|
}
|
|
2679
2759
|
const resolvedProject = resolve3(projectDir);
|
|
2680
2760
|
const absolutePath = resolve3(projectDir, filePath);
|
|
2681
|
-
if (!absolutePath.startsWith(resolvedProject +
|
|
2761
|
+
if (!absolutePath.startsWith(resolvedProject + sep2) && absolutePath !== resolvedProject) {
|
|
2682
2762
|
warnings.push({
|
|
2683
2763
|
severity: "warning",
|
|
2684
2764
|
category: "file-references",
|
|
@@ -2686,7 +2766,7 @@ function validateFileReferences(content, projectDir, warnings) {
|
|
|
2686
2766
|
});
|
|
2687
2767
|
continue;
|
|
2688
2768
|
}
|
|
2689
|
-
if (!
|
|
2769
|
+
if (!existsSync8(absolutePath)) {
|
|
2690
2770
|
warnings.push({
|
|
2691
2771
|
severity: "warning",
|
|
2692
2772
|
category: "file-references",
|
|
@@ -2770,246 +2850,1203 @@ function validateVerificationSection(content, errors) {
|
|
|
2770
2850
|
});
|
|
2771
2851
|
}
|
|
2772
2852
|
}
|
|
2773
|
-
// src/
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2853
|
+
// src/features/workflow/constants.ts
|
|
2854
|
+
var WORKFLOWS_STATE_DIR = ".weave/workflows";
|
|
2855
|
+
var INSTANCE_STATE_FILE = "state.json";
|
|
2856
|
+
var ACTIVE_INSTANCE_FILE = "active-instance.json";
|
|
2857
|
+
var WORKFLOWS_DIR_PROJECT = ".opencode/workflows";
|
|
2858
|
+
var WORKFLOWS_DIR_USER = "workflows";
|
|
2859
|
+
// src/features/workflow/storage.ts
|
|
2860
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
|
|
2861
|
+
import { join as join7 } from "path";
|
|
2862
|
+
import { randomBytes } from "node:crypto";
|
|
2863
|
+
function generateInstanceId() {
|
|
2864
|
+
return `wf_${randomBytes(4).toString("hex")}`;
|
|
2865
|
+
}
|
|
2866
|
+
function generateSlug(goal) {
|
|
2867
|
+
return goal.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 50);
|
|
2868
|
+
}
|
|
2869
|
+
function createWorkflowInstance(definition, definitionPath, goal, sessionId) {
|
|
2870
|
+
const instanceId = generateInstanceId();
|
|
2871
|
+
const slug = generateSlug(goal);
|
|
2872
|
+
const firstStepId = definition.steps[0].id;
|
|
2873
|
+
const steps = {};
|
|
2874
|
+
for (const step of definition.steps) {
|
|
2875
|
+
steps[step.id] = {
|
|
2876
|
+
id: step.id,
|
|
2877
|
+
status: step.id === firstStepId ? "active" : "pending",
|
|
2878
|
+
...step.id === firstStepId ? { started_at: new Date().toISOString() } : {}
|
|
2879
|
+
};
|
|
2778
2880
|
}
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2881
|
+
return {
|
|
2882
|
+
instance_id: instanceId,
|
|
2883
|
+
definition_id: definition.name,
|
|
2884
|
+
definition_name: definition.name,
|
|
2885
|
+
definition_path: definitionPath,
|
|
2886
|
+
goal,
|
|
2887
|
+
slug,
|
|
2888
|
+
status: "running",
|
|
2889
|
+
started_at: new Date().toISOString(),
|
|
2890
|
+
session_ids: [sessionId],
|
|
2891
|
+
current_step_id: firstStepId,
|
|
2892
|
+
steps,
|
|
2893
|
+
artifacts: {}
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
function readWorkflowInstance(directory, instanceId) {
|
|
2897
|
+
const filePath = join7(directory, WORKFLOWS_STATE_DIR, instanceId, INSTANCE_STATE_FILE);
|
|
2898
|
+
try {
|
|
2899
|
+
if (!existsSync9(filePath))
|
|
2900
|
+
return null;
|
|
2901
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
2902
|
+
const parsed = JSON.parse(raw);
|
|
2903
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
2904
|
+
return null;
|
|
2905
|
+
if (typeof parsed.instance_id !== "string")
|
|
2906
|
+
return null;
|
|
2907
|
+
return parsed;
|
|
2908
|
+
} catch {
|
|
2909
|
+
return null;
|
|
2784
2910
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2911
|
+
}
|
|
2912
|
+
function writeWorkflowInstance(directory, instance) {
|
|
2913
|
+
try {
|
|
2914
|
+
const dir = join7(directory, WORKFLOWS_STATE_DIR, instance.instance_id);
|
|
2915
|
+
if (!existsSync9(dir)) {
|
|
2916
|
+
mkdirSync3(dir, { recursive: true });
|
|
2917
|
+
}
|
|
2918
|
+
writeFileSync2(join7(dir, INSTANCE_STATE_FILE), JSON.stringify(instance, null, 2), "utf-8");
|
|
2919
|
+
return true;
|
|
2920
|
+
} catch {
|
|
2921
|
+
return false;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function readActiveInstance(directory) {
|
|
2925
|
+
const filePath = join7(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
|
|
2926
|
+
try {
|
|
2927
|
+
if (!existsSync9(filePath))
|
|
2928
|
+
return null;
|
|
2929
|
+
const raw = readFileSync7(filePath, "utf-8");
|
|
2930
|
+
const parsed = JSON.parse(raw);
|
|
2931
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.instance_id !== "string")
|
|
2932
|
+
return null;
|
|
2933
|
+
return parsed;
|
|
2934
|
+
} catch {
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
function setActiveInstance(directory, instanceId) {
|
|
2939
|
+
try {
|
|
2940
|
+
const dir = join7(directory, WORKFLOWS_STATE_DIR);
|
|
2941
|
+
if (!existsSync9(dir)) {
|
|
2942
|
+
mkdirSync3(dir, { recursive: true });
|
|
2943
|
+
}
|
|
2944
|
+
const pointer = { instance_id: instanceId };
|
|
2945
|
+
writeFileSync2(join7(dir, ACTIVE_INSTANCE_FILE), JSON.stringify(pointer, null, 2), "utf-8");
|
|
2946
|
+
return true;
|
|
2947
|
+
} catch {
|
|
2948
|
+
return false;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function clearActiveInstance(directory) {
|
|
2952
|
+
const filePath = join7(directory, WORKFLOWS_STATE_DIR, ACTIVE_INSTANCE_FILE);
|
|
2953
|
+
try {
|
|
2954
|
+
if (existsSync9(filePath)) {
|
|
2955
|
+
unlinkSync2(filePath);
|
|
2956
|
+
}
|
|
2957
|
+
return true;
|
|
2958
|
+
} catch {
|
|
2959
|
+
return false;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
function getActiveWorkflowInstance(directory) {
|
|
2963
|
+
const pointer = readActiveInstance(directory);
|
|
2964
|
+
if (!pointer)
|
|
2965
|
+
return null;
|
|
2966
|
+
return readWorkflowInstance(directory, pointer.instance_id);
|
|
2967
|
+
}
|
|
2968
|
+
// src/features/workflow/discovery.ts
|
|
2969
|
+
import * as fs5 from "fs";
|
|
2970
|
+
import * as path5 from "path";
|
|
2971
|
+
import * as os3 from "os";
|
|
2972
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
2973
|
+
|
|
2974
|
+
// src/features/workflow/schema.ts
|
|
2975
|
+
import { z as z2 } from "zod";
|
|
2976
|
+
var CompletionConfigSchema = z2.object({
|
|
2977
|
+
method: z2.enum(["user_confirm", "plan_created", "plan_complete", "review_verdict", "agent_signal"]),
|
|
2978
|
+
plan_name: z2.string().optional(),
|
|
2979
|
+
keywords: z2.array(z2.string()).optional()
|
|
2980
|
+
});
|
|
2981
|
+
var ArtifactRefSchema = z2.object({
|
|
2982
|
+
name: z2.string(),
|
|
2983
|
+
description: z2.string().optional()
|
|
2984
|
+
});
|
|
2985
|
+
var StepArtifactsSchema = z2.object({
|
|
2986
|
+
inputs: z2.array(ArtifactRefSchema).optional(),
|
|
2987
|
+
outputs: z2.array(ArtifactRefSchema).optional()
|
|
2988
|
+
});
|
|
2989
|
+
var WorkflowStepSchema = z2.object({
|
|
2990
|
+
id: z2.string().regex(/^[a-z][a-z0-9-]*$/, "Step ID must be lowercase alphanumeric with hyphens"),
|
|
2991
|
+
name: z2.string(),
|
|
2992
|
+
type: z2.enum(["interactive", "autonomous", "gate"]),
|
|
2993
|
+
agent: z2.string(),
|
|
2994
|
+
prompt: z2.string(),
|
|
2995
|
+
completion: CompletionConfigSchema,
|
|
2996
|
+
artifacts: StepArtifactsSchema.optional(),
|
|
2997
|
+
on_reject: z2.enum(["pause", "fail"]).optional()
|
|
2998
|
+
});
|
|
2999
|
+
var WorkflowDefinitionSchema = z2.object({
|
|
3000
|
+
name: z2.string().regex(/^[a-z][a-z0-9-]*$/, "Workflow name must be lowercase alphanumeric with hyphens"),
|
|
3001
|
+
description: z2.string().optional(),
|
|
3002
|
+
version: z2.number().int().positive(),
|
|
3003
|
+
steps: z2.array(WorkflowStepSchema).min(1, "Workflow must have at least one step")
|
|
3004
|
+
});
|
|
2797
3005
|
|
|
2798
|
-
|
|
2799
|
-
|
|
3006
|
+
// src/features/workflow/discovery.ts
|
|
3007
|
+
function loadWorkflowDefinition(filePath) {
|
|
3008
|
+
let raw;
|
|
3009
|
+
try {
|
|
3010
|
+
raw = fs5.readFileSync(filePath, "utf-8");
|
|
3011
|
+
} catch (err) {
|
|
3012
|
+
log("Failed to read workflow definition file", { filePath, error: String(err) });
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
let parsed;
|
|
3016
|
+
try {
|
|
3017
|
+
parsed = parseJsonc(raw);
|
|
3018
|
+
} catch (err) {
|
|
3019
|
+
log("Failed to parse workflow definition JSONC", { filePath, error: String(err) });
|
|
3020
|
+
return null;
|
|
3021
|
+
}
|
|
3022
|
+
const result = WorkflowDefinitionSchema.safeParse(parsed);
|
|
3023
|
+
if (!result.success) {
|
|
3024
|
+
log("Workflow definition failed validation", {
|
|
3025
|
+
filePath,
|
|
3026
|
+
errors: result.error.issues.map((i) => i.message)
|
|
3027
|
+
});
|
|
3028
|
+
return null;
|
|
3029
|
+
}
|
|
3030
|
+
return result.data;
|
|
3031
|
+
}
|
|
3032
|
+
function scanWorkflowDirectory(directory, scope) {
|
|
3033
|
+
if (!fs5.existsSync(directory))
|
|
3034
|
+
return [];
|
|
3035
|
+
let entries;
|
|
3036
|
+
try {
|
|
3037
|
+
entries = fs5.readdirSync(directory, { withFileTypes: true });
|
|
3038
|
+
} catch (err) {
|
|
3039
|
+
log("Failed to read workflows directory", { directory, error: String(err) });
|
|
3040
|
+
return [];
|
|
3041
|
+
}
|
|
3042
|
+
const workflows = [];
|
|
3043
|
+
for (const entry of entries) {
|
|
3044
|
+
if (!entry.isFile())
|
|
3045
|
+
continue;
|
|
3046
|
+
if (!entry.name.endsWith(".jsonc") && !entry.name.endsWith(".json"))
|
|
3047
|
+
continue;
|
|
3048
|
+
const filePath = path5.join(directory, entry.name);
|
|
3049
|
+
const definition = loadWorkflowDefinition(filePath);
|
|
3050
|
+
if (definition) {
|
|
3051
|
+
workflows.push({ definition, path: filePath, scope });
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
return workflows;
|
|
3055
|
+
}
|
|
3056
|
+
function discoverWorkflows(directory) {
|
|
3057
|
+
const projectDir = path5.join(directory, WORKFLOWS_DIR_PROJECT);
|
|
3058
|
+
const userDir = path5.join(os3.homedir(), ".config", "opencode", WORKFLOWS_DIR_USER);
|
|
3059
|
+
const userWorkflows = scanWorkflowDirectory(userDir, "user");
|
|
3060
|
+
const projectWorkflows = scanWorkflowDirectory(projectDir, "project");
|
|
3061
|
+
const byName = new Map;
|
|
3062
|
+
for (const wf of userWorkflows) {
|
|
3063
|
+
byName.set(wf.definition.name, wf);
|
|
3064
|
+
}
|
|
3065
|
+
for (const wf of projectWorkflows) {
|
|
3066
|
+
byName.set(wf.definition.name, wf);
|
|
3067
|
+
}
|
|
3068
|
+
return Array.from(byName.values());
|
|
3069
|
+
}
|
|
3070
|
+
// src/features/workflow/context.ts
|
|
3071
|
+
function resolveTemplate(template, instance, definition) {
|
|
3072
|
+
return template.replace(/\{\{(\w+)\.(\w+)\}\}/g, (_match, namespace, key) => {
|
|
3073
|
+
switch (namespace) {
|
|
3074
|
+
case "instance": {
|
|
3075
|
+
const instanceRecord = instance;
|
|
3076
|
+
const value = instanceRecord[key];
|
|
3077
|
+
return typeof value === "string" ? value : _match;
|
|
2800
3078
|
}
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3079
|
+
case "artifacts": {
|
|
3080
|
+
const value = instance.artifacts[key];
|
|
3081
|
+
return value ?? "(not yet available)";
|
|
3082
|
+
}
|
|
3083
|
+
case "step": {
|
|
3084
|
+
const currentStep = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3085
|
+
if (!currentStep)
|
|
3086
|
+
return _match;
|
|
3087
|
+
const stepRecord = currentStep;
|
|
3088
|
+
const value = stepRecord[key];
|
|
3089
|
+
return typeof value === "string" ? value : _match;
|
|
2812
3090
|
}
|
|
3091
|
+
default:
|
|
3092
|
+
return _match;
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
}
|
|
3096
|
+
function buildContextHeader(instance, definition) {
|
|
3097
|
+
const currentStepIndex = definition.steps.findIndex((s) => s.id === instance.current_step_id);
|
|
3098
|
+
const currentStepDef = definition.steps[currentStepIndex];
|
|
3099
|
+
const stepLabel = currentStepDef ? `step ${currentStepIndex + 1} of ${definition.steps.length}: ${currentStepDef.name}` : `step ${currentStepIndex + 1} of ${definition.steps.length}`;
|
|
3100
|
+
const lines = [];
|
|
3101
|
+
lines.push("## Workflow Context");
|
|
3102
|
+
lines.push(`**Goal**: "${instance.goal}"`);
|
|
3103
|
+
lines.push(`**Workflow**: ${definition.name} (${stepLabel})`);
|
|
3104
|
+
lines.push("");
|
|
3105
|
+
const completedSteps = definition.steps.filter((s) => {
|
|
3106
|
+
const state = instance.steps[s.id];
|
|
3107
|
+
return state && state.status === "completed";
|
|
3108
|
+
});
|
|
3109
|
+
if (completedSteps.length > 0) {
|
|
3110
|
+
lines.push("### Completed Steps");
|
|
3111
|
+
for (const stepDef of completedSteps) {
|
|
3112
|
+
const state = instance.steps[stepDef.id];
|
|
3113
|
+
const summary = state.summary ? ` → "${truncateSummary(state.summary)}"` : "";
|
|
3114
|
+
lines.push(`- [✓] **${stepDef.name}**${summary}`);
|
|
3115
|
+
}
|
|
3116
|
+
lines.push("");
|
|
3117
|
+
}
|
|
3118
|
+
const artifactEntries = Object.entries(instance.artifacts);
|
|
3119
|
+
if (artifactEntries.length > 0) {
|
|
3120
|
+
lines.push("### Accumulated Artifacts");
|
|
3121
|
+
for (const [name, value] of artifactEntries) {
|
|
3122
|
+
lines.push(`- **${name}**: "${truncateSummary(value)}"`);
|
|
3123
|
+
}
|
|
3124
|
+
lines.push("");
|
|
3125
|
+
}
|
|
3126
|
+
return lines.join(`
|
|
3127
|
+
`);
|
|
3128
|
+
}
|
|
3129
|
+
function composeStepPrompt(stepDef, instance, definition) {
|
|
3130
|
+
const contextHeader = buildContextHeader(instance, definition);
|
|
3131
|
+
const resolvedPrompt = resolveTemplate(stepDef.prompt, instance, definition);
|
|
3132
|
+
return `${contextHeader}---
|
|
3133
|
+
|
|
3134
|
+
## Your Task
|
|
3135
|
+
${resolvedPrompt}`;
|
|
3136
|
+
}
|
|
3137
|
+
function truncateSummary(text) {
|
|
3138
|
+
const maxLength = 200;
|
|
3139
|
+
if (text.length <= maxLength)
|
|
3140
|
+
return text;
|
|
3141
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
3142
|
+
}
|
|
3143
|
+
// src/features/workflow/completion.ts
|
|
3144
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3145
|
+
import { join as join9 } from "path";
|
|
3146
|
+
var DEFAULT_CONFIRM_KEYWORDS = ["confirmed", "approved", "continue", "done", "let's proceed", "looks good", "lgtm"];
|
|
3147
|
+
var VERDICT_APPROVE_RE = /\[\s*APPROVE\s*\]/i;
|
|
3148
|
+
var VERDICT_REJECT_RE = /\[\s*REJECT\s*\]/i;
|
|
3149
|
+
var AGENT_SIGNAL_MARKER = "<!-- workflow:step-complete -->";
|
|
3150
|
+
function checkStepCompletion(method, context) {
|
|
3151
|
+
switch (method) {
|
|
3152
|
+
case "user_confirm":
|
|
3153
|
+
return checkUserConfirm(context);
|
|
3154
|
+
case "plan_created":
|
|
3155
|
+
return checkPlanCreated(context);
|
|
3156
|
+
case "plan_complete":
|
|
3157
|
+
return checkPlanComplete(context);
|
|
3158
|
+
case "review_verdict":
|
|
3159
|
+
return checkReviewVerdict(context);
|
|
3160
|
+
case "agent_signal":
|
|
3161
|
+
return checkAgentSignal(context);
|
|
3162
|
+
default:
|
|
3163
|
+
return { complete: false, reason: `Unknown completion method: ${method}` };
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
function checkUserConfirm(context) {
|
|
3167
|
+
const { lastUserMessage, config } = context;
|
|
3168
|
+
if (!lastUserMessage)
|
|
3169
|
+
return { complete: false };
|
|
3170
|
+
const keywords = config.keywords ?? DEFAULT_CONFIRM_KEYWORDS;
|
|
3171
|
+
const lowerMessage = lastUserMessage.toLowerCase().trim();
|
|
3172
|
+
for (const keyword of keywords) {
|
|
3173
|
+
if (lowerMessage.includes(keyword.toLowerCase())) {
|
|
2813
3174
|
return {
|
|
2814
|
-
|
|
2815
|
-
|
|
3175
|
+
complete: true,
|
|
3176
|
+
summary: `User confirmed: "${lastUserMessage.slice(0, 100)}"`
|
|
2816
3177
|
};
|
|
2817
3178
|
}
|
|
2818
3179
|
}
|
|
2819
|
-
return
|
|
2820
|
-
}
|
|
2821
|
-
function extractPlanName(promptText) {
|
|
2822
|
-
const match = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i);
|
|
2823
|
-
if (!match)
|
|
2824
|
-
return null;
|
|
2825
|
-
const cleaned = match[1].trim();
|
|
2826
|
-
return cleaned || null;
|
|
3180
|
+
return { complete: false };
|
|
2827
3181
|
}
|
|
2828
|
-
function
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
`) : " (none)";
|
|
2834
|
-
return {
|
|
2835
|
-
switchAgent: "tapestry",
|
|
2836
|
-
contextInjection: `## Plan Not Found
|
|
2837
|
-
No plan matching "${requestedName}" was found.
|
|
2838
|
-
|
|
2839
|
-
Available incomplete plans:
|
|
2840
|
-
${listing}
|
|
2841
|
-
|
|
2842
|
-
Tell the user which plans are available and ask them to specify one.`
|
|
2843
|
-
};
|
|
3182
|
+
function checkPlanCreated(context) {
|
|
3183
|
+
const { config, directory } = context;
|
|
3184
|
+
const planName = config.plan_name;
|
|
3185
|
+
if (!planName) {
|
|
3186
|
+
return { complete: false, reason: "plan_created requires plan_name in completion config" };
|
|
2844
3187
|
}
|
|
2845
|
-
const
|
|
2846
|
-
|
|
3188
|
+
const plans = findPlans(directory);
|
|
3189
|
+
const matchingPlan = plans.find((p) => p.includes(planName));
|
|
3190
|
+
if (matchingPlan) {
|
|
2847
3191
|
return {
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
Tell the user this plan is already done and suggest creating a new one with Pattern.`
|
|
3192
|
+
complete: true,
|
|
3193
|
+
artifacts: { plan_path: matchingPlan },
|
|
3194
|
+
summary: `Plan created at ${matchingPlan}`
|
|
2852
3195
|
};
|
|
2853
3196
|
}
|
|
2854
|
-
const
|
|
2855
|
-
if (
|
|
3197
|
+
const directPath = join9(directory, ".weave", "plans", `${planName}.md`);
|
|
3198
|
+
if (existsSync11(directPath)) {
|
|
2856
3199
|
return {
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
${formatValidationResults(validation)}
|
|
2862
|
-
|
|
2863
|
-
Tell the user to fix these issues in the plan file and try again.`
|
|
3200
|
+
complete: true,
|
|
3201
|
+
artifacts: { plan_path: directPath },
|
|
3202
|
+
summary: `Plan created at ${directPath}`
|
|
2864
3203
|
};
|
|
2865
3204
|
}
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
const
|
|
2870
|
-
|
|
3205
|
+
return { complete: false };
|
|
3206
|
+
}
|
|
3207
|
+
function checkPlanComplete(context) {
|
|
3208
|
+
const { config, directory } = context;
|
|
3209
|
+
const planName = config.plan_name;
|
|
3210
|
+
if (!planName) {
|
|
3211
|
+
return { complete: false, reason: "plan_complete requires plan_name in completion config" };
|
|
3212
|
+
}
|
|
3213
|
+
const planPath = join9(directory, ".weave", "plans", `${planName}.md`);
|
|
3214
|
+
if (!existsSync11(planPath)) {
|
|
3215
|
+
return { complete: false, reason: `Plan file not found: ${planPath}` };
|
|
3216
|
+
}
|
|
3217
|
+
const progress = getPlanProgress(planPath);
|
|
3218
|
+
if (progress.isComplete) {
|
|
2871
3219
|
return {
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
### Validation Warnings
|
|
2876
|
-
${formatValidationResults(validation)}`
|
|
3220
|
+
complete: true,
|
|
3221
|
+
summary: `Plan completed: ${progress.completed}/${progress.total} tasks done`
|
|
2877
3222
|
};
|
|
2878
3223
|
}
|
|
2879
3224
|
return {
|
|
2880
|
-
|
|
2881
|
-
|
|
3225
|
+
complete: false,
|
|
3226
|
+
reason: `Plan in progress: ${progress.completed}/${progress.total} tasks done`
|
|
2882
3227
|
};
|
|
2883
3228
|
}
|
|
2884
|
-
function
|
|
2885
|
-
|
|
3229
|
+
function checkReviewVerdict(context) {
|
|
3230
|
+
const { lastAssistantMessage } = context;
|
|
3231
|
+
if (!lastAssistantMessage)
|
|
3232
|
+
return { complete: false };
|
|
3233
|
+
if (VERDICT_APPROVE_RE.test(lastAssistantMessage)) {
|
|
2886
3234
|
return {
|
|
2887
|
-
|
|
2888
|
-
|
|
3235
|
+
complete: true,
|
|
3236
|
+
verdict: "approve",
|
|
3237
|
+
summary: "Review verdict: APPROVED"
|
|
2889
3238
|
};
|
|
2890
3239
|
}
|
|
2891
|
-
|
|
2892
|
-
if (incompletePlans.length === 0) {
|
|
3240
|
+
if (VERDICT_REJECT_RE.test(lastAssistantMessage)) {
|
|
2893
3241
|
return {
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
Tell the user to switch to Pattern agent to create a new plan.`
|
|
3242
|
+
complete: true,
|
|
3243
|
+
verdict: "reject",
|
|
3244
|
+
summary: "Review verdict: REJECTED"
|
|
2898
3245
|
};
|
|
2899
3246
|
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3247
|
+
return { complete: false };
|
|
3248
|
+
}
|
|
3249
|
+
function checkAgentSignal(context) {
|
|
3250
|
+
const { lastAssistantMessage } = context;
|
|
3251
|
+
if (!lastAssistantMessage)
|
|
3252
|
+
return { complete: false };
|
|
3253
|
+
if (lastAssistantMessage.includes(AGENT_SIGNAL_MARKER)) {
|
|
3254
|
+
return {
|
|
3255
|
+
complete: true,
|
|
3256
|
+
summary: "Agent signaled completion"
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
return { complete: false };
|
|
3260
|
+
}
|
|
3261
|
+
// src/features/workflow/engine.ts
|
|
3262
|
+
function startWorkflow(input) {
|
|
3263
|
+
const { definition, definitionPath, goal, sessionId, directory } = input;
|
|
3264
|
+
const instance = createWorkflowInstance(definition, definitionPath, goal, sessionId);
|
|
3265
|
+
writeWorkflowInstance(directory, instance);
|
|
3266
|
+
setActiveInstance(directory, instance.instance_id);
|
|
3267
|
+
const firstStepDef = definition.steps[0];
|
|
3268
|
+
const prompt = composeStepPrompt(firstStepDef, instance, definition);
|
|
3269
|
+
return {
|
|
3270
|
+
type: "inject_prompt",
|
|
3271
|
+
prompt,
|
|
3272
|
+
agent: firstStepDef.agent
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
function checkAndAdvance(input) {
|
|
3276
|
+
const { directory, context } = input;
|
|
3277
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3278
|
+
if (!instance)
|
|
3279
|
+
return { type: "none" };
|
|
3280
|
+
if (instance.status !== "running")
|
|
3281
|
+
return { type: "none" };
|
|
3282
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3283
|
+
if (!definition)
|
|
3284
|
+
return { type: "none", reason: "Failed to load workflow definition" };
|
|
3285
|
+
const currentStepDef = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3286
|
+
if (!currentStepDef)
|
|
3287
|
+
return { type: "none", reason: "Current step not found in definition" };
|
|
3288
|
+
const stepState = instance.steps[instance.current_step_id];
|
|
3289
|
+
if (!stepState || stepState.status !== "active")
|
|
3290
|
+
return { type: "none" };
|
|
3291
|
+
const completionResult = checkStepCompletion(currentStepDef.completion.method, context);
|
|
3292
|
+
if (!completionResult.complete) {
|
|
3293
|
+
if (currentStepDef.type === "interactive")
|
|
3294
|
+
return { type: "none" };
|
|
3295
|
+
return { type: "none" };
|
|
3296
|
+
}
|
|
3297
|
+
if (currentStepDef.type === "gate" && completionResult.verdict === "reject") {
|
|
3298
|
+
return handleGateReject(directory, instance, currentStepDef, completionResult);
|
|
3299
|
+
}
|
|
3300
|
+
return advanceToNextStep(directory, instance, definition, completionResult);
|
|
3301
|
+
}
|
|
3302
|
+
function handleGateReject(directory, instance, currentStepDef, completionResult) {
|
|
3303
|
+
const stepState = instance.steps[currentStepDef.id];
|
|
3304
|
+
stepState.status = "completed";
|
|
3305
|
+
stepState.completed_at = new Date().toISOString();
|
|
3306
|
+
stepState.verdict = "reject";
|
|
3307
|
+
stepState.summary = completionResult.summary;
|
|
3308
|
+
const action = currentStepDef.on_reject ?? "pause";
|
|
3309
|
+
if (action === "fail") {
|
|
3310
|
+
instance.status = "failed";
|
|
3311
|
+
instance.ended_at = new Date().toISOString();
|
|
3312
|
+
clearActiveInstance(directory);
|
|
3313
|
+
} else {
|
|
3314
|
+
instance.status = "paused";
|
|
3315
|
+
instance.pause_reason = "Gate step rejected";
|
|
3316
|
+
}
|
|
3317
|
+
writeWorkflowInstance(directory, instance);
|
|
3318
|
+
return {
|
|
3319
|
+
type: "pause",
|
|
3320
|
+
reason: `Gate step "${currentStepDef.id}" rejected${action === "fail" ? " — workflow failed" : " — workflow paused"}`
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
function advanceToNextStep(directory, instance, definition, completionResult) {
|
|
3324
|
+
const currentStepDef = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3325
|
+
const currentIndex = definition.steps.indexOf(currentStepDef);
|
|
3326
|
+
const stepState = instance.steps[instance.current_step_id];
|
|
3327
|
+
stepState.status = "completed";
|
|
3328
|
+
stepState.completed_at = new Date().toISOString();
|
|
3329
|
+
stepState.summary = completionResult.summary;
|
|
3330
|
+
if (completionResult.verdict)
|
|
3331
|
+
stepState.verdict = completionResult.verdict;
|
|
3332
|
+
if (completionResult.artifacts) {
|
|
3333
|
+
stepState.artifacts = completionResult.artifacts;
|
|
3334
|
+
Object.assign(instance.artifacts, completionResult.artifacts);
|
|
3335
|
+
}
|
|
3336
|
+
if (currentIndex >= definition.steps.length - 1) {
|
|
3337
|
+
instance.status = "completed";
|
|
3338
|
+
instance.ended_at = new Date().toISOString();
|
|
3339
|
+
clearActiveInstance(directory);
|
|
3340
|
+
writeWorkflowInstance(directory, instance);
|
|
3341
|
+
return { type: "complete", reason: "Workflow completed — all steps done" };
|
|
3342
|
+
}
|
|
3343
|
+
const nextStepDef = definition.steps[currentIndex + 1];
|
|
3344
|
+
instance.current_step_id = nextStepDef.id;
|
|
3345
|
+
instance.steps[nextStepDef.id].status = "active";
|
|
3346
|
+
instance.steps[nextStepDef.id].started_at = new Date().toISOString();
|
|
3347
|
+
writeWorkflowInstance(directory, instance);
|
|
3348
|
+
const prompt = composeStepPrompt(nextStepDef, instance, definition);
|
|
3349
|
+
return {
|
|
3350
|
+
type: "inject_prompt",
|
|
3351
|
+
prompt,
|
|
3352
|
+
agent: nextStepDef.agent
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
function pauseWorkflow(directory, reason) {
|
|
3356
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3357
|
+
if (!instance || instance.status !== "running")
|
|
3358
|
+
return false;
|
|
3359
|
+
instance.status = "paused";
|
|
3360
|
+
instance.pause_reason = reason ?? "Paused by user";
|
|
3361
|
+
return writeWorkflowInstance(directory, instance);
|
|
3362
|
+
}
|
|
3363
|
+
function resumeWorkflow(directory) {
|
|
3364
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3365
|
+
if (!instance || instance.status !== "paused")
|
|
3366
|
+
return { type: "none", reason: "No paused workflow to resume" };
|
|
3367
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3368
|
+
if (!definition)
|
|
3369
|
+
return { type: "none", reason: "Failed to load workflow definition" };
|
|
3370
|
+
instance.status = "running";
|
|
3371
|
+
instance.pause_reason = undefined;
|
|
3372
|
+
const currentStepState = instance.steps[instance.current_step_id];
|
|
3373
|
+
if (currentStepState.status !== "active") {
|
|
3374
|
+
currentStepState.status = "active";
|
|
3375
|
+
currentStepState.started_at = new Date().toISOString();
|
|
3376
|
+
}
|
|
3377
|
+
writeWorkflowInstance(directory, instance);
|
|
3378
|
+
const currentStepDef = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3379
|
+
if (!currentStepDef)
|
|
3380
|
+
return { type: "none", reason: "Current step not found in definition" };
|
|
3381
|
+
const prompt = composeStepPrompt(currentStepDef, instance, definition);
|
|
3382
|
+
return {
|
|
3383
|
+
type: "inject_prompt",
|
|
3384
|
+
prompt,
|
|
3385
|
+
agent: currentStepDef.agent
|
|
3386
|
+
};
|
|
3387
|
+
}
|
|
3388
|
+
function skipStep(directory) {
|
|
3389
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3390
|
+
if (!instance)
|
|
3391
|
+
return { type: "none", reason: "No active workflow" };
|
|
3392
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3393
|
+
if (!definition)
|
|
3394
|
+
return { type: "none", reason: "Failed to load workflow definition" };
|
|
3395
|
+
return advanceToNextStep(directory, instance, definition, {
|
|
3396
|
+
complete: true,
|
|
3397
|
+
summary: "Step skipped by user"
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
function abortWorkflow(directory) {
|
|
3401
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3402
|
+
if (!instance)
|
|
3403
|
+
return false;
|
|
3404
|
+
instance.status = "cancelled";
|
|
3405
|
+
instance.ended_at = new Date().toISOString();
|
|
3406
|
+
clearActiveInstance(directory);
|
|
3407
|
+
return writeWorkflowInstance(directory, instance);
|
|
3408
|
+
}
|
|
3409
|
+
// src/features/workflow/hook.ts
|
|
3410
|
+
var WORKFLOW_CONTINUATION_MARKER = "<!-- weave:workflow-continuation -->";
|
|
3411
|
+
function parseWorkflowArgs(args) {
|
|
3412
|
+
const trimmed = args.trim();
|
|
3413
|
+
if (!trimmed)
|
|
3414
|
+
return { workflowName: null, goal: null };
|
|
3415
|
+
const quotedMatch = trimmed.match(/^(\S+)\s+"([^"]+)"$/);
|
|
3416
|
+
if (quotedMatch) {
|
|
3417
|
+
return { workflowName: quotedMatch[1], goal: quotedMatch[2] };
|
|
3418
|
+
}
|
|
3419
|
+
const singleQuotedMatch = trimmed.match(/^(\S+)\s+'([^']+)'$/);
|
|
3420
|
+
if (singleQuotedMatch) {
|
|
3421
|
+
return { workflowName: singleQuotedMatch[1], goal: singleQuotedMatch[2] };
|
|
3422
|
+
}
|
|
3423
|
+
const parts = trimmed.split(/\s+/);
|
|
3424
|
+
if (parts.length === 1) {
|
|
3425
|
+
return { workflowName: parts[0], goal: null };
|
|
3426
|
+
}
|
|
3427
|
+
return { workflowName: parts[0], goal: parts.slice(1).join(" ") };
|
|
3428
|
+
}
|
|
3429
|
+
function handleRunWorkflow(input) {
|
|
3430
|
+
const { promptText, sessionId, directory } = input;
|
|
3431
|
+
if (!promptText.includes("<session-context>")) {
|
|
3432
|
+
return { contextInjection: null, switchAgent: null };
|
|
3433
|
+
}
|
|
3434
|
+
const args = extractArguments(promptText);
|
|
3435
|
+
const { workflowName, goal } = parseWorkflowArgs(args);
|
|
3436
|
+
const workStateWarning = checkWorkStatePlanActive(directory);
|
|
3437
|
+
const activeInstance = getActiveWorkflowInstance(directory);
|
|
3438
|
+
if (!workflowName && !activeInstance) {
|
|
3439
|
+
const result = listAvailableWorkflows(directory);
|
|
3440
|
+
return prependWarning(result, workStateWarning);
|
|
3441
|
+
}
|
|
3442
|
+
if (!workflowName && activeInstance) {
|
|
3443
|
+
const result = resumeActiveWorkflow(directory);
|
|
3444
|
+
return prependWarning(result, workStateWarning);
|
|
3445
|
+
}
|
|
3446
|
+
if (workflowName && !goal && activeInstance && activeInstance.definition_id === workflowName) {
|
|
3447
|
+
const result = resumeActiveWorkflow(directory);
|
|
3448
|
+
return prependWarning(result, workStateWarning);
|
|
3449
|
+
}
|
|
3450
|
+
if (workflowName && goal) {
|
|
3451
|
+
if (activeInstance) {
|
|
3452
|
+
return {
|
|
3453
|
+
contextInjection: `## Workflow Already Active
|
|
3454
|
+
There is already an active workflow: "${activeInstance.definition_name}" (${activeInstance.instance_id}).
|
|
3455
|
+
Goal: "${activeInstance.goal}"
|
|
3456
|
+
|
|
3457
|
+
To start a new workflow, first abort the current one with \`/workflow abort\` or let it complete.`,
|
|
3458
|
+
switchAgent: null
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
const result = startNewWorkflow(workflowName, goal, sessionId, directory);
|
|
3462
|
+
return prependWarning(result, workStateWarning);
|
|
3463
|
+
}
|
|
3464
|
+
if (workflowName && !goal) {
|
|
3465
|
+
if (activeInstance) {
|
|
3466
|
+
return {
|
|
3467
|
+
contextInjection: `## Workflow Already Active
|
|
3468
|
+
There is already an active workflow: "${activeInstance.definition_name}" (${activeInstance.instance_id}).
|
|
3469
|
+
Goal: "${activeInstance.goal}"
|
|
3470
|
+
|
|
3471
|
+
Did you mean to resume the active workflow? Run \`/run-workflow\` without arguments to resume.`,
|
|
3472
|
+
switchAgent: null
|
|
3473
|
+
};
|
|
2926
3474
|
}
|
|
2927
3475
|
return {
|
|
2928
|
-
|
|
2929
|
-
|
|
3476
|
+
contextInjection: `## Goal Required
|
|
3477
|
+
To start the "${workflowName}" workflow, provide a goal:
|
|
3478
|
+
\`/run-workflow ${workflowName} "your goal here"\``,
|
|
3479
|
+
switchAgent: null
|
|
2930
3480
|
};
|
|
2931
3481
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
}
|
|
3482
|
+
return { contextInjection: null, switchAgent: null };
|
|
3483
|
+
}
|
|
3484
|
+
function checkWorkflowContinuation(input) {
|
|
3485
|
+
const { directory, lastAssistantMessage, lastUserMessage } = input;
|
|
3486
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3487
|
+
if (!instance)
|
|
3488
|
+
return { continuationPrompt: null, switchAgent: null };
|
|
3489
|
+
if (instance.status !== "running")
|
|
3490
|
+
return { continuationPrompt: null, switchAgent: null };
|
|
3491
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3492
|
+
if (!definition)
|
|
3493
|
+
return { continuationPrompt: null, switchAgent: null };
|
|
3494
|
+
const currentStepDef = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3495
|
+
if (!currentStepDef)
|
|
3496
|
+
return { continuationPrompt: null, switchAgent: null };
|
|
3497
|
+
const completionContext = {
|
|
3498
|
+
directory,
|
|
3499
|
+
config: currentStepDef.completion,
|
|
3500
|
+
artifacts: instance.artifacts,
|
|
3501
|
+
lastAssistantMessage,
|
|
3502
|
+
lastUserMessage
|
|
3503
|
+
};
|
|
3504
|
+
const action = checkAndAdvance({ directory, context: completionContext });
|
|
3505
|
+
switch (action.type) {
|
|
3506
|
+
case "inject_prompt":
|
|
3507
|
+
return {
|
|
3508
|
+
continuationPrompt: `${WORKFLOW_CONTINUATION_MARKER}
|
|
3509
|
+
${action.prompt}`,
|
|
3510
|
+
switchAgent: action.agent ?? null
|
|
3511
|
+
};
|
|
3512
|
+
case "complete":
|
|
3513
|
+
return {
|
|
3514
|
+
continuationPrompt: `${WORKFLOW_CONTINUATION_MARKER}
|
|
3515
|
+
## Workflow Complete
|
|
3516
|
+
${action.reason ?? "All steps have been completed."}
|
|
3517
|
+
|
|
3518
|
+
Summarize what was accomplished across all workflow steps.`,
|
|
3519
|
+
switchAgent: null
|
|
3520
|
+
};
|
|
3521
|
+
case "pause":
|
|
3522
|
+
return {
|
|
3523
|
+
continuationPrompt: `${WORKFLOW_CONTINUATION_MARKER}
|
|
3524
|
+
## Workflow Paused
|
|
3525
|
+
${action.reason ?? "The workflow has been paused."}
|
|
3526
|
+
|
|
3527
|
+
Inform the user about the pause and what to do next.`,
|
|
3528
|
+
switchAgent: null
|
|
3529
|
+
};
|
|
3530
|
+
case "none":
|
|
3531
|
+
default:
|
|
3532
|
+
return { continuationPrompt: null, switchAgent: null };
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
function checkWorkStatePlanActive(directory) {
|
|
3536
|
+
const state = readWorkState(directory);
|
|
3537
|
+
if (!state)
|
|
3538
|
+
return null;
|
|
3539
|
+
const progress = getPlanProgress(state.active_plan);
|
|
3540
|
+
if (progress.isComplete)
|
|
3541
|
+
return null;
|
|
3542
|
+
const status = state.paused ? "paused" : "running";
|
|
3543
|
+
const planName = state.plan_name ?? state.active_plan;
|
|
3544
|
+
return `## Active Plan Detected
|
|
3545
|
+
|
|
3546
|
+
There is currently an active plan being executed: "${planName}"
|
|
3547
|
+
Status: ${status} • Progress: ${progress.completed}/${progress.total} tasks complete
|
|
3548
|
+
|
|
3549
|
+
Starting a workflow will take priority over the active plan — plan continuation will be suspended while the workflow runs.
|
|
3550
|
+
|
|
3551
|
+
**Options:**
|
|
3552
|
+
- **Proceed anyway** — the plan will be paused and can be resumed with \`/start-work\` after the workflow completes
|
|
3553
|
+
- **Abort the plan first** — abandon the current plan, then start the workflow
|
|
3554
|
+
- **Cancel** — don't start the workflow, continue with the plan`;
|
|
3555
|
+
}
|
|
3556
|
+
function prependWarning(result, warning) {
|
|
3557
|
+
if (!warning)
|
|
3558
|
+
return result;
|
|
3559
|
+
if (!result.contextInjection) {
|
|
3560
|
+
return { ...result, contextInjection: warning };
|
|
3561
|
+
}
|
|
3562
|
+
return { ...result, contextInjection: `${warning}
|
|
3563
|
+
|
|
3564
|
+
---
|
|
3565
|
+
|
|
3566
|
+
${result.contextInjection}` };
|
|
3567
|
+
}
|
|
3568
|
+
function extractArguments(promptText) {
|
|
3569
|
+
const match = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i);
|
|
3570
|
+
if (!match)
|
|
3571
|
+
return "";
|
|
3572
|
+
return match[1].trim();
|
|
3573
|
+
}
|
|
3574
|
+
function listAvailableWorkflows(directory) {
|
|
3575
|
+
const workflows = discoverWorkflows(directory);
|
|
3576
|
+
if (workflows.length === 0) {
|
|
3577
|
+
return {
|
|
3578
|
+
contextInjection: "## No Workflows Available\nNo workflow definitions found.\n\nWorkflow definitions should be placed in `.opencode/workflows/` (project) or `~/.config/opencode/workflows/` (user).",
|
|
3579
|
+
switchAgent: null
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
const listing = workflows.map((w) => ` - **${w.definition.name}**: ${w.definition.description ?? "(no description)"} (${w.scope})`).join(`
|
|
2936
3583
|
`);
|
|
2937
3584
|
return {
|
|
2938
|
-
|
|
2939
|
-
contextInjection: `## Multiple Plans Found
|
|
2940
|
-
There are ${incompletePlans.length} incomplete plans:
|
|
3585
|
+
contextInjection: `## Available Workflows
|
|
2941
3586
|
${listing}
|
|
2942
3587
|
|
|
2943
|
-
|
|
3588
|
+
To start a workflow, run:
|
|
3589
|
+
\`/run-workflow <name> "your goal"\``,
|
|
3590
|
+
switchAgent: null
|
|
2944
3591
|
};
|
|
2945
3592
|
}
|
|
2946
|
-
function
|
|
2947
|
-
const
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3593
|
+
function resumeActiveWorkflow(directory) {
|
|
3594
|
+
const action = resumeWorkflow(directory);
|
|
3595
|
+
if (action.type === "none") {
|
|
3596
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3597
|
+
if (instance && instance.status === "running") {
|
|
3598
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3599
|
+
if (definition) {
|
|
3600
|
+
const currentStep = definition.steps.find((s) => s.id === instance.current_step_id);
|
|
3601
|
+
return {
|
|
3602
|
+
contextInjection: `## Workflow In Progress
|
|
3603
|
+
Workflow "${instance.definition_name}" is already running.
|
|
3604
|
+
Current step: **${currentStep?.name ?? instance.current_step_id}**
|
|
3605
|
+
Goal: "${instance.goal}"
|
|
3606
|
+
|
|
3607
|
+
Continue with the current step.`,
|
|
3608
|
+
switchAgent: currentStep?.agent ?? null
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
2960
3611
|
}
|
|
3612
|
+
return { contextInjection: null, switchAgent: null };
|
|
2961
3613
|
}
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
3614
|
+
return {
|
|
3615
|
+
contextInjection: action.prompt ?? null,
|
|
3616
|
+
switchAgent: action.agent ?? null
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
function startNewWorkflow(workflowName, goal, sessionId, directory) {
|
|
3620
|
+
const workflows = discoverWorkflows(directory);
|
|
3621
|
+
const match = workflows.find((w) => w.definition.name === workflowName);
|
|
3622
|
+
if (!match) {
|
|
3623
|
+
const available = workflows.map((w) => w.definition.name).join(", ");
|
|
3624
|
+
return {
|
|
3625
|
+
contextInjection: `## Workflow Not Found
|
|
3626
|
+
No workflow definition named "${workflowName}" was found.
|
|
3627
|
+
${available ? `Available workflows: ${available}` : "No workflow definitions available."}`,
|
|
3628
|
+
switchAgent: null
|
|
3629
|
+
};
|
|
2969
3630
|
}
|
|
2970
|
-
|
|
3631
|
+
const action = startWorkflow({
|
|
3632
|
+
definition: match.definition,
|
|
3633
|
+
definitionPath: match.path,
|
|
3634
|
+
goal,
|
|
3635
|
+
sessionId,
|
|
3636
|
+
directory
|
|
3637
|
+
});
|
|
3638
|
+
log("Workflow started", {
|
|
3639
|
+
workflowName: match.definition.name,
|
|
3640
|
+
goal,
|
|
3641
|
+
agent: action.agent
|
|
3642
|
+
});
|
|
3643
|
+
return {
|
|
3644
|
+
contextInjection: action.prompt ?? null,
|
|
3645
|
+
switchAgent: action.agent ?? null
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
// src/features/workflow/commands.ts
|
|
3649
|
+
var PAUSE_PATTERNS = [/\bworkflow\s+pause\b/i, /\bpause\s+workflow\b/i];
|
|
3650
|
+
var SKIP_PATTERNS = [/\bworkflow\s+skip\b/i, /\bskip\s+step\b/i];
|
|
3651
|
+
var ABORT_PATTERNS = [/\bworkflow\s+abort\b/i, /\babort\s+workflow\b/i];
|
|
3652
|
+
var STATUS_PATTERNS = [/\bworkflow\s+status\b/i];
|
|
3653
|
+
function handleWorkflowCommand(message, directory) {
|
|
3654
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3655
|
+
if (!instance)
|
|
3656
|
+
return { handled: false };
|
|
3657
|
+
const trimmed = message.trim();
|
|
3658
|
+
if (matchesAny(trimmed, PAUSE_PATTERNS)) {
|
|
3659
|
+
return handlePause(directory, instance);
|
|
3660
|
+
}
|
|
3661
|
+
if (matchesAny(trimmed, SKIP_PATTERNS)) {
|
|
3662
|
+
return handleSkip(directory, instance);
|
|
3663
|
+
}
|
|
3664
|
+
if (matchesAny(trimmed, ABORT_PATTERNS)) {
|
|
3665
|
+
return handleAbort(directory, instance);
|
|
3666
|
+
}
|
|
3667
|
+
if (matchesAny(trimmed, STATUS_PATTERNS)) {
|
|
3668
|
+
return handleStatus(directory, instance);
|
|
3669
|
+
}
|
|
3670
|
+
return { handled: false };
|
|
3671
|
+
}
|
|
3672
|
+
function matchesAny(text, patterns) {
|
|
3673
|
+
return patterns.some((p) => p.test(text));
|
|
3674
|
+
}
|
|
3675
|
+
function handlePause(directory, instance) {
|
|
3676
|
+
if (instance.status !== "running") {
|
|
3677
|
+
return {
|
|
3678
|
+
handled: true,
|
|
3679
|
+
contextInjection: `## Workflow Not Running
|
|
3680
|
+
The workflow "${instance.definition_name}" is currently ${instance.status}. Cannot pause.`
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
pauseWorkflow(directory, "Paused by user command");
|
|
3684
|
+
return {
|
|
3685
|
+
handled: true,
|
|
3686
|
+
contextInjection: `## Workflow Paused
|
|
3687
|
+
Workflow "${instance.definition_name}" has been paused.
|
|
3688
|
+
Goal: "${instance.goal}"
|
|
3689
|
+
Current step: ${instance.current_step_id}
|
|
3690
|
+
|
|
3691
|
+
To resume, run \`/run-workflow\`.`
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
3694
|
+
function handleSkip(directory, instance) {
|
|
3695
|
+
if (instance.status !== "running") {
|
|
3696
|
+
return {
|
|
3697
|
+
handled: true,
|
|
3698
|
+
contextInjection: `## Workflow Not Running
|
|
3699
|
+
The workflow "${instance.definition_name}" is currently ${instance.status}. Cannot skip step.`
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
const currentStepId = instance.current_step_id;
|
|
3703
|
+
const action = skipStep(directory);
|
|
3704
|
+
if (action.type === "inject_prompt") {
|
|
3705
|
+
return {
|
|
3706
|
+
handled: true,
|
|
3707
|
+
contextInjection: `## Step Skipped
|
|
3708
|
+
Skipped step "${currentStepId}".
|
|
3709
|
+
|
|
3710
|
+
${action.prompt}`,
|
|
3711
|
+
switchAgent: action.agent
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
if (action.type === "complete") {
|
|
3715
|
+
return {
|
|
3716
|
+
handled: true,
|
|
3717
|
+
contextInjection: `## Step Skipped — Workflow Complete
|
|
3718
|
+
Skipped step "${currentStepId}".
|
|
3719
|
+
${action.reason ?? "All steps have been completed."}`
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
return {
|
|
3723
|
+
handled: true,
|
|
3724
|
+
contextInjection: `## Step Skipped
|
|
3725
|
+
Skipped step "${currentStepId}".`
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
function handleAbort(directory, instance) {
|
|
3729
|
+
const name = instance.definition_name;
|
|
3730
|
+
const goal = instance.goal;
|
|
3731
|
+
abortWorkflow(directory);
|
|
3732
|
+
return {
|
|
3733
|
+
handled: true,
|
|
3734
|
+
contextInjection: `## Workflow Aborted
|
|
3735
|
+
Workflow "${name}" has been cancelled.
|
|
3736
|
+
Goal: "${goal}"
|
|
3737
|
+
|
|
3738
|
+
The workflow instance has been terminated and the active pointer cleared.`
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
function handleStatus(directory, instance) {
|
|
3742
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3743
|
+
let stepsDisplay = "";
|
|
3744
|
+
if (definition) {
|
|
3745
|
+
const lines = [];
|
|
3746
|
+
for (const stepDef of definition.steps) {
|
|
3747
|
+
const stepState = instance.steps[stepDef.id];
|
|
3748
|
+
if (!stepState)
|
|
3749
|
+
continue;
|
|
3750
|
+
if (stepState.status === "completed") {
|
|
3751
|
+
const summary = stepState.summary ? ` → ${truncate(stepState.summary, 80)}` : "";
|
|
3752
|
+
lines.push(`- [✓] ${stepDef.name}${summary}`);
|
|
3753
|
+
} else if (stepState.status === "active") {
|
|
3754
|
+
lines.push(`- [→] ${stepDef.name} (active)`);
|
|
3755
|
+
} else if (stepState.status === "skipped") {
|
|
3756
|
+
lines.push(`- [⊘] ${stepDef.name} (skipped)`);
|
|
3757
|
+
} else {
|
|
3758
|
+
lines.push(`- [ ] ${stepDef.name}`);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
stepsDisplay = lines.join(`
|
|
2971
3762
|
`);
|
|
3763
|
+
}
|
|
3764
|
+
const completedCount = Object.values(instance.steps).filter((s) => s.status === "completed").length;
|
|
3765
|
+
const totalCount = Object.keys(instance.steps).length;
|
|
3766
|
+
return {
|
|
3767
|
+
handled: true,
|
|
3768
|
+
contextInjection: `## Workflow Status: ${instance.definition_name}
|
|
3769
|
+
**Goal**: "${instance.goal}"
|
|
3770
|
+
**Instance**: ${instance.instance_id}
|
|
3771
|
+
**Status**: ${instance.status}
|
|
3772
|
+
**Progress**: ${completedCount}/${totalCount} steps
|
|
3773
|
+
|
|
3774
|
+
### Steps
|
|
3775
|
+
${stepsDisplay}`
|
|
3776
|
+
};
|
|
2972
3777
|
}
|
|
2973
|
-
function
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
return
|
|
2977
|
-
|
|
2978
|
-
|
|
3778
|
+
function truncate(text, maxLength) {
|
|
3779
|
+
if (text.length <= maxLength)
|
|
3780
|
+
return text;
|
|
3781
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
3782
|
+
}
|
|
3783
|
+
// src/hooks/start-work-hook.ts
|
|
3784
|
+
function handleStartWork(input) {
|
|
3785
|
+
const { promptText, sessionId, directory } = input;
|
|
3786
|
+
if (!promptText.includes("<session-context>")) {
|
|
3787
|
+
return { contextInjection: null, switchAgent: null };
|
|
3788
|
+
}
|
|
3789
|
+
const workflowWarning = checkWorkflowActive(directory);
|
|
3790
|
+
if (workflowWarning) {
|
|
3791
|
+
return { contextInjection: workflowWarning, switchAgent: null };
|
|
3792
|
+
}
|
|
3793
|
+
const explicitPlanName = extractPlanName(promptText);
|
|
3794
|
+
const existingState = readWorkState(directory);
|
|
3795
|
+
const allPlans = findPlans(directory);
|
|
3796
|
+
if (explicitPlanName) {
|
|
3797
|
+
return handleExplicitPlan(explicitPlanName, allPlans, sessionId, directory);
|
|
3798
|
+
}
|
|
3799
|
+
if (existingState) {
|
|
3800
|
+
const progress = getPlanProgress(existingState.active_plan);
|
|
3801
|
+
if (!progress.isComplete) {
|
|
3802
|
+
const validation = validatePlan(existingState.active_plan, directory);
|
|
3803
|
+
if (!validation.valid) {
|
|
3804
|
+
clearWorkState(directory);
|
|
3805
|
+
return {
|
|
3806
|
+
switchAgent: "tapestry",
|
|
3807
|
+
contextInjection: `## Plan Validation Failed
|
|
3808
|
+
The active plan "${existingState.plan_name}" has structural issues. Work state has been cleared.
|
|
2979
3809
|
|
|
2980
|
-
|
|
3810
|
+
${formatValidationResults(validation)}
|
|
2981
3811
|
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3812
|
+
Tell the user to fix the plan file and run /start-work again.`
|
|
3813
|
+
};
|
|
3814
|
+
}
|
|
3815
|
+
appendSessionId(directory, sessionId);
|
|
3816
|
+
resumeWork(directory);
|
|
3817
|
+
const resumeContext = buildResumeContext(existingState.active_plan, existingState.plan_name, progress, existingState.start_sha);
|
|
3818
|
+
if (validation.warnings.length > 0) {
|
|
3819
|
+
return {
|
|
3820
|
+
switchAgent: "tapestry",
|
|
3821
|
+
contextInjection: `${resumeContext}
|
|
3822
|
+
|
|
3823
|
+
### Validation Warnings
|
|
3824
|
+
${formatValidationResults(validation)}`
|
|
3825
|
+
};
|
|
3826
|
+
}
|
|
3827
|
+
return {
|
|
3828
|
+
switchAgent: "tapestry",
|
|
3829
|
+
contextInjection: resumeContext
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
return handlePlanDiscovery(allPlans, sessionId, directory);
|
|
2988
3834
|
}
|
|
2989
|
-
function
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
3835
|
+
function checkWorkflowActive(directory) {
|
|
3836
|
+
const instance = getActiveWorkflowInstance(directory);
|
|
3837
|
+
if (!instance)
|
|
3838
|
+
return null;
|
|
3839
|
+
if (instance.status !== "running" && instance.status !== "paused")
|
|
3840
|
+
return null;
|
|
3841
|
+
const definition = loadWorkflowDefinition(instance.definition_path);
|
|
3842
|
+
const totalSteps = definition ? definition.steps.length : 0;
|
|
3843
|
+
const completedSteps = Object.values(instance.steps).filter((s) => s.status === "completed").length;
|
|
3844
|
+
const status = instance.status === "paused" ? "paused" : "running";
|
|
3845
|
+
return `## Active Workflow Detected
|
|
2997
3846
|
|
|
2998
|
-
|
|
3847
|
+
There is currently an active workflow: "${instance.definition_name}" (${instance.instance_id})
|
|
3848
|
+
Goal: "${instance.goal}"
|
|
3849
|
+
Status: ${status} • Progress: ${completedSteps}/${totalSteps} steps complete
|
|
2999
3850
|
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3851
|
+
Starting plan execution will conflict with the active workflow — both systems use the idle loop and only one can drive at a time.
|
|
3852
|
+
|
|
3853
|
+
**Options:**
|
|
3854
|
+
- **Proceed anyway** — the workflow will be paused and can be resumed with \`/run-workflow\` after the plan completes
|
|
3855
|
+
- **Abort the workflow first** — cancel the workflow, then start the plan
|
|
3856
|
+
- **Cancel** — don't start the plan, continue with the workflow`;
|
|
3857
|
+
}
|
|
3858
|
+
function extractPlanName(promptText) {
|
|
3859
|
+
const match = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i);
|
|
3860
|
+
if (!match)
|
|
3861
|
+
return null;
|
|
3862
|
+
const cleaned = match[1].trim();
|
|
3863
|
+
return cleaned || null;
|
|
3006
3864
|
}
|
|
3865
|
+
function handleExplicitPlan(requestedName, allPlans, sessionId, directory) {
|
|
3866
|
+
const matched = findPlanByName(allPlans, requestedName);
|
|
3867
|
+
if (!matched) {
|
|
3868
|
+
const incompletePlans = allPlans.filter((p) => !getPlanProgress(p).isComplete);
|
|
3869
|
+
const listing = incompletePlans.length > 0 ? incompletePlans.map((p) => ` - ${getPlanName(p)}`).join(`
|
|
3870
|
+
`) : " (none)";
|
|
3871
|
+
return {
|
|
3872
|
+
switchAgent: "tapestry",
|
|
3873
|
+
contextInjection: `## Plan Not Found
|
|
3874
|
+
No plan matching "${requestedName}" was found.
|
|
3007
3875
|
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3876
|
+
Available incomplete plans:
|
|
3877
|
+
${listing}
|
|
3878
|
+
|
|
3879
|
+
Tell the user which plans are available and ask them to specify one.`
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
const progress = getPlanProgress(matched);
|
|
3883
|
+
if (progress.isComplete) {
|
|
3884
|
+
return {
|
|
3885
|
+
switchAgent: "tapestry",
|
|
3886
|
+
contextInjection: `## Plan Already Complete
|
|
3887
|
+
The plan "${getPlanName(matched)}" has all ${progress.total} tasks completed.
|
|
3888
|
+
Tell the user this plan is already done and suggest creating a new one with Pattern.`
|
|
3889
|
+
};
|
|
3890
|
+
}
|
|
3891
|
+
const validation = validatePlan(matched, directory);
|
|
3892
|
+
if (!validation.valid) {
|
|
3893
|
+
return {
|
|
3894
|
+
switchAgent: "tapestry",
|
|
3895
|
+
contextInjection: `## Plan Validation Failed
|
|
3896
|
+
The plan "${getPlanName(matched)}" has structural issues that must be fixed before execution can begin.
|
|
3897
|
+
|
|
3898
|
+
${formatValidationResults(validation)}
|
|
3899
|
+
|
|
3900
|
+
Tell the user to fix these issues in the plan file and try again.`
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
clearWorkState(directory);
|
|
3904
|
+
const state = createWorkState(matched, sessionId, "tapestry", directory);
|
|
3905
|
+
writeWorkState(directory, state);
|
|
3906
|
+
const freshContext = buildFreshContext(matched, getPlanName(matched), progress, state.start_sha);
|
|
3907
|
+
if (validation.warnings.length > 0) {
|
|
3908
|
+
return {
|
|
3909
|
+
switchAgent: "tapestry",
|
|
3910
|
+
contextInjection: `${freshContext}
|
|
3911
|
+
|
|
3912
|
+
### Validation Warnings
|
|
3913
|
+
${formatValidationResults(validation)}`
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
return {
|
|
3917
|
+
switchAgent: "tapestry",
|
|
3918
|
+
contextInjection: freshContext
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
function handlePlanDiscovery(allPlans, sessionId, directory) {
|
|
3922
|
+
if (allPlans.length === 0) {
|
|
3923
|
+
return {
|
|
3924
|
+
switchAgent: "tapestry",
|
|
3925
|
+
contextInjection: "## No Plans Found\nNo plan files found at `.weave/plans/`.\nTell the user to switch to Pattern agent to create a work plan first."
|
|
3926
|
+
};
|
|
3927
|
+
}
|
|
3928
|
+
const incompletePlans = allPlans.filter((p) => !getPlanProgress(p).isComplete);
|
|
3929
|
+
if (incompletePlans.length === 0) {
|
|
3930
|
+
return {
|
|
3931
|
+
switchAgent: "tapestry",
|
|
3932
|
+
contextInjection: `## All Plans Complete
|
|
3933
|
+
All existing plans have been completed.
|
|
3934
|
+
Tell the user to switch to Pattern agent to create a new plan.`
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
if (incompletePlans.length === 1) {
|
|
3938
|
+
const plan = incompletePlans[0];
|
|
3939
|
+
const progress = getPlanProgress(plan);
|
|
3940
|
+
const validation = validatePlan(plan, directory);
|
|
3941
|
+
if (!validation.valid) {
|
|
3942
|
+
return {
|
|
3943
|
+
switchAgent: "tapestry",
|
|
3944
|
+
contextInjection: `## Plan Validation Failed
|
|
3945
|
+
The plan "${getPlanName(plan)}" has structural issues that must be fixed before execution can begin.
|
|
3946
|
+
|
|
3947
|
+
${formatValidationResults(validation)}
|
|
3948
|
+
|
|
3949
|
+
Tell the user to fix these issues in the plan file and try again.`
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
const state = createWorkState(plan, sessionId, "tapestry", directory);
|
|
3953
|
+
writeWorkState(directory, state);
|
|
3954
|
+
const freshContext = buildFreshContext(plan, getPlanName(plan), progress, state.start_sha);
|
|
3955
|
+
if (validation.warnings.length > 0) {
|
|
3956
|
+
return {
|
|
3957
|
+
switchAgent: "tapestry",
|
|
3958
|
+
contextInjection: `${freshContext}
|
|
3959
|
+
|
|
3960
|
+
### Validation Warnings
|
|
3961
|
+
${formatValidationResults(validation)}`
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
return {
|
|
3965
|
+
switchAgent: "tapestry",
|
|
3966
|
+
contextInjection: freshContext
|
|
3967
|
+
};
|
|
3968
|
+
}
|
|
3969
|
+
const listing = incompletePlans.map((p) => {
|
|
3970
|
+
const progress = getPlanProgress(p);
|
|
3971
|
+
return ` - **${getPlanName(p)}** (${progress.completed}/${progress.total} tasks done)`;
|
|
3972
|
+
}).join(`
|
|
3973
|
+
`);
|
|
3974
|
+
return {
|
|
3975
|
+
switchAgent: "tapestry",
|
|
3976
|
+
contextInjection: `## Multiple Plans Found
|
|
3977
|
+
There are ${incompletePlans.length} incomplete plans:
|
|
3978
|
+
${listing}
|
|
3979
|
+
|
|
3980
|
+
Ask the user which plan to work on. They can run \`/start-work [plan-name]\` to select one.`
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
function findPlanByName(plans, requestedName) {
|
|
3984
|
+
const lower = requestedName.toLowerCase();
|
|
3985
|
+
const exact = plans.find((p) => getPlanName(p).toLowerCase() === lower);
|
|
3986
|
+
if (exact)
|
|
3987
|
+
return exact;
|
|
3988
|
+
const partial = plans.find((p) => getPlanName(p).toLowerCase().includes(lower));
|
|
3989
|
+
return partial || null;
|
|
3990
|
+
}
|
|
3991
|
+
function formatValidationResults(result) {
|
|
3992
|
+
const lines = [];
|
|
3993
|
+
if (result.errors.length > 0) {
|
|
3994
|
+
lines.push("**Errors (blocking):**");
|
|
3995
|
+
for (const err of result.errors) {
|
|
3996
|
+
lines.push(`- [${err.category}] ${err.message}`);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
if (result.warnings.length > 0) {
|
|
4000
|
+
if (result.errors.length > 0)
|
|
4001
|
+
lines.push("");
|
|
4002
|
+
lines.push("**Warnings:**");
|
|
4003
|
+
for (const warn of result.warnings) {
|
|
4004
|
+
lines.push(`- [${warn.category}] ${warn.message}`);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return lines.join(`
|
|
4008
|
+
`);
|
|
4009
|
+
}
|
|
4010
|
+
function buildFreshContext(planPath, planName, progress, startSha) {
|
|
4011
|
+
const shaLine = startSha ? `
|
|
4012
|
+
**Start SHA**: ${startSha}` : "";
|
|
4013
|
+
return `## Starting Plan: ${planName}
|
|
4014
|
+
**Plan file**: ${planPath}
|
|
4015
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed${shaLine}
|
|
4016
|
+
|
|
4017
|
+
Read the plan file now and begin executing from the first unchecked \`- [ ]\` task.
|
|
4018
|
+
|
|
4019
|
+
**SIDEBAR TODOS — DO THIS FIRST:**
|
|
4020
|
+
Before starting any work, use todowrite to populate the sidebar:
|
|
4021
|
+
1. Create a summary todo (in_progress): "${planName} ${progress.completed}/${progress.total}"
|
|
4022
|
+
2. Create a todo for the first unchecked task (in_progress)
|
|
4023
|
+
3. Create todos for the next 2-3 tasks (pending)
|
|
4024
|
+
Keep each todo under 35 chars. Update as you complete tasks.`;
|
|
4025
|
+
}
|
|
4026
|
+
function buildResumeContext(planPath, planName, progress, startSha) {
|
|
4027
|
+
const remaining = progress.total - progress.completed;
|
|
4028
|
+
const shaLine = startSha ? `
|
|
4029
|
+
**Start SHA**: ${startSha}` : "";
|
|
4030
|
+
return `## Resuming Plan: ${planName}
|
|
4031
|
+
**Plan file**: ${planPath}
|
|
4032
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed
|
|
4033
|
+
**Status**: RESUMING — continuing from where the previous session left off.${shaLine}
|
|
4034
|
+
|
|
4035
|
+
Read the plan file now and continue from the first unchecked \`- [ ]\` task.
|
|
4036
|
+
|
|
4037
|
+
**SIDEBAR TODOS — RESTORE STATE:**
|
|
4038
|
+
Previous session's todos are lost. Use todowrite to restore the sidebar:
|
|
4039
|
+
1. Create a summary todo (in_progress): "${planName} ${progress.completed}/${progress.total}"
|
|
4040
|
+
2. Create a todo for the next unchecked task (in_progress)
|
|
4041
|
+
3. Create todos for the following 2-3 tasks (pending)
|
|
4042
|
+
Keep each todo under 35 chars. ${remaining} task${remaining !== 1 ? "s" : ""} remaining.`;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
// src/hooks/work-continuation.ts
|
|
4046
|
+
var CONTINUATION_MARKER = "<!-- weave:continuation -->";
|
|
4047
|
+
var MAX_STALE_CONTINUATIONS = 3;
|
|
4048
|
+
function checkContinuation(input) {
|
|
4049
|
+
const { directory } = input;
|
|
3013
4050
|
const state = readWorkState(directory);
|
|
3014
4051
|
if (!state) {
|
|
3015
4052
|
return { continuationPrompt: null };
|
|
@@ -3017,123 +4054,661 @@ function checkContinuation(input) {
|
|
|
3017
4054
|
if (state.paused) {
|
|
3018
4055
|
return { continuationPrompt: null };
|
|
3019
4056
|
}
|
|
3020
|
-
if (state.session_ids.length > 0 && !state.session_ids.includes(input.sessionId)) {
|
|
3021
|
-
return { continuationPrompt: null };
|
|
4057
|
+
if (state.session_ids.length > 0 && !state.session_ids.includes(input.sessionId)) {
|
|
4058
|
+
return { continuationPrompt: null };
|
|
4059
|
+
}
|
|
4060
|
+
const progress = getPlanProgress(state.active_plan);
|
|
4061
|
+
if (progress.isComplete) {
|
|
4062
|
+
return { continuationPrompt: null };
|
|
4063
|
+
}
|
|
4064
|
+
if (state.continuation_completed_snapshot === undefined) {
|
|
4065
|
+
state.continuation_completed_snapshot = progress.completed;
|
|
4066
|
+
state.stale_continuation_count = 0;
|
|
4067
|
+
writeWorkState(directory, state);
|
|
4068
|
+
} else if (progress.completed > state.continuation_completed_snapshot) {
|
|
4069
|
+
state.continuation_completed_snapshot = progress.completed;
|
|
4070
|
+
state.stale_continuation_count = 0;
|
|
4071
|
+
writeWorkState(directory, state);
|
|
4072
|
+
} else {
|
|
4073
|
+
state.stale_continuation_count = (state.stale_continuation_count ?? 0) + 1;
|
|
4074
|
+
if (state.stale_continuation_count >= MAX_STALE_CONTINUATIONS) {
|
|
4075
|
+
state.paused = true;
|
|
4076
|
+
writeWorkState(directory, state);
|
|
4077
|
+
return { continuationPrompt: null };
|
|
4078
|
+
}
|
|
4079
|
+
writeWorkState(directory, state);
|
|
4080
|
+
}
|
|
4081
|
+
const remaining = progress.total - progress.completed;
|
|
4082
|
+
return {
|
|
4083
|
+
continuationPrompt: `${CONTINUATION_MARKER}
|
|
4084
|
+
You have an active work plan with incomplete tasks. Continue working.
|
|
4085
|
+
|
|
4086
|
+
**Plan**: ${state.plan_name}
|
|
4087
|
+
**File**: ${state.active_plan}
|
|
4088
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed (${remaining} remaining)
|
|
4089
|
+
|
|
4090
|
+
1. Read the plan file NOW to check exact current progress
|
|
4091
|
+
2. Use todowrite to restore sidebar: summary todo "${state.plan_name} ${progress.completed}/${progress.total}" (in_progress) + next task (in_progress) + 2-3 upcoming (pending). Max 35 chars each.
|
|
4092
|
+
3. Find the first unchecked \`- [ ]\` task
|
|
4093
|
+
4. Execute it, verify it, mark \`- [ ]\` → \`- [x]\`
|
|
4094
|
+
5. Update sidebar todos as you complete tasks
|
|
4095
|
+
6. Do not stop until all tasks are complete`
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// src/hooks/verification-reminder.ts
|
|
4100
|
+
function buildVerificationReminder(input) {
|
|
4101
|
+
const planContext = input.planName && input.progress ? `
|
|
4102
|
+
**Plan**: ${input.planName} (${input.progress.completed}/${input.progress.total} tasks done)` : "";
|
|
4103
|
+
return {
|
|
4104
|
+
verificationPrompt: `## Verification Required
|
|
4105
|
+
${planContext}
|
|
4106
|
+
|
|
4107
|
+
Before marking this task complete, verify the work:
|
|
4108
|
+
|
|
4109
|
+
1. **Read the changes**: \`git diff --stat\` then Read each changed file
|
|
4110
|
+
2. **Run checks**: Run relevant tests, check for linting/type errors
|
|
4111
|
+
3. **Validate behavior**: Does the code actually do what was requested?
|
|
4112
|
+
4. **Gate decision**: Can you explain what every changed line does?
|
|
4113
|
+
|
|
4114
|
+
If uncertain about quality, delegate to \`weft\` agent for a formal review:
|
|
4115
|
+
\`call_weave_agent(agent="weft", prompt="Review the changes for [task description]")\`
|
|
4116
|
+
|
|
4117
|
+
MANDATORY: If changes touch auth, crypto, certificates, tokens, signatures, or input validation, you MUST delegate to \`warp\` agent for a security audit — this is NOT optional:
|
|
4118
|
+
\`call_weave_agent(agent="warp", prompt="Security audit the changes for [task description]")\`
|
|
4119
|
+
|
|
4120
|
+
Only mark complete when ALL checks pass.`
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
|
|
4124
|
+
// src/hooks/create-hooks.ts
|
|
4125
|
+
function createHooks(args) {
|
|
4126
|
+
const { pluginConfig, isHookEnabled, directory, analyticsEnabled = false } = args;
|
|
4127
|
+
const writeGuardState = createWriteGuardState();
|
|
4128
|
+
const writeGuard = createWriteGuard(writeGuardState);
|
|
4129
|
+
const contextWindowThresholds = {
|
|
4130
|
+
warningPct: pluginConfig.experimental?.context_window_warning_threshold ?? 0.8,
|
|
4131
|
+
criticalPct: pluginConfig.experimental?.context_window_critical_threshold ?? 0.95
|
|
4132
|
+
};
|
|
4133
|
+
return {
|
|
4134
|
+
checkContextWindow: isHookEnabled("context-window-monitor") ? (state) => checkContextWindow(state, contextWindowThresholds) : null,
|
|
4135
|
+
writeGuard: isHookEnabled("write-existing-file-guard") ? writeGuard : null,
|
|
4136
|
+
shouldInjectRules: isHookEnabled("rules-injector") ? shouldInjectRules : null,
|
|
4137
|
+
getRulesForFile: isHookEnabled("rules-injector") ? getRulesForFile : null,
|
|
4138
|
+
firstMessageVariant: isHookEnabled("first-message-variant") ? { shouldApplyVariant, markApplied, markSessionCreated, clearSession } : null,
|
|
4139
|
+
processMessageForKeywords: isHookEnabled("keyword-detector") ? processMessageForKeywords : null,
|
|
4140
|
+
patternMdOnly: isHookEnabled("pattern-md-only") ? checkPatternWrite : null,
|
|
4141
|
+
startWork: isHookEnabled("start-work") ? (promptText, sessionId) => handleStartWork({ promptText, sessionId, directory }) : null,
|
|
4142
|
+
workContinuation: isHookEnabled("work-continuation") ? (sessionId) => checkContinuation({ sessionId, directory }) : null,
|
|
4143
|
+
workflowStart: isHookEnabled("workflow") ? (promptText, sessionId) => handleRunWorkflow({ promptText, sessionId, directory }) : null,
|
|
4144
|
+
workflowContinuation: isHookEnabled("workflow") ? (sessionId, lastAssistantMessage, lastUserMessage) => checkWorkflowContinuation({ sessionId, directory, lastAssistantMessage, lastUserMessage }) : null,
|
|
4145
|
+
workflowCommand: isHookEnabled("workflow") ? (message) => handleWorkflowCommand(message, directory) : null,
|
|
4146
|
+
verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null,
|
|
4147
|
+
analyticsEnabled
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
// src/hooks/session-token-state.ts
|
|
4151
|
+
var sessionMap = new Map;
|
|
4152
|
+
function setContextLimit(sessionId, maxTokens) {
|
|
4153
|
+
const existing = sessionMap.get(sessionId);
|
|
4154
|
+
sessionMap.set(sessionId, {
|
|
4155
|
+
usedTokens: existing?.usedTokens ?? 0,
|
|
4156
|
+
maxTokens
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
function updateUsage(sessionId, inputTokens) {
|
|
4160
|
+
if (inputTokens <= 0)
|
|
4161
|
+
return;
|
|
4162
|
+
const existing = sessionMap.get(sessionId);
|
|
4163
|
+
sessionMap.set(sessionId, {
|
|
4164
|
+
maxTokens: existing?.maxTokens ?? 0,
|
|
4165
|
+
usedTokens: inputTokens
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
function getState(sessionId) {
|
|
4169
|
+
return sessionMap.get(sessionId);
|
|
4170
|
+
}
|
|
4171
|
+
function clearSession2(sessionId) {
|
|
4172
|
+
sessionMap.delete(sessionId);
|
|
4173
|
+
}
|
|
4174
|
+
// src/features/analytics/storage.ts
|
|
4175
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync4, appendFileSync as appendFileSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync3, statSync as statSync2 } from "fs";
|
|
4176
|
+
import { join as join10 } from "path";
|
|
4177
|
+
|
|
4178
|
+
// src/features/analytics/types.ts
|
|
4179
|
+
var ANALYTICS_DIR = ".weave/analytics";
|
|
4180
|
+
var SESSION_SUMMARIES_FILE = "session-summaries.jsonl";
|
|
4181
|
+
var FINGERPRINT_FILE = "fingerprint.json";
|
|
4182
|
+
var METRICS_REPORTS_FILE = "metrics-reports.jsonl";
|
|
4183
|
+
var MAX_METRICS_ENTRIES = 100;
|
|
4184
|
+
function zeroTokenUsage() {
|
|
4185
|
+
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 };
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
// src/features/analytics/storage.ts
|
|
4189
|
+
var MAX_SESSION_ENTRIES = 1000;
|
|
4190
|
+
function ensureAnalyticsDir(directory) {
|
|
4191
|
+
const dir = join10(directory, ANALYTICS_DIR);
|
|
4192
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
4193
|
+
return dir;
|
|
4194
|
+
}
|
|
4195
|
+
function appendSessionSummary(directory, summary) {
|
|
4196
|
+
try {
|
|
4197
|
+
const dir = ensureAnalyticsDir(directory);
|
|
4198
|
+
const filePath = join10(dir, SESSION_SUMMARIES_FILE);
|
|
4199
|
+
const line = JSON.stringify(summary) + `
|
|
4200
|
+
`;
|
|
4201
|
+
appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
|
|
4202
|
+
try {
|
|
4203
|
+
const TYPICAL_ENTRY_BYTES = 200;
|
|
4204
|
+
const rotationSizeThreshold = MAX_SESSION_ENTRIES * TYPICAL_ENTRY_BYTES * 0.9;
|
|
4205
|
+
const { size } = statSync2(filePath);
|
|
4206
|
+
if (size > rotationSizeThreshold) {
|
|
4207
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4208
|
+
const lines = content.split(`
|
|
4209
|
+
`).filter((l) => l.trim().length > 0);
|
|
4210
|
+
if (lines.length > MAX_SESSION_ENTRIES) {
|
|
4211
|
+
const trimmed = lines.slice(-MAX_SESSION_ENTRIES).join(`
|
|
4212
|
+
`) + `
|
|
4213
|
+
`;
|
|
4214
|
+
writeFileSync3(filePath, trimmed, { encoding: "utf-8", mode: 384 });
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
} catch {}
|
|
4218
|
+
return true;
|
|
4219
|
+
} catch {
|
|
4220
|
+
return false;
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
function readSessionSummaries(directory) {
|
|
4224
|
+
const filePath = join10(directory, ANALYTICS_DIR, SESSION_SUMMARIES_FILE);
|
|
4225
|
+
try {
|
|
4226
|
+
if (!existsSync12(filePath))
|
|
4227
|
+
return [];
|
|
4228
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4229
|
+
const lines = content.split(`
|
|
4230
|
+
`).filter((line) => line.trim().length > 0);
|
|
4231
|
+
const summaries = [];
|
|
4232
|
+
for (const line of lines) {
|
|
4233
|
+
try {
|
|
4234
|
+
summaries.push(JSON.parse(line));
|
|
4235
|
+
} catch {}
|
|
4236
|
+
}
|
|
4237
|
+
return summaries;
|
|
4238
|
+
} catch {
|
|
4239
|
+
return [];
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
function writeFingerprint(directory, fingerprint) {
|
|
4243
|
+
try {
|
|
4244
|
+
const dir = ensureAnalyticsDir(directory);
|
|
4245
|
+
const filePath = join10(dir, FINGERPRINT_FILE);
|
|
4246
|
+
writeFileSync3(filePath, JSON.stringify(fingerprint, null, 2), { encoding: "utf-8", mode: 384 });
|
|
4247
|
+
return true;
|
|
4248
|
+
} catch {
|
|
4249
|
+
return false;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
function readFingerprint(directory) {
|
|
4253
|
+
const filePath = join10(directory, ANALYTICS_DIR, FINGERPRINT_FILE);
|
|
4254
|
+
try {
|
|
4255
|
+
if (!existsSync12(filePath))
|
|
4256
|
+
return null;
|
|
4257
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4258
|
+
const parsed = JSON.parse(content);
|
|
4259
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.stack))
|
|
4260
|
+
return null;
|
|
4261
|
+
return parsed;
|
|
4262
|
+
} catch {
|
|
4263
|
+
return null;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
function writeMetricsReport(directory, report) {
|
|
4267
|
+
try {
|
|
4268
|
+
const dir = ensureAnalyticsDir(directory);
|
|
4269
|
+
const filePath = join10(dir, METRICS_REPORTS_FILE);
|
|
4270
|
+
const line = JSON.stringify(report) + `
|
|
4271
|
+
`;
|
|
4272
|
+
appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
|
|
4273
|
+
try {
|
|
4274
|
+
const TYPICAL_ENTRY_BYTES = 200;
|
|
4275
|
+
const rotationSizeThreshold = MAX_METRICS_ENTRIES * TYPICAL_ENTRY_BYTES * 0.9;
|
|
4276
|
+
const { size } = statSync2(filePath);
|
|
4277
|
+
if (size > rotationSizeThreshold) {
|
|
4278
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4279
|
+
const lines = content.split(`
|
|
4280
|
+
`).filter((l) => l.trim().length > 0);
|
|
4281
|
+
if (lines.length > MAX_METRICS_ENTRIES) {
|
|
4282
|
+
const trimmed = lines.slice(-MAX_METRICS_ENTRIES).join(`
|
|
4283
|
+
`) + `
|
|
4284
|
+
`;
|
|
4285
|
+
writeFileSync3(filePath, trimmed, { encoding: "utf-8", mode: 384 });
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
} catch {}
|
|
4289
|
+
return true;
|
|
4290
|
+
} catch {
|
|
4291
|
+
return false;
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
function readMetricsReports(directory) {
|
|
4295
|
+
const filePath = join10(directory, ANALYTICS_DIR, METRICS_REPORTS_FILE);
|
|
4296
|
+
try {
|
|
4297
|
+
if (!existsSync12(filePath))
|
|
4298
|
+
return [];
|
|
4299
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
4300
|
+
const lines = content.split(`
|
|
4301
|
+
`).filter((line) => line.trim().length > 0);
|
|
4302
|
+
const reports = [];
|
|
4303
|
+
for (const line of lines) {
|
|
4304
|
+
try {
|
|
4305
|
+
reports.push(JSON.parse(line));
|
|
4306
|
+
} catch {}
|
|
4307
|
+
}
|
|
4308
|
+
return reports;
|
|
4309
|
+
} catch {
|
|
4310
|
+
return [];
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// src/features/analytics/token-report.ts
|
|
4315
|
+
function generateTokenReport(summaries) {
|
|
4316
|
+
if (summaries.length === 0) {
|
|
4317
|
+
return "No session data available.";
|
|
4318
|
+
}
|
|
4319
|
+
const sections = [];
|
|
4320
|
+
const totalSessions = summaries.length;
|
|
4321
|
+
const totalMessages = summaries.reduce((sum, s) => sum + (s.tokenUsage?.totalMessages ?? 0), 0);
|
|
4322
|
+
const totalInput = summaries.reduce((sum, s) => sum + (s.tokenUsage?.inputTokens ?? 0), 0);
|
|
4323
|
+
const totalOutput = summaries.reduce((sum, s) => sum + (s.tokenUsage?.outputTokens ?? 0), 0);
|
|
4324
|
+
const totalReasoning = summaries.reduce((sum, s) => sum + (s.tokenUsage?.reasoningTokens ?? 0), 0);
|
|
4325
|
+
const totalCacheRead = summaries.reduce((sum, s) => sum + (s.tokenUsage?.cacheReadTokens ?? 0), 0);
|
|
4326
|
+
const totalCacheWrite = summaries.reduce((sum, s) => sum + (s.tokenUsage?.cacheWriteTokens ?? 0), 0);
|
|
4327
|
+
const totalCost = summaries.reduce((sum, s) => sum + (s.totalCost ?? 0), 0);
|
|
4328
|
+
sections.push(`## Overall Totals
|
|
4329
|
+
` + `- Sessions: ${fmt(totalSessions)}
|
|
4330
|
+
` + `- Messages: ${fmt(totalMessages)}
|
|
4331
|
+
` + `- Input tokens: ${fmt(totalInput)}
|
|
4332
|
+
` + `- Output tokens: ${fmt(totalOutput)}
|
|
4333
|
+
` + `- Reasoning tokens: ${fmt(totalReasoning)}
|
|
4334
|
+
` + `- Cache read tokens: ${fmt(totalCacheRead)}
|
|
4335
|
+
` + `- Cache write tokens: ${fmt(totalCacheWrite)}
|
|
4336
|
+
` + `- Total cost: ${fmtCost(totalCost)}`);
|
|
4337
|
+
const agentGroups = new Map;
|
|
4338
|
+
for (const s of summaries) {
|
|
4339
|
+
const key = s.agentName ?? "(unknown)";
|
|
4340
|
+
const group = agentGroups.get(key);
|
|
4341
|
+
if (group) {
|
|
4342
|
+
group.push(s);
|
|
4343
|
+
} else {
|
|
4344
|
+
agentGroups.set(key, [s]);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
const agentStats = Array.from(agentGroups.entries()).map(([agent, sessions]) => {
|
|
4348
|
+
const agentCost = sessions.reduce((sum, s) => sum + (s.totalCost ?? 0), 0);
|
|
4349
|
+
const agentTokens = sessions.reduce((sum, s) => sum + (s.tokenUsage?.inputTokens ?? 0) + (s.tokenUsage?.outputTokens ?? 0) + (s.tokenUsage?.reasoningTokens ?? 0), 0);
|
|
4350
|
+
const avgTokens = sessions.length > 0 ? Math.round(agentTokens / sessions.length) : 0;
|
|
4351
|
+
const avgCost = sessions.length > 0 ? agentCost / sessions.length : 0;
|
|
4352
|
+
return { agent, sessions: sessions.length, avgTokens, avgCost, totalCost: agentCost };
|
|
4353
|
+
}).sort((a, b) => b.totalCost - a.totalCost);
|
|
4354
|
+
const agentLines = agentStats.map((a) => `- **${a.agent}**: ${fmt(a.sessions)} session${a.sessions === 1 ? "" : "s"}, ` + `avg ${fmt(a.avgTokens)} tokens/session, ` + `avg ${fmtCost(a.avgCost)}/session, ` + `total ${fmtCost(a.totalCost)}`);
|
|
4355
|
+
sections.push(`## Per-Agent Breakdown
|
|
4356
|
+
${agentLines.join(`
|
|
4357
|
+
`)}`);
|
|
4358
|
+
const top5 = [...summaries].sort((a, b) => (b.totalCost ?? 0) - (a.totalCost ?? 0)).slice(0, 5);
|
|
4359
|
+
const top5Lines = top5.map((s) => {
|
|
4360
|
+
const id = s.sessionId.length > 8 ? s.sessionId.slice(0, 8) : s.sessionId;
|
|
4361
|
+
const agent = s.agentName ?? "(unknown)";
|
|
4362
|
+
const cost = fmtCost(s.totalCost ?? 0);
|
|
4363
|
+
const tokens = (s.tokenUsage?.inputTokens ?? 0) + (s.tokenUsage?.outputTokens ?? 0) + (s.tokenUsage?.reasoningTokens ?? 0);
|
|
4364
|
+
const duration = fmtDuration(s.durationMs);
|
|
4365
|
+
return `- \`${id}\` ${agent} — ${cost}, ${fmt(tokens)} tokens, ${duration}`;
|
|
4366
|
+
});
|
|
4367
|
+
sections.push(`## Top 5 Costliest Sessions
|
|
4368
|
+
${top5Lines.join(`
|
|
4369
|
+
`)}`);
|
|
4370
|
+
return sections.join(`
|
|
4371
|
+
|
|
4372
|
+
`);
|
|
4373
|
+
}
|
|
4374
|
+
function fmt(n) {
|
|
4375
|
+
return n.toLocaleString("en-US");
|
|
4376
|
+
}
|
|
4377
|
+
function fmtCost(n) {
|
|
4378
|
+
return `$${n.toFixed(2)}`;
|
|
4379
|
+
}
|
|
4380
|
+
function fmtDuration(ms) {
|
|
4381
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
4382
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
4383
|
+
const seconds = totalSeconds % 60;
|
|
4384
|
+
if (minutes === 0)
|
|
4385
|
+
return `${seconds}s`;
|
|
4386
|
+
return `${minutes}m ${seconds}s`;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
// src/features/analytics/format-metrics.ts
|
|
4390
|
+
function formatNumber(n) {
|
|
4391
|
+
return n.toLocaleString("en-US");
|
|
4392
|
+
}
|
|
4393
|
+
function formatDuration(ms) {
|
|
4394
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
4395
|
+
if (totalSeconds < 60)
|
|
4396
|
+
return `${totalSeconds}s`;
|
|
4397
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
4398
|
+
const seconds = totalSeconds % 60;
|
|
4399
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
4400
|
+
}
|
|
4401
|
+
function formatDate(iso) {
|
|
4402
|
+
try {
|
|
4403
|
+
const d = new Date(iso);
|
|
4404
|
+
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
4405
|
+
} catch {
|
|
4406
|
+
return iso;
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
function formatReport(report) {
|
|
4410
|
+
const lines = [];
|
|
4411
|
+
const date = formatDate(report.generatedAt);
|
|
4412
|
+
lines.push(`#### \uD83D\uDCCA ${report.planName} (${date})`);
|
|
4413
|
+
lines.push("");
|
|
4414
|
+
lines.push("| Metric | Value |");
|
|
4415
|
+
lines.push("|--------|-------|");
|
|
4416
|
+
lines.push(`| Coverage | ${Math.round(report.adherence.coverage * 100)}% |`);
|
|
4417
|
+
lines.push(`| Precision | ${Math.round(report.adherence.precision * 100)}% |`);
|
|
4418
|
+
lines.push(`| Sessions | ${report.sessionCount} |`);
|
|
4419
|
+
lines.push(`| Duration | ${formatDuration(report.durationMs)} |`);
|
|
4420
|
+
lines.push(`| Input Tokens | ${formatNumber(report.tokenUsage.input)} |`);
|
|
4421
|
+
lines.push(`| Output Tokens | ${formatNumber(report.tokenUsage.output)} |`);
|
|
4422
|
+
if (report.tokenUsage.reasoning > 0) {
|
|
4423
|
+
lines.push(`| Reasoning Tokens | ${formatNumber(report.tokenUsage.reasoning)} |`);
|
|
4424
|
+
}
|
|
4425
|
+
if (report.tokenUsage.cacheRead > 0 || report.tokenUsage.cacheWrite > 0) {
|
|
4426
|
+
lines.push(`| Cache Read | ${formatNumber(report.tokenUsage.cacheRead)} |`);
|
|
4427
|
+
lines.push(`| Cache Write | ${formatNumber(report.tokenUsage.cacheWrite)} |`);
|
|
4428
|
+
}
|
|
4429
|
+
if (report.adherence.unplannedChanges.length > 0) {
|
|
4430
|
+
lines.push("");
|
|
4431
|
+
lines.push(`**Unplanned Changes**: ${report.adherence.unplannedChanges.map((f) => `\`${f}\``).join(", ")}`);
|
|
4432
|
+
}
|
|
4433
|
+
if (report.adherence.missedFiles.length > 0) {
|
|
4434
|
+
lines.push("");
|
|
4435
|
+
lines.push(`**Missed Files**: ${report.adherence.missedFiles.map((f) => `\`${f}\``).join(", ")}`);
|
|
4436
|
+
}
|
|
4437
|
+
return lines.join(`
|
|
4438
|
+
`);
|
|
4439
|
+
}
|
|
4440
|
+
function aggregateSessionTokens(summaries) {
|
|
4441
|
+
const total = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 };
|
|
4442
|
+
for (const s of summaries) {
|
|
4443
|
+
if (s.tokenUsage) {
|
|
4444
|
+
total.input += s.tokenUsage.inputTokens;
|
|
4445
|
+
total.output += s.tokenUsage.outputTokens;
|
|
4446
|
+
total.reasoning += s.tokenUsage.reasoningTokens;
|
|
4447
|
+
total.cacheRead += s.tokenUsage.cacheReadTokens;
|
|
4448
|
+
total.cacheWrite += s.tokenUsage.cacheWriteTokens;
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4451
|
+
return total;
|
|
4452
|
+
}
|
|
4453
|
+
function topTools(summaries, limit = 5) {
|
|
4454
|
+
const counts = {};
|
|
4455
|
+
for (const s of summaries) {
|
|
4456
|
+
for (const t of s.toolUsage) {
|
|
4457
|
+
counts[t.tool] = (counts[t.tool] ?? 0) + t.count;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
return Object.entries(counts).map(([tool, count]) => ({ tool, count })).sort((a, b) => b.count - a.count).slice(0, limit);
|
|
4461
|
+
}
|
|
4462
|
+
function formatMetricsMarkdown(reports, summaries, args) {
|
|
4463
|
+
if (reports.length === 0 && summaries.length === 0) {
|
|
4464
|
+
return [
|
|
4465
|
+
"## Weave Metrics Dashboard",
|
|
4466
|
+
"",
|
|
4467
|
+
"No metrics data yet.",
|
|
4468
|
+
"",
|
|
4469
|
+
"To generate metrics:",
|
|
4470
|
+
'1. Enable analytics in `weave.json`: `{ "analytics": { "enabled": true } }`',
|
|
4471
|
+
"2. Create and complete a plan using Pattern and `/start-work`",
|
|
4472
|
+
"3. Metrics are generated automatically when a plan completes"
|
|
4473
|
+
].join(`
|
|
4474
|
+
`);
|
|
4475
|
+
}
|
|
4476
|
+
const lines = ["## Weave Metrics Dashboard"];
|
|
4477
|
+
let filteredReports = reports;
|
|
4478
|
+
const trimmedArgs = args?.trim() ?? "";
|
|
4479
|
+
if (trimmedArgs && trimmedArgs !== "all") {
|
|
4480
|
+
filteredReports = reports.filter((r) => r.planName.toLowerCase().includes(trimmedArgs.toLowerCase()));
|
|
4481
|
+
}
|
|
4482
|
+
if (!trimmedArgs || trimmedArgs === "") {
|
|
4483
|
+
filteredReports = filteredReports.slice(-5);
|
|
4484
|
+
}
|
|
4485
|
+
if (filteredReports.length > 0) {
|
|
4486
|
+
lines.push("");
|
|
4487
|
+
lines.push("### Recent Plan Metrics");
|
|
4488
|
+
for (let i = 0;i < filteredReports.length; i++) {
|
|
4489
|
+
lines.push("");
|
|
4490
|
+
lines.push(formatReport(filteredReports[i]));
|
|
4491
|
+
if (i < filteredReports.length - 1) {
|
|
4492
|
+
lines.push("");
|
|
4493
|
+
lines.push("---");
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
} else if (reports.length > 0 && trimmedArgs) {
|
|
4497
|
+
lines.push("");
|
|
4498
|
+
lines.push(`### Recent Plan Metrics`);
|
|
4499
|
+
lines.push("");
|
|
4500
|
+
lines.push(`No reports found matching "${trimmedArgs}".`);
|
|
4501
|
+
}
|
|
4502
|
+
if (summaries.length > 0) {
|
|
4503
|
+
lines.push("");
|
|
4504
|
+
lines.push("---");
|
|
4505
|
+
lines.push("");
|
|
4506
|
+
lines.push("### Aggregate Session Stats");
|
|
4507
|
+
lines.push("");
|
|
4508
|
+
const totalTokens = aggregateSessionTokens(summaries);
|
|
4509
|
+
const avgDuration = summaries.reduce((sum, s) => sum + s.durationMs, 0) / summaries.length;
|
|
4510
|
+
const tools = topTools(summaries);
|
|
4511
|
+
lines.push(`- **Sessions tracked**: ${summaries.length}`);
|
|
4512
|
+
lines.push(`- **Total input tokens**: ${formatNumber(totalTokens.input)}`);
|
|
4513
|
+
lines.push(`- **Total output tokens**: ${formatNumber(totalTokens.output)}`);
|
|
4514
|
+
lines.push(`- **Average session duration**: ${formatDuration(avgDuration)}`);
|
|
4515
|
+
if (tools.length > 0) {
|
|
4516
|
+
const toolStr = tools.map((t) => `${t.tool} (${formatNumber(t.count)})`).join(", ");
|
|
4517
|
+
lines.push(`- **Top tools**: ${toolStr}`);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
return lines.join(`
|
|
4521
|
+
`);
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
// src/features/analytics/plan-parser.ts
|
|
4525
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
4526
|
+
function extractSection2(content, heading) {
|
|
4527
|
+
const lines = content.split(`
|
|
4528
|
+
`);
|
|
4529
|
+
let startIdx = -1;
|
|
4530
|
+
for (let i = 0;i < lines.length; i++) {
|
|
4531
|
+
if (lines[i].trim() === heading) {
|
|
4532
|
+
startIdx = i + 1;
|
|
4533
|
+
break;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
if (startIdx === -1)
|
|
4537
|
+
return null;
|
|
4538
|
+
const sectionLines = [];
|
|
4539
|
+
for (let i = startIdx;i < lines.length; i++) {
|
|
4540
|
+
if (/^## /.test(lines[i]))
|
|
4541
|
+
break;
|
|
4542
|
+
sectionLines.push(lines[i]);
|
|
4543
|
+
}
|
|
4544
|
+
return sectionLines.join(`
|
|
4545
|
+
`);
|
|
4546
|
+
}
|
|
4547
|
+
function extractFilePath2(raw) {
|
|
4548
|
+
let cleaned = raw.replace(/^\s*(create|modify|new:|add)\s+/i, "").replace(/\(new\)/gi, "").trim();
|
|
4549
|
+
const firstToken = cleaned.split(/\s+/)[0];
|
|
4550
|
+
if (firstToken && (firstToken.includes("/") || firstToken.endsWith(".ts") || firstToken.endsWith(".js") || firstToken.endsWith(".json") || firstToken.endsWith(".md"))) {
|
|
4551
|
+
cleaned = firstToken;
|
|
4552
|
+
} else if (!cleaned.includes("/")) {
|
|
4553
|
+
return null;
|
|
4554
|
+
}
|
|
4555
|
+
return cleaned || null;
|
|
4556
|
+
}
|
|
4557
|
+
function extractPlannedFiles(planPath) {
|
|
4558
|
+
let content;
|
|
4559
|
+
try {
|
|
4560
|
+
content = readFileSync10(planPath, "utf-8");
|
|
4561
|
+
} catch {
|
|
4562
|
+
return [];
|
|
4563
|
+
}
|
|
4564
|
+
const todosSection = extractSection2(content, "## TODOs");
|
|
4565
|
+
if (todosSection === null)
|
|
4566
|
+
return [];
|
|
4567
|
+
const files = new Set;
|
|
4568
|
+
const lines = todosSection.split(`
|
|
4569
|
+
`);
|
|
4570
|
+
for (const line of lines) {
|
|
4571
|
+
const filesMatch = /^\s*\*\*Files\*\*:?\s*(.+)$/.exec(line);
|
|
4572
|
+
if (!filesMatch)
|
|
4573
|
+
continue;
|
|
4574
|
+
const rawValue = filesMatch[1].trim();
|
|
4575
|
+
const parts = rawValue.split(",");
|
|
4576
|
+
for (const part of parts) {
|
|
4577
|
+
const trimmed = part.trim();
|
|
4578
|
+
if (!trimmed)
|
|
4579
|
+
continue;
|
|
4580
|
+
const filePath = extractFilePath2(trimmed);
|
|
4581
|
+
if (filePath) {
|
|
4582
|
+
files.add(filePath);
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
return Array.from(files);
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
// src/features/analytics/git-diff.ts
|
|
4590
|
+
import { execFileSync } from "child_process";
|
|
4591
|
+
var SHA_PATTERN = /^[0-9a-f]{4,40}$/i;
|
|
4592
|
+
function getChangedFiles(directory, fromSha) {
|
|
4593
|
+
if (!SHA_PATTERN.test(fromSha)) {
|
|
4594
|
+
return [];
|
|
3022
4595
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
4596
|
+
try {
|
|
4597
|
+
const output = execFileSync("git", ["diff", "--name-only", `${fromSha}..HEAD`], {
|
|
4598
|
+
cwd: directory,
|
|
4599
|
+
encoding: "utf-8",
|
|
4600
|
+
timeout: 1e4,
|
|
4601
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4602
|
+
});
|
|
4603
|
+
return output.split(`
|
|
4604
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4605
|
+
} catch {
|
|
4606
|
+
return [];
|
|
3026
4607
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4610
|
+
// src/features/analytics/adherence.ts
|
|
4611
|
+
function normalizePath(p) {
|
|
4612
|
+
return p.trim().toLowerCase().replace(/^\.\//, "");
|
|
4613
|
+
}
|
|
4614
|
+
function calculateAdherence(plannedFiles, actualFiles) {
|
|
4615
|
+
const plannedNorm = new Set(plannedFiles.map(normalizePath));
|
|
4616
|
+
const actualNorm = new Set(actualFiles.map(normalizePath));
|
|
4617
|
+
const plannedFilesChanged = [];
|
|
4618
|
+
const missedFiles = [];
|
|
4619
|
+
const unplannedChanges = [];
|
|
4620
|
+
for (const planned of plannedFiles) {
|
|
4621
|
+
if (actualNorm.has(normalizePath(planned))) {
|
|
4622
|
+
plannedFilesChanged.push(planned);
|
|
4623
|
+
} else {
|
|
4624
|
+
missedFiles.push(planned);
|
|
3041
4625
|
}
|
|
3042
|
-
writeWorkState(directory, state);
|
|
3043
4626
|
}
|
|
3044
|
-
const
|
|
4627
|
+
for (const actual of actualFiles) {
|
|
4628
|
+
if (!plannedNorm.has(normalizePath(actual))) {
|
|
4629
|
+
unplannedChanges.push(actual);
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
const totalPlannedFiles = plannedFiles.length;
|
|
4633
|
+
const totalActualFiles = actualFiles.length;
|
|
4634
|
+
const coverage = totalPlannedFiles === 0 ? 1 : plannedFilesChanged.length / totalPlannedFiles;
|
|
4635
|
+
const precision = totalActualFiles === 0 ? 1 : plannedFilesChanged.length / totalActualFiles;
|
|
3045
4636
|
return {
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
1. Read the plan file NOW to check exact current progress
|
|
3054
|
-
2. Use todowrite to restore sidebar: summary todo "${state.plan_name} ${progress.completed}/${progress.total}" (in_progress) + next task (in_progress) + 2-3 upcoming (pending). Max 35 chars each.
|
|
3055
|
-
3. Find the first unchecked \`- [ ]\` task
|
|
3056
|
-
4. Execute it, verify it, mark \`- [ ]\` → \`- [x]\`
|
|
3057
|
-
5. Update sidebar todos as you complete tasks
|
|
3058
|
-
6. Do not stop until all tasks are complete`
|
|
4637
|
+
coverage,
|
|
4638
|
+
precision,
|
|
4639
|
+
plannedFilesChanged,
|
|
4640
|
+
unplannedChanges,
|
|
4641
|
+
missedFiles,
|
|
4642
|
+
totalPlannedFiles,
|
|
4643
|
+
totalActualFiles
|
|
3059
4644
|
};
|
|
3060
4645
|
}
|
|
3061
4646
|
|
|
3062
|
-
// src/
|
|
3063
|
-
function
|
|
3064
|
-
const
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
MANDATORY: If changes touch auth, crypto, certificates, tokens, signatures, or input validation, you MUST delegate to \`warp\` agent for a security audit — this is NOT optional:
|
|
3081
|
-
\`call_weave_agent(agent="warp", prompt="Security audit the changes for [task description]")\`
|
|
3082
|
-
|
|
3083
|
-
Only mark complete when ALL checks pass.`
|
|
3084
|
-
};
|
|
4647
|
+
// src/features/analytics/plan-token-aggregator.ts
|
|
4648
|
+
function aggregateTokensForPlan(directory, sessionIds) {
|
|
4649
|
+
const summaries = readSessionSummaries(directory);
|
|
4650
|
+
const sessionIdSet = new Set(sessionIds);
|
|
4651
|
+
const total = zeroTokenUsage();
|
|
4652
|
+
for (const summary of summaries) {
|
|
4653
|
+
if (!sessionIdSet.has(summary.sessionId))
|
|
4654
|
+
continue;
|
|
4655
|
+
if (summary.tokenUsage) {
|
|
4656
|
+
total.input += summary.tokenUsage.inputTokens;
|
|
4657
|
+
total.output += summary.tokenUsage.outputTokens;
|
|
4658
|
+
total.reasoning += summary.tokenUsage.reasoningTokens;
|
|
4659
|
+
total.cacheRead += summary.tokenUsage.cacheReadTokens;
|
|
4660
|
+
total.cacheWrite += summary.tokenUsage.cacheWriteTokens;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return total;
|
|
3085
4664
|
}
|
|
3086
4665
|
|
|
3087
|
-
// src/
|
|
3088
|
-
function
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
|
-
function getState(sessionId) {
|
|
3129
|
-
return sessionMap.get(sessionId);
|
|
3130
|
-
}
|
|
3131
|
-
function clearSession2(sessionId) {
|
|
3132
|
-
sessionMap.delete(sessionId);
|
|
4666
|
+
// src/features/analytics/generate-metrics-report.ts
|
|
4667
|
+
function generateMetricsReport(directory, state) {
|
|
4668
|
+
try {
|
|
4669
|
+
const plannedFiles = extractPlannedFiles(state.active_plan);
|
|
4670
|
+
const actualFiles = state.start_sha ? getChangedFiles(directory, state.start_sha) : [];
|
|
4671
|
+
const adherence = calculateAdherence(plannedFiles, actualFiles);
|
|
4672
|
+
const tokenUsage = aggregateTokensForPlan(directory, state.session_ids);
|
|
4673
|
+
const summaries = readSessionSummaries(directory);
|
|
4674
|
+
const matchingSummaries = summaries.filter((s) => state.session_ids.includes(s.sessionId));
|
|
4675
|
+
const durationMs = matchingSummaries.reduce((sum, s) => sum + s.durationMs, 0);
|
|
4676
|
+
const report = {
|
|
4677
|
+
planName: getPlanName(state.active_plan),
|
|
4678
|
+
generatedAt: new Date().toISOString(),
|
|
4679
|
+
adherence,
|
|
4680
|
+
quality: undefined,
|
|
4681
|
+
gaps: undefined,
|
|
4682
|
+
tokenUsage,
|
|
4683
|
+
durationMs,
|
|
4684
|
+
sessionCount: state.session_ids.length,
|
|
4685
|
+
startSha: state.start_sha,
|
|
4686
|
+
sessionIds: [...state.session_ids]
|
|
4687
|
+
};
|
|
4688
|
+
const written = writeMetricsReport(directory, report);
|
|
4689
|
+
if (!written) {
|
|
4690
|
+
log("[analytics] Failed to write metrics report (non-fatal)");
|
|
4691
|
+
return null;
|
|
4692
|
+
}
|
|
4693
|
+
log("[analytics] Metrics report generated", {
|
|
4694
|
+
plan: report.planName,
|
|
4695
|
+
coverage: adherence.coverage,
|
|
4696
|
+
precision: adherence.precision
|
|
4697
|
+
});
|
|
4698
|
+
return report;
|
|
4699
|
+
} catch (err) {
|
|
4700
|
+
log("[analytics] Failed to generate metrics report (non-fatal)", {
|
|
4701
|
+
error: String(err)
|
|
4702
|
+
});
|
|
4703
|
+
return null;
|
|
4704
|
+
}
|
|
3133
4705
|
}
|
|
4706
|
+
|
|
3134
4707
|
// src/plugin/plugin-interface.ts
|
|
3135
4708
|
function createPluginInterface(args) {
|
|
3136
4709
|
const { pluginConfig, hooks, tools, configHandler, agents, client, directory = "", tracker } = args;
|
|
4710
|
+
const lastAssistantMessageText = new Map;
|
|
4711
|
+
const lastUserMessageText = new Map;
|
|
3137
4712
|
return {
|
|
3138
4713
|
tool: tools,
|
|
3139
4714
|
config: async (config) => {
|
|
@@ -3187,13 +4762,74 @@ ${result.contextInjection}`;
|
|
|
3187
4762
|
}
|
|
3188
4763
|
}
|
|
3189
4764
|
}
|
|
4765
|
+
if (hooks.workflowStart) {
|
|
4766
|
+
const parts = _output.parts;
|
|
4767
|
+
const message = _output.message;
|
|
4768
|
+
const promptText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
4769
|
+
`).trim() ?? "";
|
|
4770
|
+
if (promptText.includes("workflow engine will inject context")) {
|
|
4771
|
+
const result = hooks.workflowStart(promptText, sessionID);
|
|
4772
|
+
if (result.switchAgent && message) {
|
|
4773
|
+
message.agent = getAgentDisplayName(result.switchAgent);
|
|
4774
|
+
}
|
|
4775
|
+
if (result.contextInjection && parts) {
|
|
4776
|
+
const idx = parts.findIndex((p) => p.type === "text" && p.text);
|
|
4777
|
+
if (idx >= 0 && parts[idx].text) {
|
|
4778
|
+
parts[idx].text += `
|
|
4779
|
+
|
|
4780
|
+
---
|
|
4781
|
+
${result.contextInjection}`;
|
|
4782
|
+
} else {
|
|
4783
|
+
parts.push({ type: "text", text: result.contextInjection });
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
{
|
|
4789
|
+
const parts = _output.parts;
|
|
4790
|
+
const userText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
4791
|
+
`).trim() ?? "";
|
|
4792
|
+
if (userText && sessionID) {
|
|
4793
|
+
lastUserMessageText.set(sessionID, userText);
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
if (hooks.workflowCommand) {
|
|
4797
|
+
const parts = _output.parts;
|
|
4798
|
+
const message = _output.message;
|
|
4799
|
+
const userText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
4800
|
+
`).trim() ?? "";
|
|
4801
|
+
if (userText) {
|
|
4802
|
+
const cmdResult = hooks.workflowCommand(userText);
|
|
4803
|
+
if (cmdResult.handled) {
|
|
4804
|
+
if (cmdResult.contextInjection && parts) {
|
|
4805
|
+
const idx = parts.findIndex((p) => p.type === "text" && p.text);
|
|
4806
|
+
if (idx >= 0 && parts[idx].text) {
|
|
4807
|
+
parts[idx].text += `
|
|
4808
|
+
|
|
4809
|
+
---
|
|
4810
|
+
${cmdResult.contextInjection}`;
|
|
4811
|
+
} else {
|
|
4812
|
+
parts.push({ type: "text", text: cmdResult.contextInjection });
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
if (cmdResult.switchAgent && message) {
|
|
4816
|
+
message.agent = getAgentDisplayName(cmdResult.switchAgent);
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
3190
4821
|
if (directory) {
|
|
3191
4822
|
const parts = _output.parts;
|
|
3192
4823
|
const promptText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
3193
4824
|
`).trim() ?? "";
|
|
3194
4825
|
const isStartWork = promptText.includes("<session-context>");
|
|
3195
4826
|
const isContinuation = promptText.includes(CONTINUATION_MARKER);
|
|
3196
|
-
|
|
4827
|
+
const isWorkflowContinuation = promptText.includes(WORKFLOW_CONTINUATION_MARKER);
|
|
4828
|
+
const isActiveWorkflow = (() => {
|
|
4829
|
+
const wf = getActiveWorkflowInstance(directory);
|
|
4830
|
+
return wf != null && wf.status === "running";
|
|
4831
|
+
})();
|
|
4832
|
+
if (!isStartWork && !isContinuation && !isWorkflowContinuation && !isActiveWorkflow) {
|
|
3197
4833
|
const state = readWorkState(directory);
|
|
3198
4834
|
if (state && !state.paused) {
|
|
3199
4835
|
pauseWork(directory);
|
|
@@ -3210,6 +4846,9 @@ ${result.contextInjection}`;
|
|
|
3210
4846
|
setContextLimit(sessionId, maxTokens);
|
|
3211
4847
|
log("[context-window] Captured context limit", { sessionId, maxTokens });
|
|
3212
4848
|
}
|
|
4849
|
+
if (tracker && hooks.analyticsEnabled && sessionId && input.agent) {
|
|
4850
|
+
tracker.setAgentName(sessionId, input.agent);
|
|
4851
|
+
}
|
|
3213
4852
|
},
|
|
3214
4853
|
"chat.headers": async (_input, _output) => {},
|
|
3215
4854
|
event: async (input) => {
|
|
@@ -3233,38 +4872,115 @@ ${result.contextInjection}`;
|
|
|
3233
4872
|
} catch (err) {
|
|
3234
4873
|
log("[analytics] Failed to end session (non-fatal)", { error: String(err) });
|
|
3235
4874
|
}
|
|
4875
|
+
if (directory) {
|
|
4876
|
+
try {
|
|
4877
|
+
const state = readWorkState(directory);
|
|
4878
|
+
if (state) {
|
|
4879
|
+
const progress = getPlanProgress(state.active_plan);
|
|
4880
|
+
if (progress.isComplete) {
|
|
4881
|
+
generateMetricsReport(directory, state);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
} catch (err) {
|
|
4885
|
+
log("[analytics] Failed to generate metrics report on session end (non-fatal)", { error: String(err) });
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
3236
4888
|
}
|
|
3237
4889
|
}
|
|
3238
|
-
if (event.type === "message.updated"
|
|
4890
|
+
if (event.type === "message.updated") {
|
|
3239
4891
|
const evt = event;
|
|
3240
4892
|
const info = evt.properties?.info;
|
|
3241
4893
|
if (info?.role === "assistant" && info.sessionID) {
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
if (result.action !== "none") {
|
|
3253
|
-
log("[context-window] Threshold crossed", {
|
|
3254
|
-
sessionId: info.sessionID,
|
|
3255
|
-
action: result.action,
|
|
3256
|
-
usagePct: result.usagePct
|
|
4894
|
+
if (hooks.checkContextWindow) {
|
|
4895
|
+
const inputTokens = info.tokens?.input ?? 0;
|
|
4896
|
+
if (inputTokens > 0) {
|
|
4897
|
+
updateUsage(info.sessionID, inputTokens);
|
|
4898
|
+
const tokenState = getState(info.sessionID);
|
|
4899
|
+
if (tokenState && tokenState.maxTokens > 0) {
|
|
4900
|
+
const result = hooks.checkContextWindow({
|
|
4901
|
+
usedTokens: tokenState.usedTokens,
|
|
4902
|
+
maxTokens: tokenState.maxTokens,
|
|
4903
|
+
sessionId: info.sessionID
|
|
3257
4904
|
});
|
|
4905
|
+
if (result.action !== "none") {
|
|
4906
|
+
log("[context-window] Threshold crossed", {
|
|
4907
|
+
sessionId: info.sessionID,
|
|
4908
|
+
action: result.action,
|
|
4909
|
+
usagePct: result.usagePct
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
3258
4912
|
}
|
|
3259
4913
|
}
|
|
3260
4914
|
}
|
|
3261
4915
|
}
|
|
3262
4916
|
}
|
|
4917
|
+
if (event.type === "message.updated" && tracker && hooks.analyticsEnabled) {
|
|
4918
|
+
const evt = event;
|
|
4919
|
+
const info = evt.properties?.info;
|
|
4920
|
+
if (info?.role === "assistant" && info.sessionID) {
|
|
4921
|
+
if (typeof info.cost === "number" && info.cost > 0) {
|
|
4922
|
+
tracker.trackCost(info.sessionID, info.cost);
|
|
4923
|
+
}
|
|
4924
|
+
if (info.tokens) {
|
|
4925
|
+
tracker.trackTokenUsage(info.sessionID, {
|
|
4926
|
+
input: info.tokens.input ?? 0,
|
|
4927
|
+
output: info.tokens.output ?? 0,
|
|
4928
|
+
reasoning: info.tokens.reasoning ?? 0,
|
|
4929
|
+
cacheRead: info.tokens.cache?.read ?? 0,
|
|
4930
|
+
cacheWrite: info.tokens.cache?.write ?? 0
|
|
4931
|
+
});
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
3263
4935
|
if (event.type === "tui.command.execute") {
|
|
3264
4936
|
const evt = event;
|
|
3265
4937
|
if (evt.properties?.command === "session.interrupt") {
|
|
3266
4938
|
pauseWork(directory);
|
|
3267
4939
|
log("[work-continuation] User interrupt detected — work paused");
|
|
4940
|
+
if (directory) {
|
|
4941
|
+
const activeWorkflow = getActiveWorkflowInstance(directory);
|
|
4942
|
+
if (activeWorkflow && activeWorkflow.status === "running") {
|
|
4943
|
+
pauseWorkflow(directory, "User interrupt");
|
|
4944
|
+
log("[workflow] User interrupt detected — workflow paused");
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
if (event.type === "message.part.updated") {
|
|
4950
|
+
const evt = event;
|
|
4951
|
+
const part = evt.properties?.part;
|
|
4952
|
+
if (part?.type === "text" && part.sessionID && part.text) {
|
|
4953
|
+
lastAssistantMessageText.set(part.sessionID, part.text);
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
4956
|
+
if (hooks.workflowContinuation && event.type === "session.idle") {
|
|
4957
|
+
const evt = event;
|
|
4958
|
+
const sessionId = evt.properties?.sessionID ?? "";
|
|
4959
|
+
if (sessionId && directory) {
|
|
4960
|
+
const activeWorkflow = getActiveWorkflowInstance(directory);
|
|
4961
|
+
if (activeWorkflow && activeWorkflow.status === "running") {
|
|
4962
|
+
const lastMsg = lastAssistantMessageText.get(sessionId) ?? undefined;
|
|
4963
|
+
const lastUserMsg = lastUserMessageText.get(sessionId) ?? undefined;
|
|
4964
|
+
const result = hooks.workflowContinuation(sessionId, lastMsg, lastUserMsg);
|
|
4965
|
+
if (result.continuationPrompt && client) {
|
|
4966
|
+
try {
|
|
4967
|
+
await client.session.promptAsync({
|
|
4968
|
+
path: { id: sessionId },
|
|
4969
|
+
body: {
|
|
4970
|
+
parts: [{ type: "text", text: result.continuationPrompt }],
|
|
4971
|
+
...result.switchAgent ? { agent: getAgentDisplayName(result.switchAgent) } : {}
|
|
4972
|
+
}
|
|
4973
|
+
});
|
|
4974
|
+
log("[workflow] Injected workflow continuation prompt", {
|
|
4975
|
+
sessionId,
|
|
4976
|
+
agent: result.switchAgent
|
|
4977
|
+
});
|
|
4978
|
+
} catch (err) {
|
|
4979
|
+
log("[workflow] Failed to inject workflow continuation", { sessionId, error: String(err) });
|
|
4980
|
+
}
|
|
4981
|
+
return;
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
3268
4984
|
}
|
|
3269
4985
|
}
|
|
3270
4986
|
if (hooks.workContinuation && event.type === "session.idle") {
|
|
@@ -3342,79 +5058,40 @@ ${result.contextInjection}`;
|
|
|
3342
5058
|
const agentArg = input.tool === "task" && inputArgs ? inputArgs.subagent_type ?? inputArgs.description ?? "unknown" : undefined;
|
|
3343
5059
|
tracker.trackToolEnd(input.sessionID, input.tool, input.callID, agentArg);
|
|
3344
5060
|
}
|
|
5061
|
+
},
|
|
5062
|
+
"command.execute.before": async (input, output) => {
|
|
5063
|
+
const { command, arguments: args2 } = input;
|
|
5064
|
+
const parts = output.parts;
|
|
5065
|
+
if (command === "token-report") {
|
|
5066
|
+
const summaries = readSessionSummaries(directory);
|
|
5067
|
+
const reportText = generateTokenReport(summaries);
|
|
5068
|
+
parts.push({ type: "text", text: reportText });
|
|
5069
|
+
}
|
|
5070
|
+
if (command === "metrics") {
|
|
5071
|
+
if (!hooks.analyticsEnabled) {
|
|
5072
|
+
parts.push({
|
|
5073
|
+
type: "text",
|
|
5074
|
+
text: 'Analytics is not enabled. To enable it, set `"analytics": { "enabled": true }` in your `weave.json`.'
|
|
5075
|
+
});
|
|
5076
|
+
return;
|
|
5077
|
+
}
|
|
5078
|
+
const reports = readMetricsReports(directory);
|
|
5079
|
+
const summaries = readSessionSummaries(directory);
|
|
5080
|
+
const metricsMarkdown = formatMetricsMarkdown(reports, summaries, args2);
|
|
5081
|
+
parts.push({ type: "text", text: metricsMarkdown });
|
|
5082
|
+
}
|
|
3345
5083
|
}
|
|
3346
5084
|
};
|
|
3347
5085
|
}
|
|
3348
|
-
|
|
3349
|
-
// src/features/analytics/types.ts
|
|
3350
|
-
var ANALYTICS_DIR = ".weave/analytics";
|
|
3351
|
-
var SESSION_SUMMARIES_FILE = "session-summaries.jsonl";
|
|
3352
|
-
var FINGERPRINT_FILE = "fingerprint.json";
|
|
3353
|
-
// src/features/analytics/storage.ts
|
|
3354
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
3355
|
-
import { join as join7 } from "path";
|
|
3356
|
-
var MAX_SESSION_ENTRIES = 1000;
|
|
3357
|
-
function ensureAnalyticsDir(directory) {
|
|
3358
|
-
const dir = join7(directory, ANALYTICS_DIR);
|
|
3359
|
-
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
3360
|
-
return dir;
|
|
3361
|
-
}
|
|
3362
|
-
function appendSessionSummary(directory, summary) {
|
|
3363
|
-
try {
|
|
3364
|
-
const dir = ensureAnalyticsDir(directory);
|
|
3365
|
-
const filePath = join7(dir, SESSION_SUMMARIES_FILE);
|
|
3366
|
-
const line = JSON.stringify(summary) + `
|
|
3367
|
-
`;
|
|
3368
|
-
appendFileSync2(filePath, line, { encoding: "utf-8", mode: 384 });
|
|
3369
|
-
try {
|
|
3370
|
-
const content = readFileSync7(filePath, "utf-8");
|
|
3371
|
-
const lines = content.split(`
|
|
3372
|
-
`).filter((l) => l.trim().length > 0);
|
|
3373
|
-
if (lines.length > MAX_SESSION_ENTRIES) {
|
|
3374
|
-
const trimmed = lines.slice(-MAX_SESSION_ENTRIES).join(`
|
|
3375
|
-
`) + `
|
|
3376
|
-
`;
|
|
3377
|
-
writeFileSync2(filePath, trimmed, { encoding: "utf-8", mode: 384 });
|
|
3378
|
-
}
|
|
3379
|
-
} catch {}
|
|
3380
|
-
return true;
|
|
3381
|
-
} catch {
|
|
3382
|
-
return false;
|
|
3383
|
-
}
|
|
3384
|
-
}
|
|
3385
|
-
function writeFingerprint(directory, fingerprint) {
|
|
3386
|
-
try {
|
|
3387
|
-
const dir = ensureAnalyticsDir(directory);
|
|
3388
|
-
const filePath = join7(dir, FINGERPRINT_FILE);
|
|
3389
|
-
writeFileSync2(filePath, JSON.stringify(fingerprint, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3390
|
-
return true;
|
|
3391
|
-
} catch {
|
|
3392
|
-
return false;
|
|
3393
|
-
}
|
|
3394
|
-
}
|
|
3395
|
-
function readFingerprint(directory) {
|
|
3396
|
-
const filePath = join7(directory, ANALYTICS_DIR, FINGERPRINT_FILE);
|
|
3397
|
-
try {
|
|
3398
|
-
if (!existsSync8(filePath))
|
|
3399
|
-
return null;
|
|
3400
|
-
const content = readFileSync7(filePath, "utf-8");
|
|
3401
|
-
const parsed = JSON.parse(content);
|
|
3402
|
-
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.stack))
|
|
3403
|
-
return null;
|
|
3404
|
-
return parsed;
|
|
3405
|
-
} catch {
|
|
3406
|
-
return null;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
5086
|
// src/features/analytics/fingerprint.ts
|
|
3410
|
-
import { existsSync as
|
|
3411
|
-
import { join as
|
|
5087
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12, readdirSync as readdirSync5 } from "fs";
|
|
5088
|
+
import { join as join12 } from "path";
|
|
3412
5089
|
import { arch } from "os";
|
|
3413
5090
|
|
|
3414
5091
|
// src/shared/version.ts
|
|
3415
|
-
import { readFileSync as
|
|
5092
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
3416
5093
|
import { fileURLToPath } from "url";
|
|
3417
|
-
import { dirname as dirname2, join as
|
|
5094
|
+
import { dirname as dirname2, join as join11 } from "path";
|
|
3418
5095
|
var cachedVersion;
|
|
3419
5096
|
function getWeaveVersion() {
|
|
3420
5097
|
if (cachedVersion !== undefined)
|
|
@@ -3423,7 +5100,7 @@ function getWeaveVersion() {
|
|
|
3423
5100
|
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
3424
5101
|
for (const rel of ["../../package.json", "../package.json"]) {
|
|
3425
5102
|
try {
|
|
3426
|
-
const pkg = JSON.parse(
|
|
5103
|
+
const pkg = JSON.parse(readFileSync11(join11(thisDir, rel), "utf-8"));
|
|
3427
5104
|
if (pkg.name === "@opencode_weave/weave" && typeof pkg.version === "string") {
|
|
3428
5105
|
const version = pkg.version;
|
|
3429
5106
|
cachedVersion = version;
|
|
@@ -3528,7 +5205,7 @@ function detectStack(directory) {
|
|
|
3528
5205
|
const detected = [];
|
|
3529
5206
|
for (const marker of STACK_MARKERS) {
|
|
3530
5207
|
for (const file of marker.files) {
|
|
3531
|
-
if (
|
|
5208
|
+
if (existsSync13(join12(directory, file))) {
|
|
3532
5209
|
detected.push({
|
|
3533
5210
|
name: marker.name,
|
|
3534
5211
|
confidence: marker.confidence,
|
|
@@ -3539,9 +5216,9 @@ function detectStack(directory) {
|
|
|
3539
5216
|
}
|
|
3540
5217
|
}
|
|
3541
5218
|
try {
|
|
3542
|
-
const pkgPath =
|
|
3543
|
-
if (
|
|
3544
|
-
const pkg = JSON.parse(
|
|
5219
|
+
const pkgPath = join12(directory, "package.json");
|
|
5220
|
+
if (existsSync13(pkgPath)) {
|
|
5221
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3545
5222
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3546
5223
|
if (deps.react) {
|
|
3547
5224
|
detected.push({
|
|
@@ -3554,7 +5231,7 @@ function detectStack(directory) {
|
|
|
3554
5231
|
} catch {}
|
|
3555
5232
|
if (!detected.some((d) => d.name === "dotnet")) {
|
|
3556
5233
|
try {
|
|
3557
|
-
const entries =
|
|
5234
|
+
const entries = readdirSync5(directory);
|
|
3558
5235
|
const dotnetFile = entries.find((e) => e.endsWith(".csproj") || e.endsWith(".fsproj") || e.endsWith(".sln"));
|
|
3559
5236
|
if (dotnetFile) {
|
|
3560
5237
|
detected.push({
|
|
@@ -3574,27 +5251,27 @@ function detectStack(directory) {
|
|
|
3574
5251
|
});
|
|
3575
5252
|
}
|
|
3576
5253
|
function detectPackageManager(directory) {
|
|
3577
|
-
if (
|
|
5254
|
+
if (existsSync13(join12(directory, "bun.lockb")))
|
|
3578
5255
|
return "bun";
|
|
3579
|
-
if (
|
|
5256
|
+
if (existsSync13(join12(directory, "pnpm-lock.yaml")))
|
|
3580
5257
|
return "pnpm";
|
|
3581
|
-
if (
|
|
5258
|
+
if (existsSync13(join12(directory, "yarn.lock")))
|
|
3582
5259
|
return "yarn";
|
|
3583
|
-
if (
|
|
5260
|
+
if (existsSync13(join12(directory, "package-lock.json")))
|
|
3584
5261
|
return "npm";
|
|
3585
|
-
if (
|
|
5262
|
+
if (existsSync13(join12(directory, "package.json")))
|
|
3586
5263
|
return "npm";
|
|
3587
5264
|
return;
|
|
3588
5265
|
}
|
|
3589
5266
|
function detectMonorepo(directory) {
|
|
3590
5267
|
for (const marker of MONOREPO_MARKERS) {
|
|
3591
|
-
if (
|
|
5268
|
+
if (existsSync13(join12(directory, marker)))
|
|
3592
5269
|
return true;
|
|
3593
5270
|
}
|
|
3594
5271
|
try {
|
|
3595
|
-
const pkgPath =
|
|
3596
|
-
if (
|
|
3597
|
-
const pkg = JSON.parse(
|
|
5272
|
+
const pkgPath = join12(directory, "package.json");
|
|
5273
|
+
if (existsSync13(pkgPath)) {
|
|
5274
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3598
5275
|
if (pkg.workspaces)
|
|
3599
5276
|
return true;
|
|
3600
5277
|
}
|
|
@@ -3659,6 +5336,11 @@ function getOrCreateFingerprint(directory) {
|
|
|
3659
5336
|
}
|
|
3660
5337
|
}
|
|
3661
5338
|
// src/features/analytics/session-tracker.ts
|
|
5339
|
+
function safeNum(v) {
|
|
5340
|
+
const n = Number(v);
|
|
5341
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
5342
|
+
}
|
|
5343
|
+
|
|
3662
5344
|
class SessionTracker {
|
|
3663
5345
|
sessions = new Map;
|
|
3664
5346
|
directory;
|
|
@@ -3674,7 +5356,16 @@ class SessionTracker {
|
|
|
3674
5356
|
startedAt: new Date().toISOString(),
|
|
3675
5357
|
toolCounts: {},
|
|
3676
5358
|
delegations: [],
|
|
3677
|
-
inFlight: {}
|
|
5359
|
+
inFlight: {},
|
|
5360
|
+
totalCost: 0,
|
|
5361
|
+
tokenUsage: {
|
|
5362
|
+
inputTokens: 0,
|
|
5363
|
+
outputTokens: 0,
|
|
5364
|
+
reasoningTokens: 0,
|
|
5365
|
+
cacheReadTokens: 0,
|
|
5366
|
+
cacheWriteTokens: 0,
|
|
5367
|
+
totalMessages: 0
|
|
5368
|
+
}
|
|
3678
5369
|
};
|
|
3679
5370
|
this.sessions.set(sessionId, session);
|
|
3680
5371
|
return session;
|
|
@@ -3708,6 +5399,29 @@ class SessionTracker {
|
|
|
3708
5399
|
session.delegations.push(delegation);
|
|
3709
5400
|
}
|
|
3710
5401
|
}
|
|
5402
|
+
setAgentName(sessionId, agentName) {
|
|
5403
|
+
const session = this.sessions.get(sessionId);
|
|
5404
|
+
if (!session)
|
|
5405
|
+
return;
|
|
5406
|
+
if (!session.agentName) {
|
|
5407
|
+
session.agentName = agentName;
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
trackCost(sessionId, cost) {
|
|
5411
|
+
const session = this.sessions.get(sessionId);
|
|
5412
|
+
if (!session)
|
|
5413
|
+
return;
|
|
5414
|
+
session.totalCost += safeNum(cost);
|
|
5415
|
+
}
|
|
5416
|
+
trackTokenUsage(sessionId, tokens) {
|
|
5417
|
+
const session = this.startSession(sessionId);
|
|
5418
|
+
session.tokenUsage.inputTokens += safeNum(tokens.input);
|
|
5419
|
+
session.tokenUsage.outputTokens += safeNum(tokens.output);
|
|
5420
|
+
session.tokenUsage.reasoningTokens += safeNum(tokens.reasoning);
|
|
5421
|
+
session.tokenUsage.cacheReadTokens += safeNum(tokens.cacheRead);
|
|
5422
|
+
session.tokenUsage.cacheWriteTokens += safeNum(tokens.cacheWrite);
|
|
5423
|
+
session.tokenUsage.totalMessages += 1;
|
|
5424
|
+
}
|
|
3711
5425
|
endSession(sessionId) {
|
|
3712
5426
|
const session = this.sessions.get(sessionId);
|
|
3713
5427
|
if (!session)
|
|
@@ -3725,14 +5439,21 @@ class SessionTracker {
|
|
|
3725
5439
|
toolUsage,
|
|
3726
5440
|
delegations: session.delegations,
|
|
3727
5441
|
totalToolCalls,
|
|
3728
|
-
totalDelegations: session.delegations.length
|
|
5442
|
+
totalDelegations: session.delegations.length,
|
|
5443
|
+
agentName: session.agentName,
|
|
5444
|
+
totalCost: session.totalCost > 0 ? session.totalCost : undefined,
|
|
5445
|
+
tokenUsage: session.tokenUsage.totalMessages > 0 ? session.tokenUsage : undefined
|
|
3729
5446
|
};
|
|
3730
5447
|
try {
|
|
3731
5448
|
appendSessionSummary(this.directory, summary);
|
|
3732
5449
|
log("[analytics] Session summary persisted", {
|
|
3733
5450
|
sessionId,
|
|
3734
5451
|
totalToolCalls,
|
|
3735
|
-
totalDelegations: session.delegations.length
|
|
5452
|
+
totalDelegations: session.delegations.length,
|
|
5453
|
+
...summary.tokenUsage ? {
|
|
5454
|
+
inputTokens: summary.tokenUsage.inputTokens,
|
|
5455
|
+
outputTokens: summary.tokenUsage.outputTokens
|
|
5456
|
+
} : {}
|
|
3736
5457
|
});
|
|
3737
5458
|
} catch (err) {
|
|
3738
5459
|
log("[analytics] Failed to persist session summary (non-fatal)", {
|
|
@@ -3759,8 +5480,7 @@ function createSessionTracker(directory) {
|
|
|
3759
5480
|
// src/features/analytics/index.ts
|
|
3760
5481
|
function createAnalytics(directory, fingerprint) {
|
|
3761
5482
|
const tracker = createSessionTracker(directory);
|
|
3762
|
-
|
|
3763
|
-
return { tracker, fingerprint: resolvedFingerprint };
|
|
5483
|
+
return { tracker, fingerprint };
|
|
3764
5484
|
}
|
|
3765
5485
|
|
|
3766
5486
|
// src/index.ts
|
|
@@ -3769,8 +5489,9 @@ var WeavePlugin = async (ctx) => {
|
|
|
3769
5489
|
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
|
3770
5490
|
const isHookEnabled = (name) => !disabledHooks.has(name);
|
|
3771
5491
|
const analyticsEnabled = pluginConfig.analytics?.enabled === true;
|
|
3772
|
-
const
|
|
3773
|
-
const
|
|
5492
|
+
const fingerprintEnabled = analyticsEnabled && pluginConfig.analytics?.use_fingerprint === true;
|
|
5493
|
+
const fingerprint = fingerprintEnabled ? getOrCreateFingerprint(ctx.directory) : null;
|
|
5494
|
+
const configDir = join13(ctx.directory, ".opencode");
|
|
3774
5495
|
const toolsResult = await createTools({ ctx, pluginConfig });
|
|
3775
5496
|
const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn, fingerprint, configDir });
|
|
3776
5497
|
const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory, analyticsEnabled });
|