opencode-manifold 0.5.13 → 0.5.15
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/README.md +86 -80
- package/dist/index.js +302 -451
- package/dist/tui.js +7 -16
- package/package.json +1 -1
- package/src/templates/agents/erlang.md +30 -0
- package/src/templates/agents/lead.md +37 -0
- package/src/templates/agents/manifold.md +66 -133
- package/src/templates/agents/systems.md +30 -0
- package/src/templates/agents/todo.md +69 -151
- package/src/templates/skills/caveman/SKILL.md +0 -4
- package/src/templates/agents/clerk.md +0 -50
- package/src/templates/agents/debug.md +0 -76
- package/src/templates/agents/junior-dev.md +0 -81
- package/src/templates/agents/senior-dev.md +0 -73
- package/src/templates/manifold/graph/.gitkeep +0 -0
- package/src/templates/manifold/index.md +0 -11
- package/src/templates/manifold/log.md +0 -10
- package/src/templates/manifold/plans.json +0 -1
- package/src/templates/manifold/schema.md +0 -234
- package/src/templates/manifold/state.json +0 -11
- package/src/templates/skills/clerk-orchestration/SKILL.md +0 -139
- package/src/templates/skills/research/SKILL.md +0 -75
- package/src/templates/skills/wiki-ingest/SKILL.md +0 -139
- package/src/templates/skills/wiki-query/SKILL.md +0 -94
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __returnValue = (v) => v;
|
|
3
4
|
function __exportSetter(name, newValue) {
|
|
@@ -13,11 +14,12 @@ var __export = (target, all) => {
|
|
|
13
14
|
});
|
|
14
15
|
};
|
|
15
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
16
18
|
|
|
17
19
|
// src/tools/get-model-path.ts
|
|
18
20
|
var exports_get_model_path = {};
|
|
19
21
|
__export(exports_get_model_path, {
|
|
20
|
-
setPluginContext: () =>
|
|
22
|
+
setPluginContext: () => setPluginContext,
|
|
21
23
|
getModelPathTool: () => getModelPathTool,
|
|
22
24
|
getModelPath: () => getModelPath
|
|
23
25
|
});
|
|
@@ -71,16 +73,16 @@ async function getModelPath(client, sessionID) {
|
|
|
71
73
|
}
|
|
72
74
|
return "No active model found";
|
|
73
75
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
+
function setPluginContext(client) {
|
|
77
|
+
pluginClient = client;
|
|
76
78
|
}
|
|
77
79
|
function getModelPathClient() {
|
|
78
|
-
if (!
|
|
80
|
+
if (!pluginClient) {
|
|
79
81
|
throw new Error("Plugin client not initialized");
|
|
80
82
|
}
|
|
81
|
-
return
|
|
83
|
+
return pluginClient;
|
|
82
84
|
}
|
|
83
|
-
var
|
|
85
|
+
var pluginClient = null, getModelPathTool;
|
|
84
86
|
var init_get_model_path = __esm(() => {
|
|
85
87
|
getModelPathTool = tool2({
|
|
86
88
|
description: "Returns the model path (provider/model) for the currently active session. Use this to get the correct model string for agent MD file model: attributes.",
|
|
@@ -99,50 +101,11 @@ import { existsSync } from "fs";
|
|
|
99
101
|
import { join, dirname } from "path";
|
|
100
102
|
import { fileURLToPath } from "url";
|
|
101
103
|
import { homedir } from "os";
|
|
102
|
-
import { createRequire } from "module";
|
|
104
|
+
import { createRequire as createRequire2 } from "module";
|
|
103
105
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
104
|
-
var require2 =
|
|
106
|
+
var require2 = createRequire2(import.meta.url);
|
|
105
107
|
var bundledTemplatesDir = join(__dirname2, "..", "src", "templates");
|
|
106
108
|
var globalTemplatesDir = join(homedir(), ".config", "opencode", "manifold");
|
|
107
|
-
async function getPluginVersion() {
|
|
108
|
-
try {
|
|
109
|
-
const packageJson = require2(join(__dirname2, "..", "package.json"));
|
|
110
|
-
return packageJson.version || "unknown";
|
|
111
|
-
} catch {
|
|
112
|
-
return "unknown";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
async function dirHasContent(dirPath) {
|
|
116
|
-
if (!existsSync(dirPath))
|
|
117
|
-
return false;
|
|
118
|
-
try {
|
|
119
|
-
const entries = await readdir(dirPath);
|
|
120
|
-
return entries.length > 0;
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
async function copyMissingFiles(src, dest) {
|
|
126
|
-
if (!existsSync(src))
|
|
127
|
-
return [];
|
|
128
|
-
await mkdir(dest, { recursive: true });
|
|
129
|
-
const copied = [];
|
|
130
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
131
|
-
for (const entry of entries) {
|
|
132
|
-
const srcPath = join(src, entry.name);
|
|
133
|
-
const destPath = join(dest, entry.name);
|
|
134
|
-
if (entry.isDirectory()) {
|
|
135
|
-
const subCopied = await copyMissingFiles(srcPath, destPath);
|
|
136
|
-
if (subCopied.length > 0) {
|
|
137
|
-
copied.push(entry.name);
|
|
138
|
-
}
|
|
139
|
-
} else if (!existsSync(destPath)) {
|
|
140
|
-
await writeFile(destPath, await readFile(srcPath));
|
|
141
|
-
copied.push(entry.name);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return copied;
|
|
145
|
-
}
|
|
146
109
|
async function copyFiles(src, dest) {
|
|
147
110
|
if (!existsSync(src))
|
|
148
111
|
return [];
|
|
@@ -206,461 +169,349 @@ async function ensureGlobalTemplates(ctx) {
|
|
|
206
169
|
}
|
|
207
170
|
});
|
|
208
171
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
172
|
+
|
|
173
|
+
// src/tools/execute-task.ts
|
|
174
|
+
import { tool } from "@opencode-ai/plugin";
|
|
175
|
+
import { z } from "zod";
|
|
176
|
+
|
|
177
|
+
// src/lib/agent-invoker.ts
|
|
178
|
+
async function invokeAgent(client, options) {
|
|
179
|
+
const {
|
|
180
|
+
agent,
|
|
181
|
+
prompt,
|
|
182
|
+
model,
|
|
183
|
+
tools,
|
|
184
|
+
system,
|
|
185
|
+
maxPollAttempts = 300,
|
|
186
|
+
pollIntervalMs = 1000
|
|
187
|
+
} = options;
|
|
188
|
+
let sessionID;
|
|
189
|
+
try {
|
|
190
|
+
const createRes = await client.session.create({
|
|
191
|
+
body: { title: `manifold-${agent}` }
|
|
192
|
+
});
|
|
193
|
+
if (createRes.error || !createRes.data) {
|
|
194
|
+
throw new Error(`Failed to create session: ${JSON.stringify(createRes.error)}`);
|
|
216
195
|
}
|
|
217
|
-
|
|
218
|
-
|
|
196
|
+
sessionID = createRes.data.id;
|
|
197
|
+
await client.app.log({
|
|
198
|
+
body: {
|
|
199
|
+
service: "opencode-manifold",
|
|
200
|
+
level: "info",
|
|
201
|
+
message: `Invoking agent "${agent}" in session ${sessionID}`
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const promptBody = {
|
|
205
|
+
agent,
|
|
206
|
+
parts: [{ type: "text", text: prompt }]
|
|
207
|
+
};
|
|
208
|
+
if (model)
|
|
209
|
+
promptBody.model = model;
|
|
210
|
+
if (tools)
|
|
211
|
+
promptBody.tools = tools;
|
|
212
|
+
if (system)
|
|
213
|
+
promptBody.system = system;
|
|
214
|
+
const promptRes = await client.session.promptAsync({
|
|
215
|
+
path: { id: sessionID },
|
|
216
|
+
body: promptBody
|
|
217
|
+
});
|
|
218
|
+
if (promptRes.error) {
|
|
219
|
+
throw new Error(`Failed to send prompt: ${JSON.stringify(promptRes.error)}`);
|
|
220
|
+
}
|
|
221
|
+
const text = await pollForResponse(client, sessionID, maxPollAttempts, pollIntervalMs);
|
|
222
|
+
await client.app.log({
|
|
223
|
+
body: {
|
|
224
|
+
service: "opencode-manifold",
|
|
225
|
+
level: "info",
|
|
226
|
+
message: `Agent "${agent}" completed in session ${sessionID}`
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return { success: true, text, sessionID };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
232
|
await client.app.log({
|
|
220
233
|
body: {
|
|
221
234
|
service: "opencode-manifold",
|
|
222
235
|
level: "error",
|
|
223
|
-
message: `
|
|
236
|
+
message: `Agent "${agent}" failed: ${message}`
|
|
224
237
|
}
|
|
225
238
|
});
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const skillsCopied = await copyMissingFiles(join(globalTemplatesDir, "skills"), join(directory, ".opencode", "skills"));
|
|
233
|
-
if (skillsCopied.length > 0) {
|
|
234
|
-
initialized.push(`skills (${skillsCopied.join(", ")})`);
|
|
235
|
-
}
|
|
236
|
-
const manifoldCopied = await copyMissingFiles(join(globalTemplatesDir, "manifold"), join(directory, "Manifold"));
|
|
237
|
-
if (manifoldCopied.length > 0) {
|
|
238
|
-
initialized.push(`Manifold/ (${manifoldCopied.join(", ")})`);
|
|
239
|
-
}
|
|
240
|
-
const projectCommandsDir = join(directory, ".opencode", "commands");
|
|
241
|
-
const commandsToCopy = [];
|
|
242
|
-
const commandsInitialized = [];
|
|
243
|
-
for (const cmd of commandsToCopy) {
|
|
244
|
-
const src = join(globalTemplatesDir, "commands", cmd);
|
|
245
|
-
const dest = join(projectCommandsDir, cmd);
|
|
246
|
-
if (existsSync(src) && !existsSync(dest)) {
|
|
247
|
-
await copyFile(src, dest);
|
|
248
|
-
commandsInitialized.push(cmd);
|
|
239
|
+
return { success: false, text: "", sessionID: sessionID || "", error: message };
|
|
240
|
+
} finally {
|
|
241
|
+
if (sessionID) {
|
|
242
|
+
try {
|
|
243
|
+
await client.session.delete({ path: { id: sessionID } });
|
|
244
|
+
} catch {}
|
|
249
245
|
}
|
|
250
246
|
}
|
|
251
|
-
if (commandsInitialized.length > 0) {
|
|
252
|
-
initialized.push(`commands (${commandsInitialized.join(", ")})`);
|
|
253
|
-
}
|
|
254
|
-
const version = await getPluginVersion();
|
|
255
|
-
await writeFile(join(directory, "Manifold", "VERSION"), version + `
|
|
256
|
-
`);
|
|
257
|
-
await client.app.log({
|
|
258
|
-
body: {
|
|
259
|
-
service: "opencode-manifold",
|
|
260
|
-
level: "info",
|
|
261
|
-
message: `/manifold-init complete: ${initialized.join(", ") || "already initialized"}`
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
return initialized;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/tools/dispatch-task.ts
|
|
268
|
-
import { tool } from "@opencode-ai/plugin";
|
|
269
|
-
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
270
|
-
import { existsSync as existsSync2 } from "fs";
|
|
271
|
-
import { join as join2 } from "path";
|
|
272
|
-
var pluginClient = null;
|
|
273
|
-
function setPluginContext(client) {
|
|
274
|
-
pluginClient = client;
|
|
275
247
|
}
|
|
276
|
-
async function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
248
|
+
async function pollForResponse(client, sessionID, maxAttempts, intervalMs) {
|
|
249
|
+
let lastMessageCount = 0;
|
|
250
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
251
|
+
await sleep(intervalMs);
|
|
252
|
+
const messagesRes = await client.session.messages({
|
|
253
|
+
path: { id: sessionID }
|
|
254
|
+
});
|
|
255
|
+
if (messagesRes.error || !messagesRes.data)
|
|
256
|
+
continue;
|
|
257
|
+
const messages = messagesRes.data;
|
|
258
|
+
if (messages.length <= lastMessageCount)
|
|
259
|
+
continue;
|
|
260
|
+
lastMessageCount = messages.length;
|
|
261
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
262
|
+
const msg = messages[i];
|
|
263
|
+
const info = msg.info;
|
|
264
|
+
if (info?.role === "assistant") {
|
|
265
|
+
return extractText(msg.parts);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
281
268
|
}
|
|
282
|
-
|
|
283
|
-
maxLoops: 3,
|
|
284
|
-
maxRetries: 1,
|
|
285
|
-
maxResults: 10,
|
|
286
|
-
recentTaskCount: 3,
|
|
287
|
-
clerkRetryEnabled: true,
|
|
288
|
-
timeout: 300,
|
|
289
|
-
testCommand: null
|
|
290
|
-
};
|
|
269
|
+
throw new Error(`Timed out waiting for agent response after ${maxAttempts} attempts`);
|
|
291
270
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (existsSync2(statePath)) {
|
|
295
|
-
const content = await readFile2(statePath, "utf-8");
|
|
296
|
-
const states = JSON.parse(content);
|
|
297
|
-
return states[taskNumber] || null;
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
271
|
+
function extractText(parts) {
|
|
272
|
+
return parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
300
273
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
let states = {};
|
|
304
|
-
if (existsSync2(statePath)) {
|
|
305
|
-
const content = await readFile2(statePath, "utf-8");
|
|
306
|
-
states = JSON.parse(content);
|
|
307
|
-
}
|
|
308
|
-
states[state.task_number] = state;
|
|
309
|
-
await writeFile2(statePath, JSON.stringify(states, null, 2));
|
|
274
|
+
function sleep(ms) {
|
|
275
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
310
276
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
277
|
+
|
|
278
|
+
// src/tools/execute-task.ts
|
|
279
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
280
|
+
import { existsSync as existsSync2 } from "fs";
|
|
281
|
+
import { join as join2 } from "path";
|
|
282
|
+
var TaskSchema = z.object({
|
|
283
|
+
task_number: z.number().describe("The task number from the todo list"),
|
|
284
|
+
title: z.string().describe("Short task title"),
|
|
285
|
+
description: z.string().describe("Full task description"),
|
|
286
|
+
context_notes: z.string().describe("Pre-loaded context from Todo agent research: relevant files, patterns, prior work, gotchas"),
|
|
287
|
+
plan_file: z.string().describe("Path to the todo.md file")
|
|
288
|
+
});
|
|
289
|
+
var executeTaskTool = (client, directory) => tool(TaskSchema, {
|
|
290
|
+
async execute(params) {
|
|
291
|
+
const { task_number, title, description, context_notes, plan_file } = params;
|
|
292
|
+
await client.app.log({
|
|
293
|
+
body: {
|
|
294
|
+
service: "opencode-manifold",
|
|
295
|
+
level: "info",
|
|
296
|
+
message: `Task ${task_number}: ${title}`
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
const priorTasks = await readPriorTasks(directory, 3);
|
|
300
|
+
const directive = buildDirective(title, description, context_notes, priorTasks);
|
|
301
|
+
await client.app.log({
|
|
302
|
+
body: { service: "opencode-manifold", level: "info", message: `Initiating Socratic Dialectic...` }
|
|
303
|
+
});
|
|
304
|
+
const [erlangResult, systemsResult] = await Promise.all([
|
|
305
|
+
invokeAgent(client, {
|
|
306
|
+
agent: "erlang",
|
|
307
|
+
prompt: `TASK: ${title}
|
|
308
|
+
${directive}`
|
|
309
|
+
}),
|
|
310
|
+
invokeAgent(client, {
|
|
311
|
+
agent: "systems",
|
|
312
|
+
prompt: `TASK: ${title}
|
|
313
|
+
${directive}`
|
|
314
|
+
})
|
|
315
|
+
]);
|
|
316
|
+
if (!erlangResult.success || !systemsResult.success) {
|
|
317
|
+
const msg2 = `Initial Parallel Phase failed: Erlang(${erlangResult.success}), Systems(${systemsResult.success})`;
|
|
318
|
+
await logResult(directory, task_number, title, "escalated", msg2, "");
|
|
319
|
+
return { task_number, status: "escalated", summary: msg2, loops: 1 };
|
|
319
320
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
phase: "awaiting_outcome"
|
|
356
|
-
},
|
|
357
|
-
message: "Waiting for senior dev outcome"
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
if (state.phase === "awaiting_outcome") {
|
|
361
|
-
const isSuccess = args.success === true;
|
|
362
|
-
if (isSuccess) {
|
|
321
|
+
await client.app.log({
|
|
322
|
+
body: { service: "opencode-manifold", level: "info", message: `Synthesizing Consensus...` }
|
|
323
|
+
});
|
|
324
|
+
const leadResult = await invokeAgent(client, {
|
|
325
|
+
agent: "lead",
|
|
326
|
+
prompt: `TASK: ${title}
|
|
327
|
+
|
|
328
|
+
ORIGINAL DIRECTIVE:
|
|
329
|
+
${directive}
|
|
330
|
+
|
|
331
|
+
--- SOLUTION A (ERLANG/DX) ---
|
|
332
|
+
${erlangResult.text}
|
|
333
|
+
|
|
334
|
+
--- SOLUTION B (SYSTEMS/SECURITY) ---
|
|
335
|
+
${systemsResult.text}`
|
|
336
|
+
});
|
|
337
|
+
if (!leadResult.success) {
|
|
338
|
+
const msg2 = `Synthesis Phase failed: ${leadResult.error}`;
|
|
339
|
+
await logResult(directory, task_number, title, "escalated", msg2, "");
|
|
340
|
+
return { task_number, status: "escalated", summary: msg2, loops: 1 };
|
|
341
|
+
}
|
|
342
|
+
const finalImplementation = leadResult.text;
|
|
343
|
+
const reviewResult = await invokeAgent(client, {
|
|
344
|
+
agent: "lead",
|
|
345
|
+
prompt: `Review the following implementation against the original task: "${title}".
|
|
346
|
+
Original Task: ${description}
|
|
347
|
+
Implementation: ${finalImplementation}
|
|
348
|
+
|
|
349
|
+
If it is complete and correct, respond ONLY with "COMPLETE". If there are glaring errors or missing pieces, describe the questions/fixes needed starting with "QUESTIONS".`
|
|
350
|
+
});
|
|
351
|
+
const review = reviewResult.success ? reviewResult.text.trim() : "QUESTIONS: Review failed.";
|
|
352
|
+
const firstWord = review.split(/\s+/)[0].toUpperCase();
|
|
353
|
+
if (firstWord === "COMPLETE") {
|
|
354
|
+
await logResult(directory, task_number, title, "completed", extractSummary(finalImplementation), finalImplementation);
|
|
355
|
+
await markTodoDone(join2(directory, plan_file), task_number);
|
|
363
356
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
phase: "logging"
|
|
370
|
-
},
|
|
371
|
-
message: "Task succeeded - logging results"
|
|
357
|
+
task_number,
|
|
358
|
+
status: "completed",
|
|
359
|
+
summary: extractSummary(finalImplementation),
|
|
360
|
+
loops: 1,
|
|
361
|
+
files_changed: extractFiles(finalImplementation)
|
|
372
362
|
};
|
|
373
|
-
} else {
|
|
374
|
-
if (state.clerk_retries < settings.maxRetries) {
|
|
375
|
-
return {
|
|
376
|
-
route: "reprompt",
|
|
377
|
-
agent: "clerk",
|
|
378
|
-
prompt: buildRePromptPrompt(state, taskDescription),
|
|
379
|
-
state: {
|
|
380
|
-
task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
|
|
381
|
-
phase: "awaiting_execution"
|
|
382
|
-
},
|
|
383
|
-
message: `Retrying task (attempt ${state.clerk_retries + 1}/${settings.maxRetries})`
|
|
384
|
-
};
|
|
385
|
-
} else {
|
|
386
|
-
return {
|
|
387
|
-
route: "escalate",
|
|
388
|
-
agent: null,
|
|
389
|
-
prompt: buildEscalationPrompt(state, taskDescription),
|
|
390
|
-
state: {
|
|
391
|
-
task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
|
|
392
|
-
phase: "escalated"
|
|
393
|
-
},
|
|
394
|
-
message: "Escalating to user - all retries exhausted"
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
363
|
}
|
|
364
|
+
const msg = `Socratic Synthesis requires refinement: ${review}`;
|
|
365
|
+
await logResult(directory, task_number, title, "escalated", msg, finalImplementation);
|
|
366
|
+
return { task_number, status: "escalated", summary: msg, loops: 1 };
|
|
398
367
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
prompt: "Dispatcher encountered unexpected state",
|
|
403
|
-
state: {
|
|
404
|
-
task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
|
|
405
|
-
phase: "escalated"
|
|
406
|
-
},
|
|
407
|
-
message: "Unexpected state - escalating"
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
function buildExecutePrompt(taskNumber, taskDescription) {
|
|
411
|
-
return `CLERK - EXECUTE TASK
|
|
412
|
-
TASK NUMBER: ${taskNumber}
|
|
413
|
-
TASK: ${taskDescription}
|
|
414
|
-
|
|
415
|
-
INSTRUCTIONS:
|
|
416
|
-
|
|
417
|
-
1. RESEARCH
|
|
418
|
-
Use the Research skill with the given task to gather potentially relevant context.
|
|
368
|
+
});
|
|
369
|
+
function buildDirective(title, description, contextNotes, priorTasks) {
|
|
370
|
+
return `## Directive
|
|
419
371
|
|
|
420
|
-
|
|
421
|
-
Combine into a scoped prompt:
|
|
422
|
-
- Task goal (what Senior Dev must accomplish)
|
|
423
|
-
- Relevant code snippets from research (3-10, include file paths)
|
|
424
|
-
- Prior decisions from wiki logs
|
|
425
|
-
- Design guidelines (language/framework conventions, patterns)
|
|
426
|
-
Balance: enough to be useful, not overwhelming. Target ~800 words max.
|
|
372
|
+
**Task:** ${title}
|
|
427
373
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
- If task has purity tag ([pure], [shell], [mixed]):
|
|
431
|
-
- [pure]: Frame as data transformation, specify input/output types, emphasize no IO/side effects
|
|
432
|
-
- [shell]: Clarify IO boundary, what external system involved
|
|
433
|
-
- [mixed]: Note where logic/IO boundary falls
|
|
434
|
-
- Don't force functional style if task is naturally imperative
|
|
374
|
+
**Description:**
|
|
375
|
+
${description}
|
|
435
376
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
- Header: Date, Status=IN_PROGRESS, Task Description
|
|
439
|
-
- Include the scoped prompt you composed
|
|
440
|
-
- List context documents used (files, wiki logs, graphs)
|
|
377
|
+
**Context Notes (from pre-planning research):**
|
|
378
|
+
${contextNotes}
|
|
441
379
|
|
|
442
|
-
|
|
443
|
-
|
|
380
|
+
**Prior Tasks (compound knowledge):**
|
|
381
|
+
${priorTasks}
|
|
444
382
|
|
|
445
|
-
|
|
446
|
-
When senior-dev returns, determine if it reported "task complete" or "task failure".
|
|
447
|
-
Call dispatchTask() again (no arguments needed - the tool will ask for the result next).
|
|
383
|
+
---
|
|
448
384
|
|
|
449
|
-
|
|
450
|
-
- Do not make autonomous decisions about the task lifecycle.
|
|
451
|
-
- Wait for the dispatcher's next instruction after senior-dev completes.`;
|
|
385
|
+
**Implement this task. Follow the Context Notes. Use \`codebase_search\` if you need to clarify something the Context Notes don't cover.**`;
|
|
452
386
|
}
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
387
|
+
async function readPriorTasks(directory, n) {
|
|
388
|
+
const tasksDir = join2(directory, "Manifold", "tasks");
|
|
389
|
+
if (!existsSync2(tasksDir))
|
|
390
|
+
return "No prior tasks.";
|
|
391
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
392
|
+
const entries = await readdir2(tasksDir);
|
|
393
|
+
const md = entries.filter((e) => e.endsWith(".md")).sort().reverse();
|
|
394
|
+
const selected = md.slice(0, n);
|
|
395
|
+
if (selected.length === 0)
|
|
396
|
+
return "No prior tasks.";
|
|
397
|
+
let out = "";
|
|
398
|
+
for (const f of selected) {
|
|
399
|
+
const content = await readFile2(join2(tasksDir, f), "utf-8").catch(() => "");
|
|
400
|
+
const summaryMatch = content.match(/## Summary\n([\s\S]*?)(?=\n## |$)/);
|
|
401
|
+
const filesMatch = content.match(/## Files Touched\n([\s\S]*?)(?=\n## |$)/);
|
|
402
|
+
out += `
|
|
403
|
+
--- ${f} ---
|
|
404
|
+
`;
|
|
405
|
+
if (summaryMatch)
|
|
406
|
+
out += `Summary: ${summaryMatch[1].trim()}
|
|
407
|
+
`;
|
|
408
|
+
if (filesMatch)
|
|
409
|
+
out += `Files: ${filesMatch[1].trim()}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
return out || "Prior tasks exist but no summaries extracted.";
|
|
462
413
|
}
|
|
463
|
-
function
|
|
464
|
-
const taskId = `${state.slug}-${state.task_number.toString().padStart(3, "0")}`;
|
|
414
|
+
async function logResult(directory, taskNumber, title, status, summary, implementation) {
|
|
465
415
|
const date = new Date().toISOString().split("T")[0];
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
INSTRUCTIONS:
|
|
471
|
-
1. Update task log at \`Manifold/tasks/${taskId}.md\`:
|
|
472
|
-
- Change Status from IN_PROGRESS to COMPLETED (or FAILED if applicable)
|
|
473
|
-
- Add senior-dev's final implementation summary
|
|
474
|
-
- Document what was done and files changed
|
|
475
|
-
- If failed: document what was tried and why it failed
|
|
416
|
+
const tasksDir = join2(directory, "Manifold", "tasks");
|
|
417
|
+
await mkdir2(tasksDir, { recursive: true });
|
|
418
|
+
const taskPath = join2(tasksDir, `${taskNumber.toString().padStart(3, "0")}-${slugify(title)}.md`);
|
|
419
|
+
let content = `# Task ${taskNumber}: ${title}
|
|
476
420
|
|
|
477
|
-
|
|
478
|
-
|
|
421
|
+
`;
|
|
422
|
+
content += `**Date:** ${date}
|
|
423
|
+
`;
|
|
424
|
+
content += `**Status:** ${status.toUpperCase()}
|
|
479
425
|
|
|
480
|
-
|
|
481
|
-
|
|
426
|
+
`;
|
|
427
|
+
content += `## Summary
|
|
428
|
+
${summary}
|
|
482
429
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
- Replace \`/\` with \`__SL__\` and \`.\` with \`__DT__\` in filenames
|
|
487
|
-
- Example: \`src/middleware/auth.ts\` → \`Manifold/graph/src__SL__middleware__SL__auth__DT__ts.md\`
|
|
488
|
-
|
|
489
|
-
AFTER COMPLETING LOGGING:
|
|
490
|
-
Call dispatchTask() with no arguments. The dispatcher will complete the task and reset state.`;
|
|
491
|
-
}
|
|
492
|
-
function buildRePromptPrompt(state, taskDescription) {
|
|
493
|
-
const attemptNumber = state.clerk_retries + 1;
|
|
494
|
-
return `CLERK - RECOVERY ATTEMPT ${attemptNumber}
|
|
495
|
-
TASK: ${taskDescription}
|
|
496
|
-
PREVIOUS ATTEMPT FAILED
|
|
430
|
+
`;
|
|
431
|
+
if (implementation) {
|
|
432
|
+
content += `## Implementation
|
|
497
433
|
|
|
498
|
-
|
|
499
|
-
1. Call @senior-dev again with the same task description
|
|
500
|
-
2. Include context from the previous failure:
|
|
501
|
-
- The previous implementation was not accepted
|
|
502
|
-
- Senior-dev should try a different approach
|
|
503
|
-
3. Senior-dev will again manage the review loop internally
|
|
434
|
+
${implementation}
|
|
504
435
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
The task has failed after ${state.clerk_retries} recovery attempts.
|
|
513
|
-
|
|
514
|
-
INSTRUCTIONS:
|
|
515
|
-
1. Log the failure to \`Manifold/tasks/<task-id>.md\` with status=FAILED
|
|
516
|
-
2. Report to the user:
|
|
517
|
-
- Task description: ${taskDescription}
|
|
518
|
-
- Status: FAILED
|
|
519
|
-
- Attempts made: ${state.clerk_retries}
|
|
520
|
-
- Summary of what was tried and why it failed
|
|
436
|
+
`;
|
|
437
|
+
}
|
|
438
|
+
const files = extractFiles(implementation);
|
|
439
|
+
if (files.length > 0) {
|
|
440
|
+
content += `## Files Touched
|
|
441
|
+
${files.map((f) => `- \`${f}\``).join(`
|
|
442
|
+
`)}
|
|
521
443
|
|
|
522
|
-
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
await writeFile2(taskPath, content);
|
|
447
|
+
const memoryPath = join2(directory, "Manifold", "memory.md");
|
|
448
|
+
let memory = "";
|
|
449
|
+
if (existsSync2(memoryPath)) {
|
|
450
|
+
memory = await readFile2(memoryPath, "utf-8");
|
|
451
|
+
}
|
|
452
|
+
memory += `
|
|
453
|
+
## ${date} task-${taskNumber}
|
|
454
|
+
`;
|
|
455
|
+
memory += `**${title}** — ${status.toUpperCase()}
|
|
456
|
+
`;
|
|
457
|
+
memory += `- ${summary.substring(0, 300)}
|
|
458
|
+
`;
|
|
459
|
+
if (files.length > 0) {
|
|
460
|
+
memory += `- Files: ${files.join(", ")}
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
await writeFile2(memoryPath, memory.trim() + `
|
|
464
|
+
`);
|
|
523
465
|
}
|
|
524
|
-
function
|
|
525
|
-
if (!
|
|
526
|
-
|
|
466
|
+
async function markTodoDone(planPath, taskNumber) {
|
|
467
|
+
if (!existsSync2(planPath))
|
|
468
|
+
return;
|
|
469
|
+
const content = await readFile2(planPath, "utf-8");
|
|
470
|
+
const regex = new RegExp(`^(## Task ${taskNumber}:.*)$`, "gm");
|
|
471
|
+
const updated = content.replace(regex, "$1 [x]");
|
|
472
|
+
if (updated !== content) {
|
|
473
|
+
await writeFile2(planPath, updated);
|
|
527
474
|
}
|
|
528
|
-
return pluginClient;
|
|
529
475
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
success: tool.schema.boolean().optional().describe("TRUE if senior-dev completed task successfully, FALSE if failed")
|
|
537
|
-
},
|
|
538
|
-
async execute(args, context) {
|
|
539
|
-
const { task_number, plan_file, description } = args;
|
|
540
|
-
const client = getClient();
|
|
541
|
-
const directory = context.directory;
|
|
542
|
-
await client.app.log({
|
|
543
|
-
body: {
|
|
544
|
-
service: "opencode-manifold",
|
|
545
|
-
level: "info",
|
|
546
|
-
message: `dispatchTask called: task ${task_number || "unknown"}`
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
const settings = await readSettings(directory);
|
|
550
|
-
const existingState = task_number ? await readDispatcherState(directory, task_number) : null;
|
|
551
|
-
let taskDescription = description || "";
|
|
552
|
-
if (!taskDescription && args.plan_file && task_number) {
|
|
553
|
-
try {
|
|
554
|
-
const planPath = join2(directory, args.plan_file);
|
|
555
|
-
if (existsSync2(planPath)) {
|
|
556
|
-
const planContent = await readFile2(planPath, "utf-8");
|
|
557
|
-
const taskInfo = extractTaskFromPlan(planContent, task_number);
|
|
558
|
-
if (taskInfo) {
|
|
559
|
-
taskDescription = taskInfo.description;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
} catch (error) {
|
|
563
|
-
await client.app.log({
|
|
564
|
-
body: {
|
|
565
|
-
service: "opencode-manifold",
|
|
566
|
-
level: "error",
|
|
567
|
-
message: `Failed to read plan file: ${error}`
|
|
568
|
-
}
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const result = runDispatcherLogic(args, existingState, settings, taskDescription);
|
|
573
|
-
let newPhase = "init";
|
|
574
|
-
if (result.route === "execute")
|
|
575
|
-
newPhase = "awaiting_execution";
|
|
576
|
-
else if (result.route === "outcome_check")
|
|
577
|
-
newPhase = "awaiting_outcome";
|
|
578
|
-
else if (result.route === "logging")
|
|
579
|
-
newPhase = "logging";
|
|
580
|
-
else if (result.route === "complete")
|
|
581
|
-
newPhase = "complete";
|
|
582
|
-
else if (result.route === "escalate")
|
|
583
|
-
newPhase = "escalated";
|
|
584
|
-
else if (result.route === "reprompt")
|
|
585
|
-
newPhase = "awaiting_execution";
|
|
586
|
-
if (task_number && result.state.task_id) {
|
|
587
|
-
if (result.route === "logging" || result.route === "escalate") {
|
|
588
|
-
const newState = {
|
|
589
|
-
task_number,
|
|
590
|
-
plan_file: plan_file || "",
|
|
591
|
-
slug: result.state.task_id.split("-")[0],
|
|
592
|
-
phase: newPhase,
|
|
593
|
-
senior_output: args.senior_output || existingState?.senior_output || "",
|
|
594
|
-
clerk_summary: args.clerk_summary || existingState?.clerk_summary || "",
|
|
595
|
-
loops: existingState?.loops || 0,
|
|
596
|
-
clerk_retries: existingState?.clerk_retries || 0
|
|
597
|
-
};
|
|
598
|
-
await writeDispatcherState(directory, newState);
|
|
599
|
-
} else {
|
|
600
|
-
const newState = {
|
|
601
|
-
task_number,
|
|
602
|
-
plan_file: plan_file || "",
|
|
603
|
-
slug: result.state.task_id.split("-")[0],
|
|
604
|
-
phase: newPhase,
|
|
605
|
-
senior_output: args.senior_output || existingState?.senior_output || "",
|
|
606
|
-
clerk_summary: args.clerk_summary || existingState?.clerk_summary || "",
|
|
607
|
-
loops: existingState?.loops || 0,
|
|
608
|
-
clerk_retries: existingState?.clerk_retries || 0
|
|
609
|
-
};
|
|
610
|
-
await writeDispatcherState(directory, newState);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
await client.app.log({
|
|
614
|
-
body: {
|
|
615
|
-
service: "opencode-manifold",
|
|
616
|
-
level: "info",
|
|
617
|
-
message: `dispatchTask result: ${result.route} → ${result.agent || "none"} (${result.message})`
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
return JSON.stringify(result);
|
|
476
|
+
function extractSummary(implementation) {
|
|
477
|
+
const lines = implementation.split(`
|
|
478
|
+
`).filter((l) => l.trim());
|
|
479
|
+
const marker = lines.find((l) => /^(?:Summary|IMPLEMENTATION SUMMARY|What was done):/i.test(l));
|
|
480
|
+
if (marker) {
|
|
481
|
+
return marker.replace(/^(?:Summary|IMPLEMENTATION SUMMARY|What was done):\s*/i, "").trim();
|
|
621
482
|
}
|
|
622
|
-
|
|
483
|
+
return lines[0]?.substring(0, 200) || "No summary";
|
|
484
|
+
}
|
|
485
|
+
function extractFiles(implementation) {
|
|
486
|
+
const files = [];
|
|
487
|
+
const mdMatches = implementation.matchAll(/\[`([^`]+)`\]/g);
|
|
488
|
+
for (const m of mdMatches)
|
|
489
|
+
files.push(m[1]);
|
|
490
|
+
const pathMatches = implementation.matchAll(/(?:src|lib|test|spec)\/[^:\s)]+\.(?:ts|tsx|js|jsx|py|rs|go)\b/g);
|
|
491
|
+
for (const m of pathMatches)
|
|
492
|
+
files.push(m[0]);
|
|
493
|
+
return [...new Set(files)];
|
|
494
|
+
}
|
|
495
|
+
function slugify(s) {
|
|
496
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 40);
|
|
497
|
+
}
|
|
623
498
|
|
|
624
499
|
// src/index.ts
|
|
625
500
|
init_get_model_path();
|
|
626
501
|
var ManifoldPlugin = async (ctx) => {
|
|
627
502
|
setPluginContext(ctx.client);
|
|
628
|
-
|
|
503
|
+
const { client, directory } = ctx;
|
|
629
504
|
await ensureGlobalTemplates(ctx);
|
|
630
|
-
await
|
|
505
|
+
await client.app.log({
|
|
631
506
|
body: {
|
|
632
507
|
service: "opencode-manifold",
|
|
633
508
|
level: "info",
|
|
634
|
-
message: "Open Manifold
|
|
509
|
+
message: "Open Manifold v2.1 loaded"
|
|
635
510
|
}
|
|
636
511
|
});
|
|
637
512
|
return {
|
|
638
513
|
tool: {
|
|
639
|
-
|
|
640
|
-
getModelPath: getModelPathTool
|
|
641
|
-
},
|
|
642
|
-
"command.execute.before": async (input, output) => {
|
|
643
|
-
if (input.command === "manifold-init") {
|
|
644
|
-
const initialized = await initProject(ctx.directory, ctx.client);
|
|
645
|
-
if (initialized.length > 0) {
|
|
646
|
-
output.parts = [
|
|
647
|
-
{
|
|
648
|
-
type: "text",
|
|
649
|
-
text: `Manifold initialized: ${initialized.join(", ")}`
|
|
650
|
-
}
|
|
651
|
-
];
|
|
652
|
-
} else {
|
|
653
|
-
output.parts = [
|
|
654
|
-
{
|
|
655
|
-
type: "text",
|
|
656
|
-
text: "All Manifold files already present. To reset a specific component, delete the corresponding file(s) from `.opencode/agents/`, `.opencode/skills/`, or `Manifold/`, then run `/manifold-init` again."
|
|
657
|
-
}
|
|
658
|
-
];
|
|
659
|
-
}
|
|
660
|
-
} else if (input.command === "manifold-model-path") {
|
|
661
|
-
const modelPath = await getModelPath(ctx.client, input.sessionID || undefined);
|
|
662
|
-
output.parts = [{ type: "text", text: modelPath }];
|
|
663
|
-
}
|
|
514
|
+
execute_task: executeTaskTool(client, directory)
|
|
664
515
|
}
|
|
665
516
|
};
|
|
666
517
|
};
|