@opencode_weave/weave 0.3.2
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/LICENSE +21 -0
- package/README.md +317 -0
- package/dist/agents/agent-builder.d.ts +10 -0
- package/dist/agents/builtin-agents.d.ts +16 -0
- package/dist/agents/dynamic-prompt-builder.d.ts +29 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/agents/loom/default.d.ts +2 -0
- package/dist/agents/loom/index.d.ts +2 -0
- package/dist/agents/model-resolution.d.ts +19 -0
- package/dist/agents/pattern/default.d.ts +2 -0
- package/dist/agents/pattern/index.d.ts +2 -0
- package/dist/agents/shuttle/default.d.ts +2 -0
- package/dist/agents/shuttle/index.d.ts +2 -0
- package/dist/agents/spindle/default.d.ts +2 -0
- package/dist/agents/spindle/index.d.ts +2 -0
- package/dist/agents/tapestry/default.d.ts +2 -0
- package/dist/agents/tapestry/index.d.ts +2 -0
- package/dist/agents/thread/default.d.ts +2 -0
- package/dist/agents/thread/index.d.ts +2 -0
- package/dist/agents/types.d.ts +82 -0
- package/dist/agents/warp/default.d.ts +2 -0
- package/dist/agents/warp/index.d.ts +2 -0
- package/dist/agents/weft/default.d.ts +2 -0
- package/dist/agents/weft/index.d.ts +2 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/merge.d.ts +3 -0
- package/dist/config/schema.d.ts +162 -0
- package/dist/create-managers.d.ts +18 -0
- package/dist/create-tools.d.ts +16 -0
- package/dist/features/builtin-commands/commands.d.ts +2 -0
- package/dist/features/builtin-commands/index.d.ts +2 -0
- package/dist/features/builtin-commands/templates/start-work.d.ts +1 -0
- package/dist/features/builtin-commands/types.d.ts +16 -0
- package/dist/features/skill-loader/builtin-skills.d.ts +2 -0
- package/dist/features/skill-loader/discovery.d.ts +12 -0
- package/dist/features/skill-loader/index.d.ts +7 -0
- package/dist/features/skill-loader/loader.d.ts +6 -0
- package/dist/features/skill-loader/merger.d.ts +2 -0
- package/dist/features/skill-loader/resolver.d.ts +6 -0
- package/dist/features/skill-loader/types.d.ts +25 -0
- package/dist/features/work-state/constants.d.ts +8 -0
- package/dist/features/work-state/index.d.ts +3 -0
- package/dist/features/work-state/storage.d.ts +38 -0
- package/dist/features/work-state/types.d.ts +27 -0
- package/dist/hooks/context-window-monitor.d.ts +19 -0
- package/dist/hooks/create-hooks.d.ts +32 -0
- package/dist/hooks/first-message-variant.d.ts +5 -0
- package/dist/hooks/index.d.ts +12 -0
- package/dist/hooks/keyword-detector.d.ts +8 -0
- package/dist/hooks/pattern-md-only.d.ts +13 -0
- package/dist/hooks/rules-injector.d.ts +6 -0
- package/dist/hooks/start-work-hook.d.ts +20 -0
- package/dist/hooks/verification-reminder.d.ts +22 -0
- package/dist/hooks/work-continuation.d.ts +17 -0
- package/dist/hooks/write-existing-file-guard.d.ts +14 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2191 -0
- package/dist/managers/background-manager.d.ts +68 -0
- package/dist/managers/config-handler.d.ts +54 -0
- package/dist/managers/index.d.ts +6 -0
- package/dist/managers/skill-mcp-manager.d.ts +30 -0
- package/dist/plugin/index.d.ts +1 -0
- package/dist/plugin/plugin-interface.d.ts +12 -0
- package/dist/plugin/types.d.ts +5 -0
- package/dist/shared/agent-display-names.d.ts +20 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/log.d.ts +2 -0
- package/dist/shared/types.d.ts +6 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/permissions.d.ts +18 -0
- package/dist/tools/registry.d.ts +29 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2191 @@
|
|
|
1
|
+
// src/config/loader.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join as join2 } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { parse } from "jsonc-parser";
|
|
6
|
+
|
|
7
|
+
// src/config/schema.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var AgentOverrideConfigSchema = z.object({
|
|
10
|
+
model: z.string().optional(),
|
|
11
|
+
fallback_models: z.array(z.string()).optional(),
|
|
12
|
+
variant: z.string().optional(),
|
|
13
|
+
category: z.string().optional(),
|
|
14
|
+
skills: z.array(z.string()).optional(),
|
|
15
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
16
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
17
|
+
prompt: z.string().optional(),
|
|
18
|
+
prompt_append: z.string().optional(),
|
|
19
|
+
tools: z.record(z.string(), z.boolean()).optional(),
|
|
20
|
+
disable: z.boolean().optional(),
|
|
21
|
+
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
|
22
|
+
maxTokens: z.number().optional()
|
|
23
|
+
});
|
|
24
|
+
var AgentOverridesSchema = z.record(z.string(), AgentOverrideConfigSchema);
|
|
25
|
+
var CategoryConfigSchema = z.object({
|
|
26
|
+
description: z.string().optional(),
|
|
27
|
+
model: z.string().optional(),
|
|
28
|
+
fallback_models: z.array(z.string()).optional(),
|
|
29
|
+
variant: z.string().optional(),
|
|
30
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
31
|
+
top_p: z.number().min(0).max(1).optional(),
|
|
32
|
+
maxTokens: z.number().optional(),
|
|
33
|
+
tools: z.record(z.string(), z.boolean()).optional(),
|
|
34
|
+
prompt_append: z.string().optional(),
|
|
35
|
+
disable: z.boolean().optional()
|
|
36
|
+
});
|
|
37
|
+
var CategoriesConfigSchema = z.record(z.string(), CategoryConfigSchema);
|
|
38
|
+
var BackgroundConfigSchema = z.object({
|
|
39
|
+
defaultConcurrency: z.number().min(1).optional(),
|
|
40
|
+
providerConcurrency: z.record(z.string(), z.number().min(0)).optional(),
|
|
41
|
+
modelConcurrency: z.record(z.string(), z.number().min(0)).optional(),
|
|
42
|
+
staleTimeoutMs: z.number().min(60000).optional()
|
|
43
|
+
});
|
|
44
|
+
var TmuxConfigSchema = z.object({
|
|
45
|
+
enabled: z.boolean().optional(),
|
|
46
|
+
layout: z.enum(["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical"]).optional(),
|
|
47
|
+
main_pane_size: z.number().optional()
|
|
48
|
+
});
|
|
49
|
+
var SkillsConfigSchema = z.object({
|
|
50
|
+
paths: z.array(z.string()).optional(),
|
|
51
|
+
recursive: z.boolean().optional()
|
|
52
|
+
});
|
|
53
|
+
var ExperimentalConfigSchema = z.object({
|
|
54
|
+
plugin_load_timeout_ms: z.number().min(1000).optional(),
|
|
55
|
+
context_window_warning_threshold: z.number().min(0).max(1).optional(),
|
|
56
|
+
context_window_critical_threshold: z.number().min(0).max(1).optional()
|
|
57
|
+
});
|
|
58
|
+
var WeaveConfigSchema = z.object({
|
|
59
|
+
$schema: z.string().optional(),
|
|
60
|
+
agents: AgentOverridesSchema.optional(),
|
|
61
|
+
categories: CategoriesConfigSchema.optional(),
|
|
62
|
+
disabled_hooks: z.array(z.string()).optional(),
|
|
63
|
+
disabled_tools: z.array(z.string()).optional(),
|
|
64
|
+
disabled_agents: z.array(z.string()).optional(),
|
|
65
|
+
disabled_skills: z.array(z.string()).optional(),
|
|
66
|
+
background: BackgroundConfigSchema.optional(),
|
|
67
|
+
tmux: TmuxConfigSchema.optional(),
|
|
68
|
+
skills: SkillsConfigSchema.optional(),
|
|
69
|
+
experimental: ExperimentalConfigSchema.optional()
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// src/config/merge.ts
|
|
73
|
+
function deepMergeObjects(base, override) {
|
|
74
|
+
const result = { ...base };
|
|
75
|
+
for (const key of Object.keys(override)) {
|
|
76
|
+
const overrideVal = override[key];
|
|
77
|
+
const baseVal = base[key];
|
|
78
|
+
if (overrideVal !== null && typeof overrideVal === "object" && !Array.isArray(overrideVal) && baseVal !== null && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
79
|
+
result[key] = deepMergeObjects(baseVal, overrideVal);
|
|
80
|
+
} else if (overrideVal !== undefined) {
|
|
81
|
+
result[key] = overrideVal;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function mergeStringArrays(base, override) {
|
|
87
|
+
if (!base && !override)
|
|
88
|
+
return;
|
|
89
|
+
return [...new Set([...base ?? [], ...override ?? []])];
|
|
90
|
+
}
|
|
91
|
+
function mergeConfigs(user, project) {
|
|
92
|
+
return {
|
|
93
|
+
...user,
|
|
94
|
+
...project,
|
|
95
|
+
agents: user.agents || project.agents ? deepMergeObjects(user.agents ?? {}, project.agents ?? {}) : undefined,
|
|
96
|
+
categories: user.categories || project.categories ? deepMergeObjects(user.categories ?? {}, project.categories ?? {}) : undefined,
|
|
97
|
+
disabled_hooks: mergeStringArrays(user.disabled_hooks, project.disabled_hooks),
|
|
98
|
+
disabled_tools: mergeStringArrays(user.disabled_tools, project.disabled_tools),
|
|
99
|
+
disabled_agents: mergeStringArrays(user.disabled_agents, project.disabled_agents),
|
|
100
|
+
disabled_skills: mergeStringArrays(user.disabled_skills, project.disabled_skills),
|
|
101
|
+
background: project.background ?? user.background,
|
|
102
|
+
tmux: project.tmux ?? user.tmux,
|
|
103
|
+
skills: project.skills ?? user.skills,
|
|
104
|
+
experimental: user.experimental || project.experimental ? { ...user.experimental, ...project.experimental } : undefined
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/shared/log.ts
|
|
109
|
+
import * as fs from "fs";
|
|
110
|
+
import * as path from "path";
|
|
111
|
+
import * as os from "os";
|
|
112
|
+
var LOG_FILE = path.join(os.tmpdir(), "weave-opencode.log");
|
|
113
|
+
function log(message, data) {
|
|
114
|
+
try {
|
|
115
|
+
const timestamp = new Date().toISOString();
|
|
116
|
+
const entry = `[${timestamp}] ${message}${data !== undefined ? " " + JSON.stringify(data) : ""}
|
|
117
|
+
`;
|
|
118
|
+
fs.appendFileSync(LOG_FILE, entry);
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/config/loader.ts
|
|
123
|
+
function readJsoncFile(filePath) {
|
|
124
|
+
try {
|
|
125
|
+
const text = readFileSync(filePath, "utf-8");
|
|
126
|
+
const errors = [];
|
|
127
|
+
const parsed = parse(text, errors);
|
|
128
|
+
if (errors.length > 0) {
|
|
129
|
+
log(`JSONC parse warnings in ${filePath}: ${errors.length} issue(s)`);
|
|
130
|
+
}
|
|
131
|
+
return parsed ?? {};
|
|
132
|
+
} catch (e) {
|
|
133
|
+
log(`Failed to read config file ${filePath}`, e);
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function detectConfigFile(basePath) {
|
|
138
|
+
const jsoncPath = basePath + ".jsonc";
|
|
139
|
+
if (existsSync(jsoncPath))
|
|
140
|
+
return jsoncPath;
|
|
141
|
+
const jsonPath = basePath + ".json";
|
|
142
|
+
if (existsSync(jsonPath))
|
|
143
|
+
return jsonPath;
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function loadWeaveConfig(directory, _ctx) {
|
|
147
|
+
const userBasePath = join2(homedir(), ".config", "opencode", "weave-opencode");
|
|
148
|
+
const projectBasePath = join2(directory, ".opencode", "weave-opencode");
|
|
149
|
+
const userConfigPath = detectConfigFile(userBasePath);
|
|
150
|
+
const projectConfigPath = detectConfigFile(projectBasePath);
|
|
151
|
+
const userRaw = userConfigPath ? readJsoncFile(userConfigPath) : {};
|
|
152
|
+
const projectRaw = projectConfigPath ? readJsoncFile(projectConfigPath) : {};
|
|
153
|
+
const merged = mergeConfigs(userRaw, projectRaw);
|
|
154
|
+
const result = WeaveConfigSchema.safeParse(merged);
|
|
155
|
+
if (!result.success) {
|
|
156
|
+
log("WeaveConfig validation errors — using defaults", result.error.issues);
|
|
157
|
+
return WeaveConfigSchema.parse({});
|
|
158
|
+
}
|
|
159
|
+
return result.data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/shared/agent-display-names.ts
|
|
163
|
+
var AGENT_DISPLAY_NAMES = {
|
|
164
|
+
loom: "Loom (Main Orchestrator)",
|
|
165
|
+
tapestry: "Tapestry (Execution Orchestrator)",
|
|
166
|
+
shuttle: "shuttle",
|
|
167
|
+
pattern: "pattern",
|
|
168
|
+
thread: "thread",
|
|
169
|
+
spindle: "spindle",
|
|
170
|
+
warp: "warp"
|
|
171
|
+
};
|
|
172
|
+
function getAgentDisplayName(configKey) {
|
|
173
|
+
const exactMatch = AGENT_DISPLAY_NAMES[configKey];
|
|
174
|
+
if (exactMatch !== undefined)
|
|
175
|
+
return exactMatch;
|
|
176
|
+
const lowerKey = configKey.toLowerCase();
|
|
177
|
+
for (const [k, v] of Object.entries(AGENT_DISPLAY_NAMES)) {
|
|
178
|
+
if (k.toLowerCase() === lowerKey)
|
|
179
|
+
return v;
|
|
180
|
+
}
|
|
181
|
+
return configKey;
|
|
182
|
+
}
|
|
183
|
+
var REVERSE_DISPLAY_NAMES = Object.fromEntries(Object.entries(AGENT_DISPLAY_NAMES).map(([key, displayName]) => [displayName.toLowerCase(), key]));
|
|
184
|
+
|
|
185
|
+
// src/features/builtin-commands/templates/start-work.ts
|
|
186
|
+
var START_WORK_TEMPLATE = `You are being activated by the /start-work command to execute a Weave plan.
|
|
187
|
+
|
|
188
|
+
## Your Mission
|
|
189
|
+
Read and execute the work plan, completing each task systematically.
|
|
190
|
+
|
|
191
|
+
## Startup Procedure
|
|
192
|
+
|
|
193
|
+
1. **Check for active work state**: Read \`.weave/state.json\` to see if there's a plan already in progress.
|
|
194
|
+
2. **If resuming**: The system has injected context below with the active plan path and progress. Read the plan file, find the first unchecked \`- [ ]\` task, and continue from there.
|
|
195
|
+
3. **If starting fresh**: The system has selected a plan and created work state. Read the plan file and begin from the first task.
|
|
196
|
+
|
|
197
|
+
## Execution Loop
|
|
198
|
+
|
|
199
|
+
For each unchecked \`- [ ]\` task in the plan:
|
|
200
|
+
|
|
201
|
+
1. **Read** the task description, acceptance criteria, and any references
|
|
202
|
+
2. **Execute** the task — write code, run commands, create files as needed
|
|
203
|
+
3. **Verify** the work — run tests, check for errors, validate acceptance criteria
|
|
204
|
+
4. **Mark complete** — use the Edit tool to change \`- [ ]\` to \`- [x]\` in the plan file
|
|
205
|
+
5. **Move on** — find the next unchecked task and repeat
|
|
206
|
+
|
|
207
|
+
## Rules
|
|
208
|
+
|
|
209
|
+
- Work through tasks **top to bottom** unless dependencies require a different order
|
|
210
|
+
- **Verify every task** before marking it complete
|
|
211
|
+
- If blocked on a task, document the reason as a comment in the plan and move to the next unblocked task
|
|
212
|
+
- Report progress after each task: "Completed task N/M: [title]"
|
|
213
|
+
- Do NOT stop until all checkboxes are checked or you are explicitly told to stop
|
|
214
|
+
- After all tasks are complete, report a final summary`;
|
|
215
|
+
|
|
216
|
+
// src/features/builtin-commands/commands.ts
|
|
217
|
+
var BUILTIN_COMMANDS = {
|
|
218
|
+
"start-work": {
|
|
219
|
+
name: "start-work",
|
|
220
|
+
description: "Start executing a Weave plan created by Pattern",
|
|
221
|
+
agent: "tapestry",
|
|
222
|
+
template: `<command-instruction>
|
|
223
|
+
${START_WORK_TEMPLATE}
|
|
224
|
+
</command-instruction>
|
|
225
|
+
<session-context>Session ID: $SESSION_ID Timestamp: $TIMESTAMP</session-context>
|
|
226
|
+
<user-request>$ARGUMENTS</user-request>`,
|
|
227
|
+
argumentHint: "[plan-name]"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
// src/managers/config-handler.ts
|
|
231
|
+
class ConfigHandler {
|
|
232
|
+
pluginConfig;
|
|
233
|
+
constructor(options) {
|
|
234
|
+
this.pluginConfig = options.pluginConfig;
|
|
235
|
+
}
|
|
236
|
+
async handle(input) {
|
|
237
|
+
const { pluginConfig, agents = {}, availableTools = [] } = input;
|
|
238
|
+
this.applyProviderConfig();
|
|
239
|
+
const resolvedAgents = this.applyAgentConfig(agents, pluginConfig);
|
|
240
|
+
const resolvedTools = this.applyToolConfig(availableTools, pluginConfig);
|
|
241
|
+
const resolvedMcps = this.applyMcpConfig();
|
|
242
|
+
const resolvedCommands = this.applyCommandConfig();
|
|
243
|
+
this.applySkillConfig();
|
|
244
|
+
const defaultAgent = this.resolveDefaultAgent(resolvedAgents);
|
|
245
|
+
return {
|
|
246
|
+
agents: resolvedAgents,
|
|
247
|
+
defaultAgent,
|
|
248
|
+
tools: resolvedTools,
|
|
249
|
+
mcps: resolvedMcps,
|
|
250
|
+
commands: resolvedCommands
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
applyProviderConfig() {}
|
|
254
|
+
applyAgentConfig(agents, pluginConfig) {
|
|
255
|
+
const disabledSet = new Set(pluginConfig.disabled_agents ?? []);
|
|
256
|
+
const overrides = pluginConfig.agents ?? {};
|
|
257
|
+
const result = {};
|
|
258
|
+
for (const [name, agentConfig] of Object.entries(agents)) {
|
|
259
|
+
if (disabledSet.has(name)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const override = overrides[name];
|
|
263
|
+
const merged = override ? { ...agentConfig, ...override } : { ...agentConfig };
|
|
264
|
+
const displayName = getAgentDisplayName(name);
|
|
265
|
+
result[displayName] = merged;
|
|
266
|
+
}
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
resolveDefaultAgent(agents) {
|
|
270
|
+
const loomDisplayName = getAgentDisplayName("loom");
|
|
271
|
+
if (agents[loomDisplayName])
|
|
272
|
+
return loomDisplayName;
|
|
273
|
+
const firstKey = Object.keys(agents)[0];
|
|
274
|
+
return firstKey;
|
|
275
|
+
}
|
|
276
|
+
applyToolConfig(availableTools, pluginConfig) {
|
|
277
|
+
const disabledSet = new Set(pluginConfig.disabled_tools ?? []);
|
|
278
|
+
return availableTools.filter((tool) => !disabledSet.has(tool));
|
|
279
|
+
}
|
|
280
|
+
applyMcpConfig() {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
applyCommandConfig() {
|
|
284
|
+
const commands = structuredClone(BUILTIN_COMMANDS);
|
|
285
|
+
for (const cmd of Object.values(commands)) {
|
|
286
|
+
if (cmd?.agent && typeof cmd.agent === "string") {
|
|
287
|
+
cmd.agent = getAgentDisplayName(cmd.agent);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return commands;
|
|
291
|
+
}
|
|
292
|
+
applySkillConfig() {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/managers/background-manager.ts
|
|
296
|
+
var TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
297
|
+
|
|
298
|
+
class BackgroundManager {
|
|
299
|
+
tasks = new Map;
|
|
300
|
+
maxConcurrent;
|
|
301
|
+
constructor(options) {
|
|
302
|
+
this.maxConcurrent = options?.maxConcurrent ?? 5;
|
|
303
|
+
}
|
|
304
|
+
spawn(options) {
|
|
305
|
+
const runningCount = this.getRunningCount();
|
|
306
|
+
if (runningCount >= this.maxConcurrent) {
|
|
307
|
+
throw new Error(`Concurrency limit reached: ${runningCount}/${this.maxConcurrent} tasks running`);
|
|
308
|
+
}
|
|
309
|
+
const id = crypto.randomUUID();
|
|
310
|
+
const record = {
|
|
311
|
+
id,
|
|
312
|
+
status: "pending",
|
|
313
|
+
options,
|
|
314
|
+
startedAt: new Date
|
|
315
|
+
};
|
|
316
|
+
this.tasks.set(id, record);
|
|
317
|
+
return id;
|
|
318
|
+
}
|
|
319
|
+
getTask(taskId) {
|
|
320
|
+
return this.tasks.get(taskId);
|
|
321
|
+
}
|
|
322
|
+
cancel(taskId) {
|
|
323
|
+
const record = this.tasks.get(taskId);
|
|
324
|
+
if (!record) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (TERMINAL_STATUSES.has(record.status)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
record.status = "cancelled";
|
|
331
|
+
record.completedAt = new Date;
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
cancelAll() {
|
|
335
|
+
for (const record of this.tasks.values()) {
|
|
336
|
+
if (!TERMINAL_STATUSES.has(record.status)) {
|
|
337
|
+
record.status = "cancelled";
|
|
338
|
+
record.completedAt = new Date;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
list(filter) {
|
|
343
|
+
const all = Array.from(this.tasks.values());
|
|
344
|
+
if (filter?.status !== undefined) {
|
|
345
|
+
return all.filter((t) => t.status === filter.status);
|
|
346
|
+
}
|
|
347
|
+
return all;
|
|
348
|
+
}
|
|
349
|
+
getRunningCount() {
|
|
350
|
+
let count = 0;
|
|
351
|
+
for (const record of this.tasks.values()) {
|
|
352
|
+
if (record.status === "running") {
|
|
353
|
+
count++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return count;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/managers/skill-mcp-manager.ts
|
|
361
|
+
function createStdioClient(config, info) {
|
|
362
|
+
const { command, args = [] } = config;
|
|
363
|
+
const { serverName, skillName } = info;
|
|
364
|
+
if (!command) {
|
|
365
|
+
throw new Error(`missing 'command' field for stdio MCP server '${serverName}' in skill '${skillName}'`);
|
|
366
|
+
}
|
|
367
|
+
const env = { ...process.env, ...config.env ?? {} };
|
|
368
|
+
let proc = null;
|
|
369
|
+
try {
|
|
370
|
+
proc = Bun.spawn([command, ...args], {
|
|
371
|
+
env,
|
|
372
|
+
stdin: "pipe",
|
|
373
|
+
stdout: "pipe",
|
|
374
|
+
stderr: "pipe"
|
|
375
|
+
});
|
|
376
|
+
} catch (spawnError) {
|
|
377
|
+
const msg = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
378
|
+
throw new Error(`stdio MCP server '${serverName}' in skill '${skillName}' failed to start: ${msg}`);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
async callTool(_params) {
|
|
382
|
+
throw new Error("not implemented in v1");
|
|
383
|
+
},
|
|
384
|
+
async close() {
|
|
385
|
+
if (proc) {
|
|
386
|
+
proc.kill();
|
|
387
|
+
proc = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function getClientKey(info) {
|
|
393
|
+
return `${info.sessionID}:${info.skillName}:${info.serverName}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
class SkillMcpManager {
|
|
397
|
+
clients = new Map;
|
|
398
|
+
async getOrCreateClient(info, config) {
|
|
399
|
+
const key = getClientKey(info);
|
|
400
|
+
const existing = this.clients.get(key);
|
|
401
|
+
if (existing) {
|
|
402
|
+
return existing;
|
|
403
|
+
}
|
|
404
|
+
const { serverName, skillName } = info;
|
|
405
|
+
if (config.type === "http") {
|
|
406
|
+
throw new Error("HTTP MCP not supported in v1");
|
|
407
|
+
}
|
|
408
|
+
if (!config.command) {
|
|
409
|
+
throw new Error(`missing 'command' field for stdio MCP server '${serverName}' in skill '${skillName}'`);
|
|
410
|
+
}
|
|
411
|
+
const client = createStdioClient(config, info);
|
|
412
|
+
this.clients.set(key, client);
|
|
413
|
+
return client;
|
|
414
|
+
}
|
|
415
|
+
async disconnectSession(sessionID) {
|
|
416
|
+
const prefix = `${sessionID}:`;
|
|
417
|
+
const keysToRemove = [];
|
|
418
|
+
for (const key of this.clients.keys()) {
|
|
419
|
+
if (key.startsWith(prefix)) {
|
|
420
|
+
keysToRemove.push(key);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
await Promise.all(keysToRemove.map(async (key) => {
|
|
424
|
+
const client = this.clients.get(key);
|
|
425
|
+
if (client) {
|
|
426
|
+
await client.close();
|
|
427
|
+
this.clients.delete(key);
|
|
428
|
+
}
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
async disconnectAll() {
|
|
432
|
+
await Promise.all(Array.from(this.clients.entries()).map(async ([key, client]) => {
|
|
433
|
+
await client.close();
|
|
434
|
+
this.clients.delete(key);
|
|
435
|
+
}));
|
|
436
|
+
this.clients.clear();
|
|
437
|
+
}
|
|
438
|
+
getConnectedServers() {
|
|
439
|
+
return Array.from(this.clients.keys());
|
|
440
|
+
}
|
|
441
|
+
isConnected(info) {
|
|
442
|
+
return this.clients.has(getClientKey(info));
|
|
443
|
+
}
|
|
444
|
+
async callTool(info, config, name, args) {
|
|
445
|
+
const maxAttempts = 3;
|
|
446
|
+
let lastError = null;
|
|
447
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
448
|
+
try {
|
|
449
|
+
const client = await this.getOrCreateClient(info, config);
|
|
450
|
+
const result = await client.callTool({ name, arguments: args });
|
|
451
|
+
return result.content;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
454
|
+
if (!lastError.message.toLowerCase().includes("not connected")) {
|
|
455
|
+
throw lastError;
|
|
456
|
+
}
|
|
457
|
+
if (attempt === maxAttempts) {
|
|
458
|
+
throw new Error(`Failed after 3 reconnection attempts: ${lastError.message}`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
throw lastError ?? new Error("Operation failed with unknown error");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/agents/loom/default.ts
|
|
467
|
+
var LOOM_DEFAULTS = {
|
|
468
|
+
temperature: 0.1,
|
|
469
|
+
description: "Loom (Main Orchestrator)",
|
|
470
|
+
prompt: `<Role>
|
|
471
|
+
Loom — main orchestrator for Weave.
|
|
472
|
+
Plan tasks, coordinate work, and delegate to specialized agents.
|
|
473
|
+
You are the team lead. Understand the request, break it into tasks, delegate intelligently.
|
|
474
|
+
</Role>
|
|
475
|
+
|
|
476
|
+
<Discipline>
|
|
477
|
+
TODO OBSESSION (NON-NEGOTIABLE):
|
|
478
|
+
- 2+ steps → todowrite FIRST, atomic breakdown
|
|
479
|
+
- Mark in_progress before starting (ONE at a time)
|
|
480
|
+
- Mark completed IMMEDIATELY after each step
|
|
481
|
+
- NEVER batch completions
|
|
482
|
+
|
|
483
|
+
No todos on multi-step work = INCOMPLETE WORK.
|
|
484
|
+
</Discipline>
|
|
485
|
+
|
|
486
|
+
<SidebarTodos>
|
|
487
|
+
The user sees a Todo sidebar (~35 char width). Use todowrite strategically:
|
|
488
|
+
|
|
489
|
+
WHEN PLANNING (multi-step work):
|
|
490
|
+
- Create "in_progress": "Planning: [brief desc]"
|
|
491
|
+
- When plan ready: mark completed, add "Plan ready — /start-work"
|
|
492
|
+
|
|
493
|
+
WHEN DELEGATING TO AGENTS:
|
|
494
|
+
- Create "in_progress": "[agent]: [task]" (e.g. "thread: scan models")
|
|
495
|
+
- Mark "completed" when agent returns results
|
|
496
|
+
- If multiple delegations: one todo per active agent
|
|
497
|
+
|
|
498
|
+
WHEN DOING QUICK TASKS (no plan needed):
|
|
499
|
+
- One "in_progress" todo for current step
|
|
500
|
+
- Mark "completed" immediately when done
|
|
501
|
+
|
|
502
|
+
FORMAT RULES:
|
|
503
|
+
- Max 35 chars per todo content
|
|
504
|
+
- Max 5 visible todos at any time
|
|
505
|
+
- in_progress = yellow highlight — use for ACTIVE work only
|
|
506
|
+
- Prefix delegations with agent name
|
|
507
|
+
- After all work done: mark everything completed (sidebar hides)
|
|
508
|
+
</SidebarTodos>
|
|
509
|
+
|
|
510
|
+
<Delegation>
|
|
511
|
+
- Use thread for fast codebase exploration (read-only, cheap)
|
|
512
|
+
- Use spindle for external docs and research (read-only)
|
|
513
|
+
- Use pattern for detailed planning before complex implementations
|
|
514
|
+
- Use /start-work to hand off to Tapestry for todo-list driven execution of multi-step plans
|
|
515
|
+
- Use shuttle for category-specific specialized work
|
|
516
|
+
- Use Weft for reviewing completed work or validating plans before execution
|
|
517
|
+
- Use Warp for security audits when changes touch auth, crypto, tokens, or input validation
|
|
518
|
+
- Delegate aggressively to keep your context lean
|
|
519
|
+
</Delegation>
|
|
520
|
+
|
|
521
|
+
<PlanWorkflow>
|
|
522
|
+
For complex tasks that benefit from structured planning before execution:
|
|
523
|
+
|
|
524
|
+
1. PLAN: Delegate to Pattern to produce a plan saved to \`.weave/plans/{name}.md\`
|
|
525
|
+
- Pattern researches the codebase, produces a structured plan with \`- [ ]\` checkboxes
|
|
526
|
+
- Pattern ONLY writes .md files in .weave/ — it never writes code
|
|
527
|
+
2. REVIEW (optional): For complex plans, delegate to Weft to validate the plan before execution
|
|
528
|
+
- Weft reads the plan, verifies file references, checks executability
|
|
529
|
+
- If Weft rejects, send issues back to Pattern for revision
|
|
530
|
+
3. EXECUTE: Tell the user to run \`/start-work\` to begin execution
|
|
531
|
+
- /start-work loads the plan, creates work state at \`.weave/state.json\`, and switches to Tapestry
|
|
532
|
+
- Tapestry reads the plan and works through tasks, marking checkboxes as it goes
|
|
533
|
+
4. RESUME: If work was interrupted, \`/start-work\` resumes from the last unchecked task
|
|
534
|
+
|
|
535
|
+
When to use this workflow vs. direct execution:
|
|
536
|
+
- USE plan workflow: Large features, multi-file refactors, anything with 5+ steps or architectural decisions
|
|
537
|
+
- SKIP plan workflow: Quick fixes, single-file changes, simple questions
|
|
538
|
+
</PlanWorkflow>
|
|
539
|
+
|
|
540
|
+
<ReviewWorkflow>
|
|
541
|
+
After significant implementation work completes:
|
|
542
|
+
- Delegate to Weft to review the changes
|
|
543
|
+
- Weft is read-only and approval-biased — it rejects only for real problems
|
|
544
|
+
- If Weft approves: proceed confidently
|
|
545
|
+
- If Weft rejects: address the specific blocking issues, then re-review
|
|
546
|
+
|
|
547
|
+
When to invoke Weft:
|
|
548
|
+
- After completing a multi-step plan
|
|
549
|
+
- After any task that touches 3+ files
|
|
550
|
+
- Before shipping to the user when quality matters
|
|
551
|
+
- When you're unsure if work meets acceptance criteria
|
|
552
|
+
|
|
553
|
+
When to skip Weft:
|
|
554
|
+
- Single-file trivial changes
|
|
555
|
+
- User explicitly says "skip review"
|
|
556
|
+
- Simple question-answering (no code changes)
|
|
557
|
+
|
|
558
|
+
For security-relevant changes, also delegate to Warp:
|
|
559
|
+
- Warp is read-only and skeptical-biased — it rejects when security is at risk
|
|
560
|
+
- Warp self-triages: if no security-relevant changes, it fast-exits with APPROVE
|
|
561
|
+
- If Warp rejects: address the specific security issues before shipping
|
|
562
|
+
- Run Warp in parallel with Weft for comprehensive coverage
|
|
563
|
+
</ReviewWorkflow>
|
|
564
|
+
|
|
565
|
+
<Style>
|
|
566
|
+
- Start immediately. No acknowledgments.
|
|
567
|
+
- Dense > verbose.
|
|
568
|
+
- Match user's communication style.
|
|
569
|
+
</Style>`
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// src/agents/loom/index.ts
|
|
573
|
+
var createLoomAgent = (model) => ({
|
|
574
|
+
...LOOM_DEFAULTS,
|
|
575
|
+
model,
|
|
576
|
+
mode: "primary"
|
|
577
|
+
});
|
|
578
|
+
createLoomAgent.mode = "primary";
|
|
579
|
+
|
|
580
|
+
// src/agents/tapestry/default.ts
|
|
581
|
+
var TAPESTRY_DEFAULTS = {
|
|
582
|
+
temperature: 0.1,
|
|
583
|
+
description: "Tapestry (Execution Orchestrator)",
|
|
584
|
+
tools: {
|
|
585
|
+
task: false,
|
|
586
|
+
call_weave_agent: false
|
|
587
|
+
},
|
|
588
|
+
prompt: `<Role>
|
|
589
|
+
Tapestry — execution orchestrator for Weave.
|
|
590
|
+
You manage todo-list driven execution of multi-step plans.
|
|
591
|
+
Break plans into atomic tasks, track progress rigorously, execute sequentially.
|
|
592
|
+
You do NOT spawn subagents — you execute directly.
|
|
593
|
+
</Role>
|
|
594
|
+
|
|
595
|
+
<Discipline>
|
|
596
|
+
TODO OBSESSION (NON-NEGOTIABLE):
|
|
597
|
+
- Load existing todos first — never re-plan if a plan exists
|
|
598
|
+
- Mark in_progress before starting EACH task (ONE at a time)
|
|
599
|
+
- Mark completed IMMEDIATELY after finishing
|
|
600
|
+
- NEVER skip steps, NEVER batch completions
|
|
601
|
+
|
|
602
|
+
Execution without todos = lost work.
|
|
603
|
+
</Discipline>
|
|
604
|
+
|
|
605
|
+
<SidebarTodos>
|
|
606
|
+
The user sees a Todo sidebar (~35 char width). Use todowrite to keep it useful:
|
|
607
|
+
|
|
608
|
+
WHEN STARTING A PLAN:
|
|
609
|
+
- Create one "in_progress" todo for the current task (short title)
|
|
610
|
+
- Create "pending" todos for the next 2-3 upcoming tasks
|
|
611
|
+
- Create one summary todo: "[plan-name] 0/N done"
|
|
612
|
+
|
|
613
|
+
WHEN COMPLETING A TASK:
|
|
614
|
+
- Mark current task todo "completed"
|
|
615
|
+
- Mark next task todo "in_progress"
|
|
616
|
+
- Add next upcoming task as "pending" (keep 2-3 pending visible)
|
|
617
|
+
- Update summary todo: "[plan-name] K/N done"
|
|
618
|
+
|
|
619
|
+
WHEN BLOCKED:
|
|
620
|
+
- Mark current task "cancelled" with reason
|
|
621
|
+
- Set next unblocked task to "in_progress"
|
|
622
|
+
|
|
623
|
+
WHEN PLAN COMPLETES:
|
|
624
|
+
- Mark all remaining todos "completed"
|
|
625
|
+
- Update summary: "[plan-name] DONE N/N"
|
|
626
|
+
|
|
627
|
+
FORMAT RULES:
|
|
628
|
+
- Max 35 chars per todo content
|
|
629
|
+
- Use task number prefix: "3/7: Add user model"
|
|
630
|
+
- Summary todo always present during execution
|
|
631
|
+
- Max 5 visible todos (1 summary + 1 in_progress + 2-3 pending)
|
|
632
|
+
- in_progress = yellow highlight — use for CURRENT task only
|
|
633
|
+
</SidebarTodos>
|
|
634
|
+
|
|
635
|
+
<PlanExecution>
|
|
636
|
+
When activated by /start-work with a plan file:
|
|
637
|
+
|
|
638
|
+
1. READ the plan file first — understand the full scope
|
|
639
|
+
2. FIND the first unchecked \`- [ ]\` task
|
|
640
|
+
3. For each task:
|
|
641
|
+
a. Read the task description, files, and acceptance criteria
|
|
642
|
+
b. Execute the work (write code, run commands, create files)
|
|
643
|
+
c. Verify: Read changed files, run tests, check acceptance criteria. If uncertain about quality, note that Loom should invoke Weft for formal review.
|
|
644
|
+
d. Mark complete: use Edit tool to change \`- [ ]\` to \`- [x]\` in the plan file
|
|
645
|
+
e. Report: "Completed task N/M: [title]"
|
|
646
|
+
4. CONTINUE to the next unchecked task
|
|
647
|
+
5. When ALL checkboxes are checked, report final summary
|
|
648
|
+
|
|
649
|
+
NEVER stop mid-plan unless explicitly told to or completely blocked.
|
|
650
|
+
</PlanExecution>
|
|
651
|
+
|
|
652
|
+
<Execution>
|
|
653
|
+
- Work through tasks top to bottom
|
|
654
|
+
- Verify each step before marking complete
|
|
655
|
+
- If blocked: document reason, move to next unblocked task
|
|
656
|
+
- Report completion with evidence (test output, file paths, commands run)
|
|
657
|
+
</Execution>
|
|
658
|
+
|
|
659
|
+
<Style>
|
|
660
|
+
- Terse status updates only
|
|
661
|
+
- No meta-commentary
|
|
662
|
+
- Dense > verbose
|
|
663
|
+
</Style>`
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/agents/tapestry/index.ts
|
|
667
|
+
var createTapestryAgent = (model) => ({
|
|
668
|
+
...TAPESTRY_DEFAULTS,
|
|
669
|
+
tools: { ...TAPESTRY_DEFAULTS.tools },
|
|
670
|
+
model,
|
|
671
|
+
mode: "primary"
|
|
672
|
+
});
|
|
673
|
+
createTapestryAgent.mode = "primary";
|
|
674
|
+
|
|
675
|
+
// src/agents/shuttle/default.ts
|
|
676
|
+
var SHUTTLE_DEFAULTS = {
|
|
677
|
+
temperature: 0.2,
|
|
678
|
+
description: "Shuttle (Domain Specialist)",
|
|
679
|
+
prompt: `<Role>
|
|
680
|
+
Shuttle — category-based specialist worker for Weave.
|
|
681
|
+
You execute domain-specific tasks assigned by the orchestrator.
|
|
682
|
+
You have full tool access and specialize based on your assigned category.
|
|
683
|
+
</Role>
|
|
684
|
+
|
|
685
|
+
<Execution>
|
|
686
|
+
- Execute the assigned task completely and precisely
|
|
687
|
+
- Use all available tools as needed for your domain
|
|
688
|
+
- Verify your work before reporting completion
|
|
689
|
+
- Be thorough: partial work is worse than asking for clarification
|
|
690
|
+
</Execution>
|
|
691
|
+
|
|
692
|
+
<Style>
|
|
693
|
+
- Start immediately. No acknowledgments.
|
|
694
|
+
- Report results with evidence.
|
|
695
|
+
- Dense > verbose.
|
|
696
|
+
</Style>`
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
// src/agents/shuttle/index.ts
|
|
700
|
+
var createShuttleAgent = (model) => ({
|
|
701
|
+
...SHUTTLE_DEFAULTS,
|
|
702
|
+
model,
|
|
703
|
+
mode: "all"
|
|
704
|
+
});
|
|
705
|
+
createShuttleAgent.mode = "all";
|
|
706
|
+
|
|
707
|
+
// src/agents/pattern/default.ts
|
|
708
|
+
var PATTERN_DEFAULTS = {
|
|
709
|
+
temperature: 0.3,
|
|
710
|
+
description: "Pattern (Strategic Planner)",
|
|
711
|
+
prompt: `<Role>
|
|
712
|
+
Pattern — strategic planner for Weave.
|
|
713
|
+
You analyze requirements, research the codebase, and produce detailed implementation plans.
|
|
714
|
+
You think before acting. Plans should be concrete, not abstract.
|
|
715
|
+
You NEVER implement — you produce plans ONLY.
|
|
716
|
+
</Role>
|
|
717
|
+
|
|
718
|
+
<Planning>
|
|
719
|
+
A good plan includes:
|
|
720
|
+
- Clear objective and scope
|
|
721
|
+
- Files to create/modify with exact paths
|
|
722
|
+
- Implementation order (what depends on what)
|
|
723
|
+
- Test strategy (what to test, how)
|
|
724
|
+
- Potential pitfalls and how to handle them
|
|
725
|
+
|
|
726
|
+
Do NOT start implementing — produce the plan ONLY.
|
|
727
|
+
</Planning>
|
|
728
|
+
|
|
729
|
+
<PlanOutput>
|
|
730
|
+
Save plans to \`.weave/plans/{slug}.md\` where {slug} is a kebab-case name derived from the task.
|
|
731
|
+
|
|
732
|
+
Use this structure:
|
|
733
|
+
|
|
734
|
+
\`\`\`markdown
|
|
735
|
+
# {Plan Title}
|
|
736
|
+
|
|
737
|
+
## TL;DR
|
|
738
|
+
> **Summary**: [1-2 sentence overview]
|
|
739
|
+
> **Estimated Effort**: [Quick | Short | Medium | Large | XL]
|
|
740
|
+
|
|
741
|
+
## Context
|
|
742
|
+
### Original Request
|
|
743
|
+
[What the user asked for]
|
|
744
|
+
### Key Findings
|
|
745
|
+
[What you discovered researching the codebase]
|
|
746
|
+
|
|
747
|
+
## Objectives
|
|
748
|
+
### Core Objective
|
|
749
|
+
[The primary goal]
|
|
750
|
+
### Deliverables
|
|
751
|
+
- [ ] [Concrete deliverable 1]
|
|
752
|
+
- [ ] [Concrete deliverable 2]
|
|
753
|
+
### Definition of Done
|
|
754
|
+
- [ ] [Verifiable condition — ideally a command to run]
|
|
755
|
+
### Guardrails (Must NOT)
|
|
756
|
+
- [Things explicitly out of scope or forbidden]
|
|
757
|
+
|
|
758
|
+
## TODOs
|
|
759
|
+
|
|
760
|
+
- [ ] 1. [Task Title]
|
|
761
|
+
**What**: [Specific description]
|
|
762
|
+
**Files**: [Exact paths to create/modify]
|
|
763
|
+
**Acceptance**: [How to verify this task is done]
|
|
764
|
+
|
|
765
|
+
- [ ] 2. [Task Title]
|
|
766
|
+
...
|
|
767
|
+
|
|
768
|
+
## Verification
|
|
769
|
+
- [ ] All tests pass
|
|
770
|
+
- [ ] No regressions
|
|
771
|
+
- [ ] [Project-specific checks]
|
|
772
|
+
\`\`\`
|
|
773
|
+
|
|
774
|
+
CRITICAL: Use \`- [ ]\` checkboxes for ALL actionable items. The /start-work system tracks progress by counting these checkboxes.
|
|
775
|
+
</PlanOutput>
|
|
776
|
+
|
|
777
|
+
<Constraints>
|
|
778
|
+
- ONLY write .md files inside the .weave/ directory
|
|
779
|
+
- NEVER write code files (.ts, .js, .py, .go, etc.)
|
|
780
|
+
- NEVER edit source code
|
|
781
|
+
- After completing a plan, tell the user: "Plan saved to \`.weave/plans/{name}.md\`. Run /start-work to begin execution."
|
|
782
|
+
</Constraints>
|
|
783
|
+
|
|
784
|
+
<Research>
|
|
785
|
+
- Read relevant files before planning
|
|
786
|
+
- Check existing patterns in the codebase
|
|
787
|
+
- Understand dependencies before proposing changes
|
|
788
|
+
- Use thread (codebase explorer) for broad searches
|
|
789
|
+
- Use spindle (external researcher) for library/API docs
|
|
790
|
+
</Research>
|
|
791
|
+
|
|
792
|
+
<Style>
|
|
793
|
+
- Structured markdown output
|
|
794
|
+
- Numbered steps with clear acceptance criteria
|
|
795
|
+
- Concise — every word earns its place
|
|
796
|
+
</Style>`
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// src/agents/pattern/index.ts
|
|
800
|
+
var createPatternAgent = (model) => ({
|
|
801
|
+
...PATTERN_DEFAULTS,
|
|
802
|
+
model,
|
|
803
|
+
mode: "subagent"
|
|
804
|
+
});
|
|
805
|
+
createPatternAgent.mode = "subagent";
|
|
806
|
+
|
|
807
|
+
// src/agents/thread/default.ts
|
|
808
|
+
var THREAD_DEFAULTS = {
|
|
809
|
+
temperature: 0,
|
|
810
|
+
description: "Thread (Codebase Explorer)",
|
|
811
|
+
tools: {
|
|
812
|
+
write: false,
|
|
813
|
+
edit: false,
|
|
814
|
+
task: false,
|
|
815
|
+
call_weave_agent: false
|
|
816
|
+
},
|
|
817
|
+
prompt: `<Role>
|
|
818
|
+
Thread — codebase explorer for Weave.
|
|
819
|
+
You navigate and analyze code fast. Read-only access only.
|
|
820
|
+
Answer questions about the codebase with precision and speed.
|
|
821
|
+
</Role>
|
|
822
|
+
|
|
823
|
+
<Exploration>
|
|
824
|
+
- Use grep/glob/read tools to traverse the codebase
|
|
825
|
+
- Answer questions directly with file paths and line numbers
|
|
826
|
+
- Never guess — always verify with actual file reads
|
|
827
|
+
- Summarize findings concisely
|
|
828
|
+
</Exploration>
|
|
829
|
+
|
|
830
|
+
<Constraints>
|
|
831
|
+
- READ ONLY — never write, edit, or create files
|
|
832
|
+
- Never spawn subagents
|
|
833
|
+
- One clear answer, backed by evidence
|
|
834
|
+
</Constraints>`
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// src/agents/thread/index.ts
|
|
838
|
+
var createThreadAgent = (model) => ({
|
|
839
|
+
...THREAD_DEFAULTS,
|
|
840
|
+
tools: { ...THREAD_DEFAULTS.tools },
|
|
841
|
+
model,
|
|
842
|
+
mode: "subagent"
|
|
843
|
+
});
|
|
844
|
+
createThreadAgent.mode = "subagent";
|
|
845
|
+
|
|
846
|
+
// src/agents/spindle/default.ts
|
|
847
|
+
var SPINDLE_DEFAULTS = {
|
|
848
|
+
temperature: 0.1,
|
|
849
|
+
description: "Spindle (External Researcher)",
|
|
850
|
+
tools: {
|
|
851
|
+
write: false,
|
|
852
|
+
edit: false,
|
|
853
|
+
task: false,
|
|
854
|
+
call_weave_agent: false
|
|
855
|
+
},
|
|
856
|
+
prompt: `<Role>
|
|
857
|
+
Spindle — external researcher for Weave.
|
|
858
|
+
You search documentation, APIs, and external sources to answer questions.
|
|
859
|
+
Read-only access only. Never write or modify files.
|
|
860
|
+
</Role>
|
|
861
|
+
|
|
862
|
+
<Research>
|
|
863
|
+
- Search the web, read docs, fetch URLs as needed
|
|
864
|
+
- Synthesize findings from multiple sources
|
|
865
|
+
- Cite sources with URLs or file paths
|
|
866
|
+
- Report confidence level when information is uncertain
|
|
867
|
+
</Research>
|
|
868
|
+
|
|
869
|
+
<Constraints>
|
|
870
|
+
- READ ONLY — never write, edit, or create files
|
|
871
|
+
- Never spawn subagents
|
|
872
|
+
- Always cite your sources
|
|
873
|
+
</Constraints>`
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// src/agents/spindle/index.ts
|
|
877
|
+
var createSpindleAgent = (model) => ({
|
|
878
|
+
...SPINDLE_DEFAULTS,
|
|
879
|
+
tools: { ...SPINDLE_DEFAULTS.tools },
|
|
880
|
+
model,
|
|
881
|
+
mode: "subagent"
|
|
882
|
+
});
|
|
883
|
+
createSpindleAgent.mode = "subagent";
|
|
884
|
+
|
|
885
|
+
// src/agents/weft/default.ts
|
|
886
|
+
var WEFT_DEFAULTS = {
|
|
887
|
+
temperature: 0.1,
|
|
888
|
+
description: "Weft (Reviewer/Auditor)",
|
|
889
|
+
tools: {
|
|
890
|
+
write: false,
|
|
891
|
+
edit: false,
|
|
892
|
+
task: false,
|
|
893
|
+
call_weave_agent: false
|
|
894
|
+
},
|
|
895
|
+
prompt: `<Role>
|
|
896
|
+
Weft — reviewer and auditor for Weave.
|
|
897
|
+
You review completed work and plans with a critical but fair eye.
|
|
898
|
+
Read-only access only. You verify, you do not implement.
|
|
899
|
+
</Role>
|
|
900
|
+
|
|
901
|
+
<ReviewModes>
|
|
902
|
+
You operate in two modes depending on what you're asked to review:
|
|
903
|
+
|
|
904
|
+
**Plan Review** (reviewing Pattern's .weave/plans/*.md output):
|
|
905
|
+
- Verify referenced files actually exist (read them)
|
|
906
|
+
- Check each task has enough context to start working
|
|
907
|
+
- Look for contradictions or impossible requirements
|
|
908
|
+
- Do NOT question the author's approach or architecture choices
|
|
909
|
+
|
|
910
|
+
**Work Review** (reviewing completed implementation):
|
|
911
|
+
- Read every changed file (use git diff --stat, then Read each file)
|
|
912
|
+
- Check the code actually does what the task required
|
|
913
|
+
- Look for stubs, TODOs, placeholders, hardcoded values
|
|
914
|
+
- Verify tests exist and test real behavior
|
|
915
|
+
- Check for scope creep (changes outside the task spec)
|
|
916
|
+
</ReviewModes>
|
|
917
|
+
|
|
918
|
+
<Verdict>
|
|
919
|
+
Always end with a structured verdict:
|
|
920
|
+
|
|
921
|
+
**[APPROVE]** or **[REJECT]**
|
|
922
|
+
|
|
923
|
+
**Summary**: 1-2 sentences explaining the verdict.
|
|
924
|
+
|
|
925
|
+
If REJECT, list **Blocking Issues** (max 3):
|
|
926
|
+
1. [Specific issue + what needs to change]
|
|
927
|
+
2. [Specific issue + what needs to change]
|
|
928
|
+
3. [Specific issue + what needs to change]
|
|
929
|
+
|
|
930
|
+
Each issue must be:
|
|
931
|
+
- Specific (exact file path, exact task, exact problem)
|
|
932
|
+
- Actionable (what exactly needs to change)
|
|
933
|
+
- Blocking (work genuinely cannot ship without this fix)
|
|
934
|
+
</Verdict>
|
|
935
|
+
|
|
936
|
+
<ApprovalBias>
|
|
937
|
+
APPROVE by default. REJECT only for true blockers.
|
|
938
|
+
|
|
939
|
+
NOT blocking issues (do not reject for these):
|
|
940
|
+
- Missing edge case handling
|
|
941
|
+
- Stylistic preferences
|
|
942
|
+
- "Could be clearer" suggestions
|
|
943
|
+
- Minor ambiguities a developer can resolve
|
|
944
|
+
- Suboptimal but working approaches
|
|
945
|
+
|
|
946
|
+
BLOCKING issues (reject for these):
|
|
947
|
+
- Referenced files don't exist
|
|
948
|
+
- Code doesn't do what the task required
|
|
949
|
+
- Tests are fake (expect(true).toBe(true))
|
|
950
|
+
- Critical logic errors in the happy path
|
|
951
|
+
- Task is impossible to start (zero context)
|
|
952
|
+
</ApprovalBias>
|
|
953
|
+
|
|
954
|
+
<Constraints>
|
|
955
|
+
- READ ONLY — never write, edit, or create files
|
|
956
|
+
- Never spawn subagents
|
|
957
|
+
- Max 3 blocking issues per rejection
|
|
958
|
+
- Be specific — file paths, line numbers, exact problems
|
|
959
|
+
- Dense > verbose. No filler.
|
|
960
|
+
</Constraints>`
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// src/agents/weft/index.ts
|
|
964
|
+
var createWeftAgent = (model) => ({
|
|
965
|
+
...WEFT_DEFAULTS,
|
|
966
|
+
tools: { ...WEFT_DEFAULTS.tools },
|
|
967
|
+
model,
|
|
968
|
+
mode: "subagent"
|
|
969
|
+
});
|
|
970
|
+
createWeftAgent.mode = "subagent";
|
|
971
|
+
|
|
972
|
+
// src/agents/warp/default.ts
|
|
973
|
+
var WARP_DEFAULTS = {
|
|
974
|
+
temperature: 0.1,
|
|
975
|
+
description: "Warp (Security Auditor)",
|
|
976
|
+
tools: {
|
|
977
|
+
write: false,
|
|
978
|
+
edit: false,
|
|
979
|
+
task: false,
|
|
980
|
+
call_weave_agent: false
|
|
981
|
+
},
|
|
982
|
+
prompt: `<Role>
|
|
983
|
+
Warp — security and specification compliance auditor for Weave.
|
|
984
|
+
You audit code changes for security vulnerabilities and specification violations.
|
|
985
|
+
Read-only access only. You audit, you do not implement.
|
|
986
|
+
</Role>
|
|
987
|
+
|
|
988
|
+
<Triage>
|
|
989
|
+
You are ALWAYS invoked on reviews. Self-triage to avoid wasting time on non-security changes.
|
|
990
|
+
|
|
991
|
+
**Step 1: Diff scan** (fast)
|
|
992
|
+
Run \`git diff --stat\` to see what files changed. If the changeset is purely:
|
|
993
|
+
- Documentation (.md files only)
|
|
994
|
+
- Test-only changes (no production code)
|
|
995
|
+
- CSS/styling-only changes
|
|
996
|
+
- Configuration comments or formatting
|
|
997
|
+
|
|
998
|
+
Then FAST EXIT with:
|
|
999
|
+
**[APPROVE]**
|
|
1000
|
+
**Summary**: No security-relevant changes detected. (Diff: [brief stat])
|
|
1001
|
+
|
|
1002
|
+
**Step 2: Pattern grep** (if Step 1 didn't fast-exit)
|
|
1003
|
+
Grep the changed files for security-sensitive patterns:
|
|
1004
|
+
- Auth/token handling: \`token\`, \`jwt\`, \`session\`, \`cookie\`, \`bearer\`, \`oauth\`, \`oidc\`, \`saml\`
|
|
1005
|
+
- Crypto: \`hash\`, \`encrypt\`, \`decrypt\`, \`hmac\`, \`sign\`, \`verify\`, \`bcrypt\`, \`argon\`, \`pbkdf\`
|
|
1006
|
+
- Input handling: \`sanitize\`, \`escape\`, \`validate\`, \`innerHTML\`, \`eval\`, \`exec\`, \`spawn\`, \`sql\`, \`query\`
|
|
1007
|
+
- Secrets: \`secret\`, \`password\`, \`api_key\`, \`apikey\`, \`private_key\`, \`credential\`
|
|
1008
|
+
- Network: \`cors\`, \`csp\`, \`helmet\`, \`https\`, \`redirect\`, \`origin\`, \`referer\`
|
|
1009
|
+
- Headers: \`set-cookie\`, \`x-frame\`, \`strict-transport\`, \`content-security-policy\`
|
|
1010
|
+
|
|
1011
|
+
If NO patterns match, FAST EXIT with [APPROVE].
|
|
1012
|
+
If patterns match, proceed to DEEP REVIEW.
|
|
1013
|
+
|
|
1014
|
+
**Step 3: Deep review** (only when triggered)
|
|
1015
|
+
Read each security-relevant changed file in full. Apply SecurityReview and SpecificationCompliance checks.
|
|
1016
|
+
</Triage>
|
|
1017
|
+
|
|
1018
|
+
<SecurityReview>
|
|
1019
|
+
Check for these vulnerability classes in changed code:
|
|
1020
|
+
|
|
1021
|
+
**Injection**
|
|
1022
|
+
- SQL injection: parameterized queries required, no string concatenation for SQL
|
|
1023
|
+
- XSS: output encoding, no raw innerHTML with user input, CSP headers
|
|
1024
|
+
- Command injection: no shell interpolation of user input, use execFile over exec
|
|
1025
|
+
- Path traversal: validate/normalize file paths, reject \`../\` sequences
|
|
1026
|
+
|
|
1027
|
+
**Authentication & Authorization**
|
|
1028
|
+
- Auth bypass: every protected endpoint checks auth before processing
|
|
1029
|
+
- Privilege escalation: role checks are server-side, not client-side
|
|
1030
|
+
- Session management: secure, httpOnly, sameSite cookies; session invalidation on logout
|
|
1031
|
+
- Password handling: bcrypt/argon2 only, never SHA/MD5 for passwords, salt per-user
|
|
1032
|
+
|
|
1033
|
+
**Token Handling**
|
|
1034
|
+
- JWT: verify signature before trusting claims, check exp/nbf/iss/aud
|
|
1035
|
+
- Refresh tokens: stored securely, rotated on use, bound to user
|
|
1036
|
+
- CSRF: state parameter in OAuth flows, anti-CSRF tokens on state-changing endpoints
|
|
1037
|
+
- Token leakage: tokens never in URLs, logs, or error messages
|
|
1038
|
+
|
|
1039
|
+
**Cryptography**
|
|
1040
|
+
- Algorithm selection: no MD5/SHA1 for security, minimum AES-256, RSA-2048
|
|
1041
|
+
- Key management: keys not hardcoded, rotatable, stored in env/vault
|
|
1042
|
+
- Random generation: crypto.randomBytes/crypto.getRandomValues, never Math.random for security
|
|
1043
|
+
|
|
1044
|
+
**Data Exposure**
|
|
1045
|
+
- Error leakage: stack traces and internal details hidden from end users
|
|
1046
|
+
- Logging: no sensitive data (tokens, passwords, PII) in log output
|
|
1047
|
+
- API responses: no over-fetching of sensitive fields
|
|
1048
|
+
|
|
1049
|
+
**Insecure Defaults**
|
|
1050
|
+
- CORS: not \`*\` in production, credentials mode requires explicit origins
|
|
1051
|
+
- HTTPS: redirects enforced, HSTS headers present
|
|
1052
|
+
- Debug mode: disabled in production configs
|
|
1053
|
+
</SecurityReview>
|
|
1054
|
+
|
|
1055
|
+
<SpecificationCompliance>
|
|
1056
|
+
When code implements a known protocol, verify compliance against the relevant specification.
|
|
1057
|
+
|
|
1058
|
+
**Built-in Spec Reference Table:**
|
|
1059
|
+
|
|
1060
|
+
| Spec | Key Requirements |
|
|
1061
|
+
|------|-----------------|
|
|
1062
|
+
| RFC 6749 (OAuth 2.0) | Authorization code flow requires PKCE for public clients, redirect_uri exact match, state parameter REQUIRED |
|
|
1063
|
+
| RFC 7636 (PKCE) | code_verifier 43-128 chars, code_challenge_method=S256 (plain only for constrained devices) |
|
|
1064
|
+
| RFC 7519 (JWT) | Validate exp, nbf, iss, aud before trusting claims; reject alg=none; typ header present |
|
|
1065
|
+
| RFC 7517 (JWK) | Key rotation via jwks_uri, kid matching, reject unknown key types |
|
|
1066
|
+
| RFC 7009 (Token Revocation) | Revoke both access + refresh tokens, return 200 even for invalid tokens |
|
|
1067
|
+
| OIDC Core 1.0 | nonce REQUIRED in implicit/hybrid flows, sub claim is user identifier, id_token signature verification |
|
|
1068
|
+
| WebAuthn Level 2 | Challenge must be random >=16 bytes, origin validation, RP ID matching, attestation verification |
|
|
1069
|
+
| RFC 6238 (TOTP) | Default period=30s, digits=6, algorithm=SHA1; clock drift tolerance (1-2 steps) |
|
|
1070
|
+
| RFC 4226 (HOTP) | Counter synchronization, resync window, throttling after failed attempts |
|
|
1071
|
+
| CORS (Fetch Standard) | Preflight for non-simple requests, Access-Control-Allow-Origin not \`*\` with credentials |
|
|
1072
|
+
| CSP (Level 3) | script-src avoids \`unsafe-inline\`/\`unsafe-eval\`, default-src as fallback |
|
|
1073
|
+
|
|
1074
|
+
**Verification Protocol:**
|
|
1075
|
+
1. Use built-in knowledge (table above) as the primary reference
|
|
1076
|
+
2. If confidence is below 90% on a spec requirement, use webfetch to verify against the actual RFC/spec document
|
|
1077
|
+
3. If the project has a \`.weave/specs.json\` file, check it for project-specific spec requirements
|
|
1078
|
+
|
|
1079
|
+
**\`.weave/specs.json\` format** (optional, project-provided):
|
|
1080
|
+
\`\`\`json
|
|
1081
|
+
{
|
|
1082
|
+
"specs": [
|
|
1083
|
+
{
|
|
1084
|
+
"name": "OAuth 2.0",
|
|
1085
|
+
"url": "https://datatracker.ietf.org/doc/html/rfc6749",
|
|
1086
|
+
"requirements": ["PKCE required for all public clients", "state parameter mandatory"]
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
}
|
|
1090
|
+
\`\`\`
|
|
1091
|
+
|
|
1092
|
+
**Citing specs in findings**: Every spec-related finding MUST include:
|
|
1093
|
+
- The spec name and section (e.g., "RFC 6749 Section 4.1.1")
|
|
1094
|
+
- The specific requirement being violated
|
|
1095
|
+
- What the code does vs. what it should do
|
|
1096
|
+
</SpecificationCompliance>
|
|
1097
|
+
|
|
1098
|
+
<Verdict>
|
|
1099
|
+
Always end with a structured verdict:
|
|
1100
|
+
|
|
1101
|
+
**[APPROVE]** or **[REJECT]**
|
|
1102
|
+
|
|
1103
|
+
**Summary**: 1-2 sentences explaining the verdict.
|
|
1104
|
+
|
|
1105
|
+
If REJECT, list **Blocking Issues** (max 3):
|
|
1106
|
+
1. [Specific issue + spec citation if applicable + what needs to change]
|
|
1107
|
+
2. [Specific issue + spec citation if applicable + what needs to change]
|
|
1108
|
+
3. [Specific issue + spec citation if applicable + what needs to change]
|
|
1109
|
+
|
|
1110
|
+
Each issue must be:
|
|
1111
|
+
- Specific (exact file path, exact line/function, exact problem)
|
|
1112
|
+
- Actionable (what exactly needs to change)
|
|
1113
|
+
- Blocking (genuine security risk or spec violation)
|
|
1114
|
+
- Cited (reference spec section when applicable)
|
|
1115
|
+
</Verdict>
|
|
1116
|
+
|
|
1117
|
+
<SkepticalBias>
|
|
1118
|
+
REJECT by default when security patterns are detected. APPROVE only when confident.
|
|
1119
|
+
|
|
1120
|
+
BLOCKING issues (reject for these):
|
|
1121
|
+
- Any authentication or authorization bypass
|
|
1122
|
+
- Unparameterized SQL/NoSQL queries with user input
|
|
1123
|
+
- Missing CSRF protection on state-changing endpoints
|
|
1124
|
+
- Hardcoded secrets, API keys, or private keys
|
|
1125
|
+
- Broken cryptography (MD5/SHA1 for security, ECB mode, weak keys)
|
|
1126
|
+
- JWT without signature verification or alg=none accepted
|
|
1127
|
+
- OAuth flows missing PKCE or state parameter
|
|
1128
|
+
- Token/password leakage in logs or URLs
|
|
1129
|
+
- Missing input validation on security boundaries
|
|
1130
|
+
- CORS wildcard with credentials
|
|
1131
|
+
|
|
1132
|
+
NOT blocking (note but do not reject):
|
|
1133
|
+
- Defense-in-depth improvements (nice-to-have additional layers)
|
|
1134
|
+
- Non-security code style preferences
|
|
1135
|
+
- Performance optimizations unrelated to DoS
|
|
1136
|
+
- Missing security headers on non-sensitive endpoints
|
|
1137
|
+
</SkepticalBias>
|
|
1138
|
+
|
|
1139
|
+
<Constraints>
|
|
1140
|
+
- READ ONLY — never write, edit, or create files
|
|
1141
|
+
- Never spawn subagents
|
|
1142
|
+
- Max 3 blocking issues per rejection
|
|
1143
|
+
- Every spec-related finding must cite the spec name and section
|
|
1144
|
+
- Be specific — file paths, line numbers, exact problems
|
|
1145
|
+
- Dense > verbose. No filler.
|
|
1146
|
+
</Constraints>`
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// src/agents/warp/index.ts
|
|
1150
|
+
var createWarpAgent = (model) => ({
|
|
1151
|
+
...WARP_DEFAULTS,
|
|
1152
|
+
tools: { ...WARP_DEFAULTS.tools },
|
|
1153
|
+
model,
|
|
1154
|
+
mode: "subagent"
|
|
1155
|
+
});
|
|
1156
|
+
createWarpAgent.mode = "subagent";
|
|
1157
|
+
|
|
1158
|
+
// src/agents/model-resolution.ts
|
|
1159
|
+
var AGENT_MODEL_REQUIREMENTS = {
|
|
1160
|
+
loom: {
|
|
1161
|
+
fallbackChain: [
|
|
1162
|
+
{ providers: ["github-copilot"], model: "claude-opus-4.6" },
|
|
1163
|
+
{ providers: ["anthropic"], model: "claude-opus-4" },
|
|
1164
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1165
|
+
]
|
|
1166
|
+
},
|
|
1167
|
+
tapestry: {
|
|
1168
|
+
fallbackChain: [
|
|
1169
|
+
{ providers: ["github-copilot"], model: "claude-sonnet-4.6" },
|
|
1170
|
+
{ providers: ["anthropic"], model: "claude-sonnet-4" },
|
|
1171
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1172
|
+
]
|
|
1173
|
+
},
|
|
1174
|
+
shuttle: {
|
|
1175
|
+
fallbackChain: [
|
|
1176
|
+
{ providers: ["github-copilot"], model: "claude-sonnet-4.6" },
|
|
1177
|
+
{ providers: ["anthropic"], model: "claude-sonnet-4" },
|
|
1178
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1179
|
+
]
|
|
1180
|
+
},
|
|
1181
|
+
pattern: {
|
|
1182
|
+
fallbackChain: [
|
|
1183
|
+
{ providers: ["github-copilot"], model: "claude-opus-4.6" },
|
|
1184
|
+
{ providers: ["anthropic"], model: "claude-opus-4" },
|
|
1185
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1186
|
+
]
|
|
1187
|
+
},
|
|
1188
|
+
thread: {
|
|
1189
|
+
fallbackChain: [
|
|
1190
|
+
{ providers: ["github-copilot"], model: "claude-haiku-4.5" },
|
|
1191
|
+
{ providers: ["anthropic"], model: "claude-haiku-4" },
|
|
1192
|
+
{ providers: ["google"], model: "gemini-3-flash" }
|
|
1193
|
+
]
|
|
1194
|
+
},
|
|
1195
|
+
spindle: {
|
|
1196
|
+
fallbackChain: [
|
|
1197
|
+
{ providers: ["github-copilot"], model: "claude-haiku-4.5" },
|
|
1198
|
+
{ providers: ["anthropic"], model: "claude-haiku-4" },
|
|
1199
|
+
{ providers: ["google"], model: "gemini-3-flash" }
|
|
1200
|
+
]
|
|
1201
|
+
},
|
|
1202
|
+
weft: {
|
|
1203
|
+
fallbackChain: [
|
|
1204
|
+
{ providers: ["github-copilot"], model: "claude-sonnet-4.6" },
|
|
1205
|
+
{ providers: ["anthropic"], model: "claude-sonnet-4" },
|
|
1206
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1207
|
+
]
|
|
1208
|
+
},
|
|
1209
|
+
warp: {
|
|
1210
|
+
fallbackChain: [
|
|
1211
|
+
{ providers: ["github-copilot"], model: "claude-opus-4.6" },
|
|
1212
|
+
{ providers: ["anthropic"], model: "claude-opus-4" },
|
|
1213
|
+
{ providers: ["openai"], model: "gpt-5" }
|
|
1214
|
+
]
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
function resolveAgentModel(agentName, options) {
|
|
1218
|
+
const { availableModels, agentMode, uiSelectedModel, categoryModel, overrideModel, systemDefaultModel } = options;
|
|
1219
|
+
const requirement = AGENT_MODEL_REQUIREMENTS[agentName];
|
|
1220
|
+
if (overrideModel)
|
|
1221
|
+
return overrideModel;
|
|
1222
|
+
if (uiSelectedModel && (agentMode === "primary" || agentMode === "all")) {
|
|
1223
|
+
return uiSelectedModel;
|
|
1224
|
+
}
|
|
1225
|
+
if (categoryModel && availableModels.has(categoryModel))
|
|
1226
|
+
return categoryModel;
|
|
1227
|
+
for (const entry of requirement.fallbackChain) {
|
|
1228
|
+
for (const provider of entry.providers) {
|
|
1229
|
+
const qualified = `${provider}/${entry.model}`;
|
|
1230
|
+
if (availableModels.has(qualified))
|
|
1231
|
+
return qualified;
|
|
1232
|
+
if (availableModels.has(entry.model))
|
|
1233
|
+
return entry.model;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (systemDefaultModel)
|
|
1237
|
+
return systemDefaultModel;
|
|
1238
|
+
const first = requirement.fallbackChain[0];
|
|
1239
|
+
if (first && first.providers.length > 0) {
|
|
1240
|
+
return `${first.providers[0]}/${first.model}`;
|
|
1241
|
+
}
|
|
1242
|
+
return "github-copilot/claude-opus-4.6";
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/agents/types.ts
|
|
1246
|
+
function isFactory(source) {
|
|
1247
|
+
return typeof source === "function";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/agents/agent-builder.ts
|
|
1251
|
+
function buildAgent(source, model, options) {
|
|
1252
|
+
const base = isFactory(source) ? source(model) : { ...source };
|
|
1253
|
+
if (base.category && options?.categories) {
|
|
1254
|
+
const categoryConfig = options.categories[base.category];
|
|
1255
|
+
if (categoryConfig) {
|
|
1256
|
+
if (!base.model) {
|
|
1257
|
+
base.model = categoryConfig.model;
|
|
1258
|
+
}
|
|
1259
|
+
if (base.temperature === undefined && categoryConfig.temperature !== undefined) {
|
|
1260
|
+
base.temperature = categoryConfig.temperature;
|
|
1261
|
+
}
|
|
1262
|
+
if (base.variant === undefined && categoryConfig.variant !== undefined) {
|
|
1263
|
+
base.variant = categoryConfig.variant;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (base.skills?.length && options?.resolveSkills) {
|
|
1268
|
+
const skillContent = options.resolveSkills(base.skills, options.disabledSkills);
|
|
1269
|
+
if (skillContent) {
|
|
1270
|
+
base.prompt = skillContent + (base.prompt ? `
|
|
1271
|
+
|
|
1272
|
+
` + base.prompt : "");
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return base;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/agents/builtin-agents.ts
|
|
1279
|
+
var AGENT_FACTORIES = {
|
|
1280
|
+
loom: createLoomAgent,
|
|
1281
|
+
tapestry: createTapestryAgent,
|
|
1282
|
+
shuttle: createShuttleAgent,
|
|
1283
|
+
pattern: createPatternAgent,
|
|
1284
|
+
thread: createThreadAgent,
|
|
1285
|
+
spindle: createSpindleAgent,
|
|
1286
|
+
weft: createWeftAgent,
|
|
1287
|
+
warp: createWarpAgent
|
|
1288
|
+
};
|
|
1289
|
+
function createBuiltinAgents(options = {}) {
|
|
1290
|
+
const {
|
|
1291
|
+
disabledAgents = [],
|
|
1292
|
+
agentOverrides = {},
|
|
1293
|
+
categories,
|
|
1294
|
+
uiSelectedModel,
|
|
1295
|
+
systemDefaultModel,
|
|
1296
|
+
availableModels = new Set,
|
|
1297
|
+
disabledSkills,
|
|
1298
|
+
resolveSkills
|
|
1299
|
+
} = options;
|
|
1300
|
+
const disabledSet = new Set(disabledAgents);
|
|
1301
|
+
const result = {};
|
|
1302
|
+
for (const [name, factory] of Object.entries(AGENT_FACTORIES)) {
|
|
1303
|
+
if (disabledSet.has(name))
|
|
1304
|
+
continue;
|
|
1305
|
+
const override = agentOverrides[name];
|
|
1306
|
+
const overrideModel = override?.model;
|
|
1307
|
+
const resolvedModel = resolveAgentModel(name, {
|
|
1308
|
+
availableModels,
|
|
1309
|
+
agentMode: factory.mode,
|
|
1310
|
+
uiSelectedModel,
|
|
1311
|
+
systemDefaultModel,
|
|
1312
|
+
overrideModel
|
|
1313
|
+
});
|
|
1314
|
+
const built = buildAgent(factory, resolvedModel, {
|
|
1315
|
+
categories,
|
|
1316
|
+
disabledSkills,
|
|
1317
|
+
resolveSkills
|
|
1318
|
+
});
|
|
1319
|
+
if (override) {
|
|
1320
|
+
if (override.prompt_append) {
|
|
1321
|
+
built.prompt = (built.prompt ? built.prompt + `
|
|
1322
|
+
|
|
1323
|
+
` : "") + override.prompt_append;
|
|
1324
|
+
}
|
|
1325
|
+
if (override.temperature !== undefined) {
|
|
1326
|
+
built.temperature = override.temperature;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
result[name] = built;
|
|
1330
|
+
}
|
|
1331
|
+
return result;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/create-managers.ts
|
|
1335
|
+
function createManagers(options) {
|
|
1336
|
+
const { pluginConfig, resolveSkills } = options;
|
|
1337
|
+
const agents = createBuiltinAgents({
|
|
1338
|
+
disabledAgents: pluginConfig.disabled_agents,
|
|
1339
|
+
agentOverrides: pluginConfig.agents,
|
|
1340
|
+
resolveSkills
|
|
1341
|
+
});
|
|
1342
|
+
const configHandler = new ConfigHandler({ pluginConfig });
|
|
1343
|
+
const backgroundManager = new BackgroundManager({
|
|
1344
|
+
maxConcurrent: pluginConfig.background?.defaultConcurrency ?? 5
|
|
1345
|
+
});
|
|
1346
|
+
const skillMcpManager = new SkillMcpManager;
|
|
1347
|
+
return { configHandler, backgroundManager, skillMcpManager, agents };
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// src/features/skill-loader/loader.ts
|
|
1351
|
+
import * as path3 from "path";
|
|
1352
|
+
import * as os2 from "os";
|
|
1353
|
+
|
|
1354
|
+
// src/features/skill-loader/discovery.ts
|
|
1355
|
+
import * as fs2 from "fs";
|
|
1356
|
+
import * as path2 from "path";
|
|
1357
|
+
function parseFrontmatter(text) {
|
|
1358
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
1359
|
+
const match = frontmatterRegex.exec(text);
|
|
1360
|
+
if (!match) {
|
|
1361
|
+
return { metadata: {}, content: text };
|
|
1362
|
+
}
|
|
1363
|
+
const yamlBlock = match[1];
|
|
1364
|
+
const body = match[2];
|
|
1365
|
+
const metadata = {};
|
|
1366
|
+
try {
|
|
1367
|
+
const lines = yamlBlock.split(/\r?\n/);
|
|
1368
|
+
let i = 0;
|
|
1369
|
+
while (i < lines.length) {
|
|
1370
|
+
const line = lines[i];
|
|
1371
|
+
const keyValueMatch = /^(\w[\w-]*):\s*(.*)$/.exec(line);
|
|
1372
|
+
if (!keyValueMatch) {
|
|
1373
|
+
i++;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
const key = keyValueMatch[1];
|
|
1377
|
+
const rawValue = keyValueMatch[2].trim();
|
|
1378
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
1379
|
+
const items = rawValue.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter((s) => s.length > 0);
|
|
1380
|
+
setMetadataField(metadata, key, items);
|
|
1381
|
+
i++;
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
1384
|
+
if (rawValue === "") {
|
|
1385
|
+
const listItems = [];
|
|
1386
|
+
i++;
|
|
1387
|
+
while (i < lines.length && /^\s+-\s+(.+)$/.test(lines[i])) {
|
|
1388
|
+
const itemMatch = /^\s+-\s+(.+)$/.exec(lines[i]);
|
|
1389
|
+
if (itemMatch) {
|
|
1390
|
+
listItems.push(itemMatch[1].trim().replace(/^['"]|['"]$/g, ""));
|
|
1391
|
+
}
|
|
1392
|
+
i++;
|
|
1393
|
+
}
|
|
1394
|
+
if (listItems.length > 0) {
|
|
1395
|
+
setMetadataField(metadata, key, listItems);
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
setMetadataField(metadata, key, rawValue.replace(/^['"]|['"]$/g, ""));
|
|
1400
|
+
i++;
|
|
1401
|
+
}
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
log("Failed to parse YAML frontmatter", { error: String(err) });
|
|
1404
|
+
return { metadata: {}, content: text };
|
|
1405
|
+
}
|
|
1406
|
+
return { metadata, content: body };
|
|
1407
|
+
}
|
|
1408
|
+
function setMetadataField(metadata, key, value) {
|
|
1409
|
+
switch (key) {
|
|
1410
|
+
case "name":
|
|
1411
|
+
if (typeof value === "string")
|
|
1412
|
+
metadata.name = value;
|
|
1413
|
+
break;
|
|
1414
|
+
case "description":
|
|
1415
|
+
if (typeof value === "string")
|
|
1416
|
+
metadata.description = value;
|
|
1417
|
+
break;
|
|
1418
|
+
case "model":
|
|
1419
|
+
if (typeof value === "string")
|
|
1420
|
+
metadata.model = value;
|
|
1421
|
+
break;
|
|
1422
|
+
case "tools":
|
|
1423
|
+
metadata.tools = value;
|
|
1424
|
+
break;
|
|
1425
|
+
default:
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function scanDirectory(options) {
|
|
1430
|
+
const { directory, scope } = options;
|
|
1431
|
+
if (!fs2.existsSync(directory)) {
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1434
|
+
let entries;
|
|
1435
|
+
try {
|
|
1436
|
+
entries = fs2.readdirSync(directory, { withFileTypes: true });
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
log("Failed to read skills directory", { directory, error: String(err) });
|
|
1439
|
+
return [];
|
|
1440
|
+
}
|
|
1441
|
+
const skills = [];
|
|
1442
|
+
for (const entry of entries) {
|
|
1443
|
+
const fullPath = path2.join(directory, entry.name);
|
|
1444
|
+
if (entry.isDirectory()) {
|
|
1445
|
+
const skillFile = path2.join(fullPath, "SKILL.md");
|
|
1446
|
+
if (fs2.existsSync(skillFile)) {
|
|
1447
|
+
const skill = loadSkillFile(skillFile, scope);
|
|
1448
|
+
if (skill)
|
|
1449
|
+
skills.push(skill);
|
|
1450
|
+
}
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1454
|
+
const skill = loadSkillFile(fullPath, scope);
|
|
1455
|
+
if (skill)
|
|
1456
|
+
skills.push(skill);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return skills;
|
|
1460
|
+
}
|
|
1461
|
+
function loadSkillFile(filePath, scope) {
|
|
1462
|
+
let text;
|
|
1463
|
+
try {
|
|
1464
|
+
text = fs2.readFileSync(filePath, "utf8");
|
|
1465
|
+
} catch (err) {
|
|
1466
|
+
log("Failed to read skill file", { filePath, error: String(err) });
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
const { metadata, content } = parseFrontmatter(text);
|
|
1470
|
+
if (!metadata.name) {
|
|
1471
|
+
log("Skill file missing name in frontmatter — skipping", { filePath });
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
return { name: metadata.name, description: metadata.description ?? "", content, scope, path: filePath, model: metadata.model };
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/features/skill-loader/merger.ts
|
|
1478
|
+
var SCOPE_PRIORITY = { project: 3, user: 2, builtin: 1 };
|
|
1479
|
+
function mergeSkills(skills) {
|
|
1480
|
+
const skillMap = new Map;
|
|
1481
|
+
for (const skill of skills) {
|
|
1482
|
+
const existing = skillMap.get(skill.name);
|
|
1483
|
+
if (!existing || (SCOPE_PRIORITY[skill.scope] ?? 0) > (SCOPE_PRIORITY[existing.scope] ?? 0)) {
|
|
1484
|
+
skillMap.set(skill.name, skill);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return Array.from(skillMap.values());
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/features/skill-loader/builtin-skills.ts
|
|
1491
|
+
function createBuiltinSkills() {
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/features/skill-loader/loader.ts
|
|
1496
|
+
function loadSkills(options = {}) {
|
|
1497
|
+
const { directory, disabledSkills = [] } = options;
|
|
1498
|
+
const projectDir = path3.join(directory ?? process.cwd(), ".opencode", "skills");
|
|
1499
|
+
const userDir = path3.join(os2.homedir(), ".config", "opencode", "weave-opencode", "skills");
|
|
1500
|
+
const projectSkills = scanDirectory({ directory: projectDir, scope: "project" });
|
|
1501
|
+
const userSkills = scanDirectory({ directory: userDir, scope: "user" });
|
|
1502
|
+
const builtinSkills = createBuiltinSkills();
|
|
1503
|
+
const all = [...projectSkills, ...userSkills, ...builtinSkills];
|
|
1504
|
+
const merged = mergeSkills(all);
|
|
1505
|
+
if (disabledSkills.length === 0)
|
|
1506
|
+
return { skills: merged };
|
|
1507
|
+
const disabledSet = new Set(disabledSkills);
|
|
1508
|
+
return { skills: merged.filter((s) => !disabledSet.has(s.name)) };
|
|
1509
|
+
}
|
|
1510
|
+
// src/features/skill-loader/resolver.ts
|
|
1511
|
+
function resolveSkill(name, result) {
|
|
1512
|
+
return result.skills.find((s) => s.name === name)?.content ?? "";
|
|
1513
|
+
}
|
|
1514
|
+
function resolveMultipleSkills(skillNames, disabledSkills, discovered) {
|
|
1515
|
+
if (!discovered)
|
|
1516
|
+
return "";
|
|
1517
|
+
const parts = [];
|
|
1518
|
+
for (const name of skillNames) {
|
|
1519
|
+
if (disabledSkills?.has(name))
|
|
1520
|
+
continue;
|
|
1521
|
+
const content = resolveSkill(name, discovered);
|
|
1522
|
+
if (content) {
|
|
1523
|
+
parts.push(content);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return parts.join(`
|
|
1527
|
+
|
|
1528
|
+
`);
|
|
1529
|
+
}
|
|
1530
|
+
function createSkillResolver(discovered) {
|
|
1531
|
+
return (skillNames, disabledSkills) => {
|
|
1532
|
+
return resolveMultipleSkills(skillNames, disabledSkills, discovered);
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
// src/create-tools.ts
|
|
1536
|
+
async function createTools(options) {
|
|
1537
|
+
const { ctx, pluginConfig } = options;
|
|
1538
|
+
const skillResult = loadSkills({
|
|
1539
|
+
directory: ctx.directory,
|
|
1540
|
+
disabledSkills: pluginConfig.disabled_skills ?? []
|
|
1541
|
+
});
|
|
1542
|
+
const resolveSkillsFn = createSkillResolver(skillResult);
|
|
1543
|
+
const tools = {};
|
|
1544
|
+
return {
|
|
1545
|
+
tools,
|
|
1546
|
+
availableSkills: skillResult.skills,
|
|
1547
|
+
resolveSkillsFn
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/hooks/context-window-monitor.ts
|
|
1552
|
+
function checkContextWindow(state, thresholds = { warningPct: 0.8, criticalPct: 0.95 }) {
|
|
1553
|
+
const usagePct = state.maxTokens > 0 ? state.usedTokens / state.maxTokens : 0;
|
|
1554
|
+
if (usagePct >= thresholds.criticalPct) {
|
|
1555
|
+
const message = buildRecoveryMessage(state, usagePct);
|
|
1556
|
+
log(`[context-window] CRITICAL ${(usagePct * 100).toFixed(1)}% used in session ${state.sessionId}`);
|
|
1557
|
+
return { action: "recover", usagePct, message };
|
|
1558
|
+
}
|
|
1559
|
+
if (usagePct >= thresholds.warningPct) {
|
|
1560
|
+
log(`[context-window] WARNING ${(usagePct * 100).toFixed(1)}% used in session ${state.sessionId}`);
|
|
1561
|
+
return { action: "warn", usagePct, message: buildWarningMessage(usagePct) };
|
|
1562
|
+
}
|
|
1563
|
+
return { action: "none", usagePct };
|
|
1564
|
+
}
|
|
1565
|
+
function buildWarningMessage(usagePct) {
|
|
1566
|
+
const pct = (usagePct * 100).toFixed(0);
|
|
1567
|
+
return `⚠️ Context window at ${pct}%. Consider wrapping up the current task or spawning a background agent for remaining work.
|
|
1568
|
+
|
|
1569
|
+
Update the sidebar: use todowrite to create or update a todo (in_progress, high priority): "Context: ${pct}% — wrap up soon"`;
|
|
1570
|
+
}
|
|
1571
|
+
function buildRecoveryMessage(state, usagePct) {
|
|
1572
|
+
const pct = (usagePct * 100).toFixed(0);
|
|
1573
|
+
return `\uD83D\uDEA8 Context window at ${pct}% (${state.usedTokens}/${state.maxTokens} tokens).
|
|
1574
|
+
|
|
1575
|
+
IMMEDIATE ACTION REQUIRED:
|
|
1576
|
+
1. Save your current progress and findings to a notepad or file
|
|
1577
|
+
2. Summarize completed work and remaining tasks
|
|
1578
|
+
3. If work remains: spawn a background agent or ask the user to continue in a new session
|
|
1579
|
+
4. Do NOT attempt large new tasks — wrap up gracefully
|
|
1580
|
+
|
|
1581
|
+
Update the sidebar: use todowrite to create a todo (in_progress, high priority): "CONTEXT ${pct}% — save & stop"`;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/hooks/write-existing-file-guard.ts
|
|
1585
|
+
import * as fs3 from "fs";
|
|
1586
|
+
function createWriteGuardState() {
|
|
1587
|
+
return { readFiles: new Set };
|
|
1588
|
+
}
|
|
1589
|
+
function trackFileRead(state, filePath) {
|
|
1590
|
+
state.readFiles.add(filePath);
|
|
1591
|
+
}
|
|
1592
|
+
function checkWriteAllowed(state, filePath) {
|
|
1593
|
+
if (!fs3.existsSync(filePath)) {
|
|
1594
|
+
return { allowed: true };
|
|
1595
|
+
}
|
|
1596
|
+
if (state.readFiles.has(filePath)) {
|
|
1597
|
+
return { allowed: true };
|
|
1598
|
+
}
|
|
1599
|
+
const warning = `⚠️ Write guard: Attempting to write to '${filePath}' without reading it first. Read the file before overwriting to avoid data loss.`;
|
|
1600
|
+
log(`[write-guard] BLOCKED write to unread file: ${filePath}`);
|
|
1601
|
+
return { allowed: false, warning };
|
|
1602
|
+
}
|
|
1603
|
+
function createWriteGuard(state) {
|
|
1604
|
+
return {
|
|
1605
|
+
trackRead: (filePath) => trackFileRead(state, filePath),
|
|
1606
|
+
checkWrite: (filePath) => checkWriteAllowed(state, filePath)
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/hooks/rules-injector.ts
|
|
1611
|
+
import * as fs4 from "fs";
|
|
1612
|
+
import * as path4 from "path";
|
|
1613
|
+
var RULES_FILENAMES = ["AGENTS.md", ".rules", "CLAUDE.md"];
|
|
1614
|
+
function findRulesFile(directory) {
|
|
1615
|
+
for (const filename of RULES_FILENAMES) {
|
|
1616
|
+
const candidate = path4.join(directory, filename);
|
|
1617
|
+
if (fs4.existsSync(candidate)) {
|
|
1618
|
+
return candidate;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
function loadRulesForDirectory(directory) {
|
|
1624
|
+
const rulesFile = findRulesFile(directory);
|
|
1625
|
+
if (!rulesFile)
|
|
1626
|
+
return;
|
|
1627
|
+
try {
|
|
1628
|
+
const content = fs4.readFileSync(rulesFile, "utf8");
|
|
1629
|
+
log(`[rules-injector] Loaded rules from ${rulesFile}`);
|
|
1630
|
+
return content;
|
|
1631
|
+
} catch {
|
|
1632
|
+
log(`[rules-injector] Failed to read rules file: ${rulesFile}`);
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function shouldInjectRules(toolName) {
|
|
1637
|
+
return toolName === "read" || toolName === "write" || toolName === "edit";
|
|
1638
|
+
}
|
|
1639
|
+
function getDirectoryFromFilePath(filePath) {
|
|
1640
|
+
return path4.dirname(path4.resolve(filePath));
|
|
1641
|
+
}
|
|
1642
|
+
function buildRulesInjection(rulesContent, directory) {
|
|
1643
|
+
return `<rules source="${directory}">
|
|
1644
|
+
${rulesContent}
|
|
1645
|
+
</rules>`;
|
|
1646
|
+
}
|
|
1647
|
+
function getRulesForFile(filePath) {
|
|
1648
|
+
const dir = getDirectoryFromFilePath(filePath);
|
|
1649
|
+
const content = loadRulesForDirectory(dir);
|
|
1650
|
+
if (!content)
|
|
1651
|
+
return;
|
|
1652
|
+
return buildRulesInjection(content, dir);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/hooks/first-message-variant.ts
|
|
1656
|
+
var appliedSessions = new Set;
|
|
1657
|
+
var createdSessions = new Set;
|
|
1658
|
+
function markSessionCreated(sessionId) {
|
|
1659
|
+
createdSessions.add(sessionId);
|
|
1660
|
+
}
|
|
1661
|
+
function markApplied(sessionId) {
|
|
1662
|
+
appliedSessions.add(sessionId);
|
|
1663
|
+
}
|
|
1664
|
+
function shouldApplyVariant(sessionId) {
|
|
1665
|
+
return createdSessions.has(sessionId) && !appliedSessions.has(sessionId);
|
|
1666
|
+
}
|
|
1667
|
+
function clearSession(sessionId) {
|
|
1668
|
+
appliedSessions.delete(sessionId);
|
|
1669
|
+
createdSessions.delete(sessionId);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// src/hooks/keyword-detector.ts
|
|
1673
|
+
var DEFAULT_KEYWORD_ACTIONS = [
|
|
1674
|
+
{
|
|
1675
|
+
keyword: "ultrawork",
|
|
1676
|
+
injection: `[ULTRAWORK MODE ACTIVATED]
|
|
1677
|
+
Maximum effort engaged. Use ALL available agents in parallel. No shortcuts. Complete the task fully and deeply before responding.`
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
keyword: "ulw",
|
|
1681
|
+
injection: `[ULTRAWORK MODE ACTIVATED]
|
|
1682
|
+
Maximum effort engaged. Use ALL available agents in parallel. No shortcuts. Complete the task fully and deeply before responding.`
|
|
1683
|
+
}
|
|
1684
|
+
];
|
|
1685
|
+
function detectKeywords(message, actions = DEFAULT_KEYWORD_ACTIONS) {
|
|
1686
|
+
const lower = message.toLowerCase();
|
|
1687
|
+
return actions.filter((a) => lower.includes(a.keyword.toLowerCase()));
|
|
1688
|
+
}
|
|
1689
|
+
function buildKeywordInjection(detected) {
|
|
1690
|
+
if (detected.length === 0)
|
|
1691
|
+
return;
|
|
1692
|
+
return detected.map((a) => a.injection).join(`
|
|
1693
|
+
|
|
1694
|
+
`);
|
|
1695
|
+
}
|
|
1696
|
+
function processMessageForKeywords(message, sessionId, actions) {
|
|
1697
|
+
const detected = detectKeywords(message, actions);
|
|
1698
|
+
if (detected.length > 0) {
|
|
1699
|
+
log(`[keyword-detector] Detected keywords in session ${sessionId}: ${detected.map((a) => a.keyword).join(", ")}`);
|
|
1700
|
+
}
|
|
1701
|
+
return buildKeywordInjection(detected);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// src/hooks/pattern-md-only.ts
|
|
1705
|
+
var WRITE_TOOLS = new Set(["write", "edit"]);
|
|
1706
|
+
var WEAVE_DIR_SEGMENT = ".weave";
|
|
1707
|
+
function checkPatternWrite(agentName, toolName, filePath) {
|
|
1708
|
+
if (agentName !== "pattern") {
|
|
1709
|
+
return { allowed: true };
|
|
1710
|
+
}
|
|
1711
|
+
if (!WRITE_TOOLS.has(toolName)) {
|
|
1712
|
+
return { allowed: true };
|
|
1713
|
+
}
|
|
1714
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
1715
|
+
if (!normalizedPath.includes(`${WEAVE_DIR_SEGMENT}/`)) {
|
|
1716
|
+
return {
|
|
1717
|
+
allowed: false,
|
|
1718
|
+
reason: `Pattern agent can only write to .weave/ directory. Attempted: ${filePath}`
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (!normalizedPath.endsWith(".md")) {
|
|
1722
|
+
return {
|
|
1723
|
+
allowed: false,
|
|
1724
|
+
reason: `Pattern agent can only write .md files. Attempted: ${filePath}`
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
return { allowed: true };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/features/work-state/constants.ts
|
|
1731
|
+
var WEAVE_DIR = ".weave";
|
|
1732
|
+
var WORK_STATE_FILE = "state.json";
|
|
1733
|
+
var WORK_STATE_PATH = `${WEAVE_DIR}/${WORK_STATE_FILE}`;
|
|
1734
|
+
var PLANS_DIR = `${WEAVE_DIR}/plans`;
|
|
1735
|
+
// src/features/work-state/storage.ts
|
|
1736
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync, unlinkSync, mkdirSync, readdirSync as readdirSync2, statSync } from "fs";
|
|
1737
|
+
import { join as join6, basename } from "path";
|
|
1738
|
+
var UNCHECKED_RE = /^[-*]\s*\[\s*\]/gm;
|
|
1739
|
+
var CHECKED_RE = /^[-*]\s*\[[xX]\]/gm;
|
|
1740
|
+
function readWorkState(directory) {
|
|
1741
|
+
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
1742
|
+
try {
|
|
1743
|
+
if (!existsSync5(filePath))
|
|
1744
|
+
return null;
|
|
1745
|
+
const raw = readFileSync4(filePath, "utf-8");
|
|
1746
|
+
const parsed = JSON.parse(raw);
|
|
1747
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1748
|
+
return null;
|
|
1749
|
+
if (typeof parsed.active_plan !== "string")
|
|
1750
|
+
return null;
|
|
1751
|
+
if (!Array.isArray(parsed.session_ids)) {
|
|
1752
|
+
parsed.session_ids = [];
|
|
1753
|
+
}
|
|
1754
|
+
return parsed;
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function writeWorkState(directory, state) {
|
|
1760
|
+
try {
|
|
1761
|
+
const dir = join6(directory, WEAVE_DIR);
|
|
1762
|
+
if (!existsSync5(dir)) {
|
|
1763
|
+
mkdirSync(dir, { recursive: true });
|
|
1764
|
+
}
|
|
1765
|
+
writeFileSync(join6(dir, WORK_STATE_FILE), JSON.stringify(state, null, 2), "utf-8");
|
|
1766
|
+
return true;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return false;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function clearWorkState(directory) {
|
|
1772
|
+
const filePath = join6(directory, WEAVE_DIR, WORK_STATE_FILE);
|
|
1773
|
+
try {
|
|
1774
|
+
if (existsSync5(filePath)) {
|
|
1775
|
+
unlinkSync(filePath);
|
|
1776
|
+
}
|
|
1777
|
+
return true;
|
|
1778
|
+
} catch {
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
function appendSessionId(directory, sessionId) {
|
|
1783
|
+
const state = readWorkState(directory);
|
|
1784
|
+
if (!state)
|
|
1785
|
+
return null;
|
|
1786
|
+
if (!state.session_ids.includes(sessionId)) {
|
|
1787
|
+
state.session_ids.push(sessionId);
|
|
1788
|
+
writeWorkState(directory, state);
|
|
1789
|
+
}
|
|
1790
|
+
return state;
|
|
1791
|
+
}
|
|
1792
|
+
function createWorkState(planPath, sessionId, agent) {
|
|
1793
|
+
return {
|
|
1794
|
+
active_plan: planPath,
|
|
1795
|
+
started_at: new Date().toISOString(),
|
|
1796
|
+
session_ids: [sessionId],
|
|
1797
|
+
plan_name: getPlanName(planPath),
|
|
1798
|
+
...agent !== undefined ? { agent } : {}
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
function findPlans(directory) {
|
|
1802
|
+
const plansDir = join6(directory, PLANS_DIR);
|
|
1803
|
+
try {
|
|
1804
|
+
if (!existsSync5(plansDir))
|
|
1805
|
+
return [];
|
|
1806
|
+
const files = readdirSync2(plansDir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
1807
|
+
const fullPath = join6(plansDir, f);
|
|
1808
|
+
const stat = statSync(fullPath);
|
|
1809
|
+
return { path: fullPath, mtime: stat.mtimeMs };
|
|
1810
|
+
}).sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
|
|
1811
|
+
return files;
|
|
1812
|
+
} catch {
|
|
1813
|
+
return [];
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function getPlanProgress(planPath) {
|
|
1817
|
+
if (!existsSync5(planPath)) {
|
|
1818
|
+
return { total: 0, completed: 0, isComplete: true };
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
const content = readFileSync4(planPath, "utf-8");
|
|
1822
|
+
const unchecked = content.match(UNCHECKED_RE) || [];
|
|
1823
|
+
const checked = content.match(CHECKED_RE) || [];
|
|
1824
|
+
const total = unchecked.length + checked.length;
|
|
1825
|
+
const completed = checked.length;
|
|
1826
|
+
return {
|
|
1827
|
+
total,
|
|
1828
|
+
completed,
|
|
1829
|
+
isComplete: total === 0 || completed === total
|
|
1830
|
+
};
|
|
1831
|
+
} catch {
|
|
1832
|
+
return { total: 0, completed: 0, isComplete: true };
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
function getPlanName(planPath) {
|
|
1836
|
+
return basename(planPath, ".md");
|
|
1837
|
+
}
|
|
1838
|
+
// src/hooks/start-work-hook.ts
|
|
1839
|
+
function handleStartWork(input) {
|
|
1840
|
+
const { promptText, sessionId, directory } = input;
|
|
1841
|
+
if (!promptText.includes("<session-context>")) {
|
|
1842
|
+
return { contextInjection: null, switchAgent: null };
|
|
1843
|
+
}
|
|
1844
|
+
const explicitPlanName = extractPlanName(promptText);
|
|
1845
|
+
const existingState = readWorkState(directory);
|
|
1846
|
+
const allPlans = findPlans(directory);
|
|
1847
|
+
if (explicitPlanName) {
|
|
1848
|
+
return handleExplicitPlan(explicitPlanName, allPlans, sessionId, directory);
|
|
1849
|
+
}
|
|
1850
|
+
if (existingState) {
|
|
1851
|
+
const progress = getPlanProgress(existingState.active_plan);
|
|
1852
|
+
if (!progress.isComplete) {
|
|
1853
|
+
appendSessionId(directory, sessionId);
|
|
1854
|
+
return {
|
|
1855
|
+
switchAgent: "tapestry",
|
|
1856
|
+
contextInjection: buildResumeContext(existingState.active_plan, existingState.plan_name, progress)
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
return handlePlanDiscovery(allPlans, sessionId, directory);
|
|
1861
|
+
}
|
|
1862
|
+
function extractPlanName(promptText) {
|
|
1863
|
+
const match = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i);
|
|
1864
|
+
if (!match)
|
|
1865
|
+
return null;
|
|
1866
|
+
const cleaned = match[1].trim();
|
|
1867
|
+
return cleaned || null;
|
|
1868
|
+
}
|
|
1869
|
+
function handleExplicitPlan(requestedName, allPlans, sessionId, directory) {
|
|
1870
|
+
const matched = findPlanByName(allPlans, requestedName);
|
|
1871
|
+
if (!matched) {
|
|
1872
|
+
const incompletePlans = allPlans.filter((p) => !getPlanProgress(p).isComplete);
|
|
1873
|
+
const listing = incompletePlans.length > 0 ? incompletePlans.map((p) => ` - ${getPlanName(p)}`).join(`
|
|
1874
|
+
`) : " (none)";
|
|
1875
|
+
return {
|
|
1876
|
+
switchAgent: "tapestry",
|
|
1877
|
+
contextInjection: `## Plan Not Found
|
|
1878
|
+
No plan matching "${requestedName}" was found.
|
|
1879
|
+
|
|
1880
|
+
Available incomplete plans:
|
|
1881
|
+
${listing}
|
|
1882
|
+
|
|
1883
|
+
Tell the user which plans are available and ask them to specify one.`
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
const progress = getPlanProgress(matched);
|
|
1887
|
+
if (progress.isComplete) {
|
|
1888
|
+
return {
|
|
1889
|
+
switchAgent: "tapestry",
|
|
1890
|
+
contextInjection: `## Plan Already Complete
|
|
1891
|
+
The plan "${getPlanName(matched)}" has all ${progress.total} tasks completed.
|
|
1892
|
+
Tell the user this plan is already done and suggest creating a new one with Pattern.`
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
clearWorkState(directory);
|
|
1896
|
+
const state = createWorkState(matched, sessionId, "tapestry");
|
|
1897
|
+
writeWorkState(directory, state);
|
|
1898
|
+
return {
|
|
1899
|
+
switchAgent: "tapestry",
|
|
1900
|
+
contextInjection: buildFreshContext(matched, getPlanName(matched), progress)
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
function handlePlanDiscovery(allPlans, sessionId, directory) {
|
|
1904
|
+
if (allPlans.length === 0) {
|
|
1905
|
+
return {
|
|
1906
|
+
switchAgent: "tapestry",
|
|
1907
|
+
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."
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const incompletePlans = allPlans.filter((p) => !getPlanProgress(p).isComplete);
|
|
1911
|
+
if (incompletePlans.length === 0) {
|
|
1912
|
+
return {
|
|
1913
|
+
switchAgent: "tapestry",
|
|
1914
|
+
contextInjection: `## All Plans Complete
|
|
1915
|
+
All existing plans have been completed.
|
|
1916
|
+
Tell the user to switch to Pattern agent to create a new plan.`
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
if (incompletePlans.length === 1) {
|
|
1920
|
+
const plan = incompletePlans[0];
|
|
1921
|
+
const progress = getPlanProgress(plan);
|
|
1922
|
+
const state = createWorkState(plan, sessionId, "tapestry");
|
|
1923
|
+
writeWorkState(directory, state);
|
|
1924
|
+
return {
|
|
1925
|
+
switchAgent: "tapestry",
|
|
1926
|
+
contextInjection: buildFreshContext(plan, getPlanName(plan), progress)
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
const listing = incompletePlans.map((p) => {
|
|
1930
|
+
const progress = getPlanProgress(p);
|
|
1931
|
+
return ` - **${getPlanName(p)}** (${progress.completed}/${progress.total} tasks done)`;
|
|
1932
|
+
}).join(`
|
|
1933
|
+
`);
|
|
1934
|
+
return {
|
|
1935
|
+
switchAgent: "tapestry",
|
|
1936
|
+
contextInjection: `## Multiple Plans Found
|
|
1937
|
+
There are ${incompletePlans.length} incomplete plans:
|
|
1938
|
+
${listing}
|
|
1939
|
+
|
|
1940
|
+
Ask the user which plan to work on. They can run \`/start-work [plan-name]\` to select one.`
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
function findPlanByName(plans, requestedName) {
|
|
1944
|
+
const lower = requestedName.toLowerCase();
|
|
1945
|
+
const exact = plans.find((p) => getPlanName(p).toLowerCase() === lower);
|
|
1946
|
+
if (exact)
|
|
1947
|
+
return exact;
|
|
1948
|
+
const partial = plans.find((p) => getPlanName(p).toLowerCase().includes(lower));
|
|
1949
|
+
return partial || null;
|
|
1950
|
+
}
|
|
1951
|
+
function buildFreshContext(planPath, planName, progress) {
|
|
1952
|
+
return `## Starting Plan: ${planName}
|
|
1953
|
+
**Plan file**: ${planPath}
|
|
1954
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed
|
|
1955
|
+
|
|
1956
|
+
Read the plan file now and begin executing from the first unchecked \`- [ ]\` task.
|
|
1957
|
+
|
|
1958
|
+
**SIDEBAR TODOS — DO THIS FIRST:**
|
|
1959
|
+
Before starting any work, use todowrite to populate the sidebar:
|
|
1960
|
+
1. Create a summary todo (in_progress): "${planName} ${progress.completed}/${progress.total}"
|
|
1961
|
+
2. Create a todo for the first unchecked task (in_progress)
|
|
1962
|
+
3. Create todos for the next 2-3 tasks (pending)
|
|
1963
|
+
Keep each todo under 35 chars. Update as you complete tasks.`;
|
|
1964
|
+
}
|
|
1965
|
+
function buildResumeContext(planPath, planName, progress) {
|
|
1966
|
+
const remaining = progress.total - progress.completed;
|
|
1967
|
+
return `## Resuming Plan: ${planName}
|
|
1968
|
+
**Plan file**: ${planPath}
|
|
1969
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed
|
|
1970
|
+
**Status**: RESUMING — continuing from where the previous session left off.
|
|
1971
|
+
|
|
1972
|
+
Read the plan file now and continue from the first unchecked \`- [ ]\` task.
|
|
1973
|
+
|
|
1974
|
+
**SIDEBAR TODOS — RESTORE STATE:**
|
|
1975
|
+
Previous session's todos are lost. Use todowrite to restore the sidebar:
|
|
1976
|
+
1. Create a summary todo (in_progress): "${planName} ${progress.completed}/${progress.total}"
|
|
1977
|
+
2. Create a todo for the next unchecked task (in_progress)
|
|
1978
|
+
3. Create todos for the following 2-3 tasks (pending)
|
|
1979
|
+
Keep each todo under 35 chars. ${remaining} task${remaining !== 1 ? "s" : ""} remaining.`;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/hooks/work-continuation.ts
|
|
1983
|
+
function checkContinuation(input) {
|
|
1984
|
+
const { directory } = input;
|
|
1985
|
+
const state = readWorkState(directory);
|
|
1986
|
+
if (!state) {
|
|
1987
|
+
return { continuationPrompt: null };
|
|
1988
|
+
}
|
|
1989
|
+
const progress = getPlanProgress(state.active_plan);
|
|
1990
|
+
if (progress.isComplete) {
|
|
1991
|
+
return { continuationPrompt: null };
|
|
1992
|
+
}
|
|
1993
|
+
const remaining = progress.total - progress.completed;
|
|
1994
|
+
return {
|
|
1995
|
+
continuationPrompt: `You have an active work plan with incomplete tasks. Continue working.
|
|
1996
|
+
|
|
1997
|
+
**Plan**: ${state.plan_name}
|
|
1998
|
+
**File**: ${state.active_plan}
|
|
1999
|
+
**Progress**: ${progress.completed}/${progress.total} tasks completed (${remaining} remaining)
|
|
2000
|
+
|
|
2001
|
+
1. Read the plan file NOW to check exact current progress
|
|
2002
|
+
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.
|
|
2003
|
+
3. Find the first unchecked \`- [ ]\` task
|
|
2004
|
+
4. Execute it, verify it, mark \`- [ ]\` → \`- [x]\`
|
|
2005
|
+
5. Update sidebar todos as you complete tasks
|
|
2006
|
+
6. Do not stop until all tasks are complete`
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/hooks/verification-reminder.ts
|
|
2011
|
+
function buildVerificationReminder(input) {
|
|
2012
|
+
const planContext = input.planName && input.progress ? `
|
|
2013
|
+
**Plan**: ${input.planName} (${input.progress.completed}/${input.progress.total} tasks done)` : "";
|
|
2014
|
+
return {
|
|
2015
|
+
verificationPrompt: `## Verification Required
|
|
2016
|
+
${planContext}
|
|
2017
|
+
|
|
2018
|
+
Before marking this task complete, verify the work:
|
|
2019
|
+
|
|
2020
|
+
1. **Read the changes**: \`git diff --stat\` then Read each changed file
|
|
2021
|
+
2. **Run checks**: Run relevant tests, check for linting/type errors
|
|
2022
|
+
3. **Validate behavior**: Does the code actually do what was requested?
|
|
2023
|
+
4. **Gate decision**: Can you explain what every changed line does?
|
|
2024
|
+
|
|
2025
|
+
If uncertain about quality, delegate to \`weft\` agent for a formal review:
|
|
2026
|
+
\`call_weave_agent(agent="weft", prompt="Review the changes for [task description]")\`
|
|
2027
|
+
|
|
2028
|
+
If changes touch auth, crypto, tokens, or input validation, delegate to \`warp\` agent for a security audit:
|
|
2029
|
+
\`call_weave_agent(agent="warp", prompt="Security audit the changes for [task description]")\`
|
|
2030
|
+
|
|
2031
|
+
Only mark complete when ALL checks pass.`
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/hooks/create-hooks.ts
|
|
2036
|
+
function createHooks(args) {
|
|
2037
|
+
const { isHookEnabled, directory } = args;
|
|
2038
|
+
const writeGuardState = createWriteGuardState();
|
|
2039
|
+
const writeGuard = createWriteGuard(writeGuardState);
|
|
2040
|
+
const contextWindowThresholds = {
|
|
2041
|
+
warningPct: 0.8,
|
|
2042
|
+
criticalPct: 0.95
|
|
2043
|
+
};
|
|
2044
|
+
return {
|
|
2045
|
+
checkContextWindow: isHookEnabled("context-window-monitor") ? (state) => checkContextWindow(state, contextWindowThresholds) : null,
|
|
2046
|
+
writeGuard: isHookEnabled("write-existing-file-guard") ? writeGuard : null,
|
|
2047
|
+
shouldInjectRules: isHookEnabled("rules-injector") ? shouldInjectRules : null,
|
|
2048
|
+
getRulesForFile: isHookEnabled("rules-injector") ? getRulesForFile : null,
|
|
2049
|
+
firstMessageVariant: isHookEnabled("first-message-variant") ? { shouldApplyVariant, markApplied, markSessionCreated, clearSession } : null,
|
|
2050
|
+
processMessageForKeywords: isHookEnabled("keyword-detector") ? processMessageForKeywords : null,
|
|
2051
|
+
patternMdOnly: isHookEnabled("pattern-md-only") ? checkPatternWrite : null,
|
|
2052
|
+
startWork: isHookEnabled("start-work") ? (promptText, sessionId) => handleStartWork({ promptText, sessionId, directory }) : null,
|
|
2053
|
+
workContinuation: isHookEnabled("work-continuation") ? (sessionId) => checkContinuation({ sessionId, directory }) : null,
|
|
2054
|
+
verificationReminder: isHookEnabled("verification-reminder") ? buildVerificationReminder : null
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// src/plugin/plugin-interface.ts
|
|
2059
|
+
function createPluginInterface(args) {
|
|
2060
|
+
const { pluginConfig, hooks, tools, configHandler, agents } = args;
|
|
2061
|
+
return {
|
|
2062
|
+
tool: tools,
|
|
2063
|
+
config: async (config) => {
|
|
2064
|
+
const result = await configHandler.handle({
|
|
2065
|
+
pluginConfig,
|
|
2066
|
+
agents,
|
|
2067
|
+
availableTools: []
|
|
2068
|
+
});
|
|
2069
|
+
config.agent = result.agents;
|
|
2070
|
+
config.command = result.commands;
|
|
2071
|
+
if (result.defaultAgent) {
|
|
2072
|
+
config.default_agent = result.defaultAgent;
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
"chat.message": async (input, _output) => {
|
|
2076
|
+
const { sessionID } = input;
|
|
2077
|
+
if (hooks.checkContextWindow) {
|
|
2078
|
+
hooks.checkContextWindow({
|
|
2079
|
+
usedTokens: 0,
|
|
2080
|
+
maxTokens: 0,
|
|
2081
|
+
sessionId: sessionID
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
if (hooks.firstMessageVariant) {
|
|
2085
|
+
if (hooks.firstMessageVariant.shouldApplyVariant(sessionID)) {
|
|
2086
|
+
hooks.firstMessageVariant.markApplied(sessionID);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (hooks.processMessageForKeywords) {
|
|
2090
|
+
hooks.processMessageForKeywords("", sessionID);
|
|
2091
|
+
}
|
|
2092
|
+
if (hooks.startWork) {
|
|
2093
|
+
const parts = _output.parts;
|
|
2094
|
+
const message = _output.message;
|
|
2095
|
+
if (parts) {
|
|
2096
|
+
const timestamp = new Date().toISOString();
|
|
2097
|
+
for (const part of parts) {
|
|
2098
|
+
if (part.type === "text" && part.text) {
|
|
2099
|
+
part.text = part.text.replace(/\$SESSION_ID/g, sessionID).replace(/\$TIMESTAMP/g, timestamp);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const promptText = parts?.filter((p) => p.type === "text" && p.text).map((p) => p.text).join(`
|
|
2104
|
+
`).trim() ?? "";
|
|
2105
|
+
const result = hooks.startWork(promptText, sessionID);
|
|
2106
|
+
if (result.switchAgent && message) {
|
|
2107
|
+
message.agent = getAgentDisplayName(result.switchAgent);
|
|
2108
|
+
}
|
|
2109
|
+
if (result.contextInjection && parts) {
|
|
2110
|
+
const idx = parts.findIndex((p) => p.type === "text" && p.text);
|
|
2111
|
+
if (idx >= 0 && parts[idx].text) {
|
|
2112
|
+
parts[idx].text += `
|
|
2113
|
+
|
|
2114
|
+
---
|
|
2115
|
+
${result.contextInjection}`;
|
|
2116
|
+
} else {
|
|
2117
|
+
parts.push({ type: "text", text: result.contextInjection });
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
"chat.params": async (_input, _output) => {},
|
|
2123
|
+
"chat.headers": async (_input, _output) => {},
|
|
2124
|
+
event: async (input) => {
|
|
2125
|
+
const { event } = input;
|
|
2126
|
+
if (hooks.firstMessageVariant) {
|
|
2127
|
+
if (event.type === "session.created") {
|
|
2128
|
+
const evt = event;
|
|
2129
|
+
hooks.firstMessageVariant.markSessionCreated(evt.properties.info.id);
|
|
2130
|
+
}
|
|
2131
|
+
if (event.type === "session.deleted") {
|
|
2132
|
+
const evt = event;
|
|
2133
|
+
hooks.firstMessageVariant.clearSession(evt.properties.info.id);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
if (hooks.workContinuation && event.type === "session.idle") {
|
|
2137
|
+
const evt = event;
|
|
2138
|
+
const sessionId = evt.properties?.sessionID ?? "";
|
|
2139
|
+
if (sessionId) {
|
|
2140
|
+
const result = hooks.workContinuation(sessionId);
|
|
2141
|
+
if (result.continuationPrompt) {}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
"tool.execute.before": async (input, _output) => {
|
|
2146
|
+
const args2 = _output.args;
|
|
2147
|
+
const filePath = args2?.file_path ?? args2?.path ?? "";
|
|
2148
|
+
if (filePath && hooks.shouldInjectRules && hooks.getRulesForFile) {
|
|
2149
|
+
if (hooks.shouldInjectRules(input.tool)) {
|
|
2150
|
+
hooks.getRulesForFile(filePath);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
if (filePath && hooks.writeGuard) {
|
|
2154
|
+
if (input.tool === "read") {
|
|
2155
|
+
hooks.writeGuard.trackRead(filePath);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (filePath && hooks.patternMdOnly) {
|
|
2159
|
+
const agentName = input.agent;
|
|
2160
|
+
if (agentName) {
|
|
2161
|
+
const check = hooks.patternMdOnly(agentName, input.tool, filePath);
|
|
2162
|
+
if (!check.allowed) {
|
|
2163
|
+
throw new Error(check.reason ?? "Pattern agent is restricted to .md files in .weave/");
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
},
|
|
2168
|
+
"tool.execute.after": async (_input, _output) => {}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// src/index.ts
|
|
2173
|
+
var WeavePlugin = async (ctx) => {
|
|
2174
|
+
const pluginConfig = loadWeaveConfig(ctx.directory, ctx);
|
|
2175
|
+
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
|
|
2176
|
+
const isHookEnabled = (name) => !disabledHooks.has(name);
|
|
2177
|
+
const toolsResult = await createTools({ ctx, pluginConfig });
|
|
2178
|
+
const managers = createManagers({ ctx, pluginConfig, resolveSkills: toolsResult.resolveSkillsFn });
|
|
2179
|
+
const hooks = createHooks({ pluginConfig, isHookEnabled, directory: ctx.directory });
|
|
2180
|
+
return createPluginInterface({
|
|
2181
|
+
pluginConfig,
|
|
2182
|
+
hooks,
|
|
2183
|
+
tools: toolsResult.tools,
|
|
2184
|
+
configHandler: managers.configHandler,
|
|
2185
|
+
agents: managers.agents
|
|
2186
|
+
});
|
|
2187
|
+
};
|
|
2188
|
+
var src_default = WeavePlugin;
|
|
2189
|
+
export {
|
|
2190
|
+
src_default as default
|
|
2191
|
+
};
|