opencode-manifold 0.5.14 → 0.5.16
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 +314 -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 +68 -125
- package/src/templates/agents/systems.md +30 -0
- package/src/templates/agents/todo.md +69 -160
- 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,23 @@ 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 =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
var require2 = createRequire2(import.meta.url);
|
|
107
|
+
function getBundledTemplatesDir() {
|
|
108
|
+
const possiblePaths = [
|
|
109
|
+
join(__dirname2, "..", "src", "templates"),
|
|
110
|
+
join(__dirname2, "templates"),
|
|
111
|
+
join(__dirname2, "..", "templates")
|
|
112
|
+
];
|
|
113
|
+
for (const p of possiblePaths) {
|
|
114
|
+
if (existsSync(p))
|
|
115
|
+
return p;
|
|
113
116
|
}
|
|
117
|
+
return possiblePaths[0];
|
|
114
118
|
}
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
}
|
|
119
|
+
var bundledTemplatesDir = getBundledTemplatesDir();
|
|
120
|
+
var globalTemplatesDir = join(homedir(), ".config", "opencode", "manifold");
|
|
146
121
|
async function copyFiles(src, dest) {
|
|
147
122
|
if (!existsSync(src))
|
|
148
123
|
return [];
|
|
@@ -206,461 +181,349 @@ async function ensureGlobalTemplates(ctx) {
|
|
|
206
181
|
}
|
|
207
182
|
});
|
|
208
183
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
184
|
+
|
|
185
|
+
// src/tools/execute-task.ts
|
|
186
|
+
import { tool } from "@opencode-ai/plugin";
|
|
187
|
+
import { z } from "zod";
|
|
188
|
+
|
|
189
|
+
// src/lib/agent-invoker.ts
|
|
190
|
+
async function invokeAgent(client, options) {
|
|
191
|
+
const {
|
|
192
|
+
agent,
|
|
193
|
+
prompt,
|
|
194
|
+
model,
|
|
195
|
+
tools,
|
|
196
|
+
system,
|
|
197
|
+
maxPollAttempts = 300,
|
|
198
|
+
pollIntervalMs = 1000
|
|
199
|
+
} = options;
|
|
200
|
+
let sessionID;
|
|
201
|
+
try {
|
|
202
|
+
const createRes = await client.session.create({
|
|
203
|
+
body: { title: `manifold-${agent}` }
|
|
204
|
+
});
|
|
205
|
+
if (createRes.error || !createRes.data) {
|
|
206
|
+
throw new Error(`Failed to create session: ${JSON.stringify(createRes.error)}`);
|
|
216
207
|
}
|
|
217
|
-
|
|
218
|
-
|
|
208
|
+
sessionID = createRes.data.id;
|
|
209
|
+
await client.app.log({
|
|
210
|
+
body: {
|
|
211
|
+
service: "opencode-manifold",
|
|
212
|
+
level: "info",
|
|
213
|
+
message: `Invoking agent "${agent}" in session ${sessionID}`
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const promptBody = {
|
|
217
|
+
agent,
|
|
218
|
+
parts: [{ type: "text", text: prompt }]
|
|
219
|
+
};
|
|
220
|
+
if (model)
|
|
221
|
+
promptBody.model = model;
|
|
222
|
+
if (tools)
|
|
223
|
+
promptBody.tools = tools;
|
|
224
|
+
if (system)
|
|
225
|
+
promptBody.system = system;
|
|
226
|
+
const promptRes = await client.session.promptAsync({
|
|
227
|
+
path: { id: sessionID },
|
|
228
|
+
body: promptBody
|
|
229
|
+
});
|
|
230
|
+
if (promptRes.error) {
|
|
231
|
+
throw new Error(`Failed to send prompt: ${JSON.stringify(promptRes.error)}`);
|
|
232
|
+
}
|
|
233
|
+
const text = await pollForResponse(client, sessionID, maxPollAttempts, pollIntervalMs);
|
|
234
|
+
await client.app.log({
|
|
235
|
+
body: {
|
|
236
|
+
service: "opencode-manifold",
|
|
237
|
+
level: "info",
|
|
238
|
+
message: `Agent "${agent}" completed in session ${sessionID}`
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return { success: true, text, sessionID };
|
|
242
|
+
} catch (error) {
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
244
|
await client.app.log({
|
|
220
245
|
body: {
|
|
221
246
|
service: "opencode-manifold",
|
|
222
247
|
level: "error",
|
|
223
|
-
message: `
|
|
248
|
+
message: `Agent "${agent}" failed: ${message}`
|
|
224
249
|
}
|
|
225
250
|
});
|
|
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);
|
|
251
|
+
return { success: false, text: "", sessionID: sessionID || "", error: message };
|
|
252
|
+
} finally {
|
|
253
|
+
if (sessionID) {
|
|
254
|
+
try {
|
|
255
|
+
await client.session.delete({ path: { id: sessionID } });
|
|
256
|
+
} catch {}
|
|
249
257
|
}
|
|
250
258
|
}
|
|
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
259
|
}
|
|
276
|
-
async function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
260
|
+
async function pollForResponse(client, sessionID, maxAttempts, intervalMs) {
|
|
261
|
+
let lastMessageCount = 0;
|
|
262
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
263
|
+
await sleep(intervalMs);
|
|
264
|
+
const messagesRes = await client.session.messages({
|
|
265
|
+
path: { id: sessionID }
|
|
266
|
+
});
|
|
267
|
+
if (messagesRes.error || !messagesRes.data)
|
|
268
|
+
continue;
|
|
269
|
+
const messages = messagesRes.data;
|
|
270
|
+
if (messages.length <= lastMessageCount)
|
|
271
|
+
continue;
|
|
272
|
+
lastMessageCount = messages.length;
|
|
273
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
274
|
+
const msg = messages[i];
|
|
275
|
+
const info = msg.info;
|
|
276
|
+
if (info?.role === "assistant") {
|
|
277
|
+
return extractText(msg.parts);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
281
280
|
}
|
|
282
|
-
|
|
283
|
-
maxLoops: 3,
|
|
284
|
-
maxRetries: 1,
|
|
285
|
-
maxResults: 10,
|
|
286
|
-
recentTaskCount: 3,
|
|
287
|
-
clerkRetryEnabled: true,
|
|
288
|
-
timeout: 300,
|
|
289
|
-
testCommand: null
|
|
290
|
-
};
|
|
281
|
+
throw new Error(`Timed out waiting for agent response after ${maxAttempts} attempts`);
|
|
291
282
|
}
|
|
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;
|
|
283
|
+
function extractText(parts) {
|
|
284
|
+
return parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
300
285
|
}
|
|
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));
|
|
286
|
+
function sleep(ms) {
|
|
287
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
310
288
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
289
|
+
|
|
290
|
+
// src/tools/execute-task.ts
|
|
291
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
292
|
+
import { existsSync as existsSync2 } from "fs";
|
|
293
|
+
import { join as join2 } from "path";
|
|
294
|
+
var TaskSchema = z.object({
|
|
295
|
+
task_number: z.number().describe("The task number from the todo list"),
|
|
296
|
+
title: z.string().describe("Short task title"),
|
|
297
|
+
description: z.string().describe("Full task description"),
|
|
298
|
+
context_notes: z.string().describe("Pre-loaded context from Todo agent research: relevant files, patterns, prior work, gotchas"),
|
|
299
|
+
plan_file: z.string().describe("Path to the todo.md file")
|
|
300
|
+
});
|
|
301
|
+
var executeTaskTool = (client, directory) => tool(TaskSchema, {
|
|
302
|
+
async execute(params) {
|
|
303
|
+
const { task_number, title, description, context_notes, plan_file } = params;
|
|
304
|
+
await client.app.log({
|
|
305
|
+
body: {
|
|
306
|
+
service: "opencode-manifold",
|
|
307
|
+
level: "info",
|
|
308
|
+
message: `Task ${task_number}: ${title}`
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const priorTasks = await readPriorTasks(directory, 3);
|
|
312
|
+
const directive = buildDirective(title, description, context_notes, priorTasks);
|
|
313
|
+
await client.app.log({
|
|
314
|
+
body: { service: "opencode-manifold", level: "info", message: `Initiating Socratic Dialectic...` }
|
|
315
|
+
});
|
|
316
|
+
const [erlangResult, systemsResult] = await Promise.all([
|
|
317
|
+
invokeAgent(client, {
|
|
318
|
+
agent: "erlang",
|
|
319
|
+
prompt: `TASK: ${title}
|
|
320
|
+
${directive}`
|
|
321
|
+
}),
|
|
322
|
+
invokeAgent(client, {
|
|
323
|
+
agent: "systems",
|
|
324
|
+
prompt: `TASK: ${title}
|
|
325
|
+
${directive}`
|
|
326
|
+
})
|
|
327
|
+
]);
|
|
328
|
+
if (!erlangResult.success || !systemsResult.success) {
|
|
329
|
+
const msg2 = `Initial Parallel Phase failed: Erlang(${erlangResult.success}), Systems(${systemsResult.success})`;
|
|
330
|
+
await logResult(directory, task_number, title, "escalated", msg2, "");
|
|
331
|
+
return { task_number, status: "escalated", summary: msg2, loops: 1 };
|
|
319
332
|
}
|
|
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) {
|
|
333
|
+
await client.app.log({
|
|
334
|
+
body: { service: "opencode-manifold", level: "info", message: `Synthesizing Consensus...` }
|
|
335
|
+
});
|
|
336
|
+
const leadResult = await invokeAgent(client, {
|
|
337
|
+
agent: "lead",
|
|
338
|
+
prompt: `TASK: ${title}
|
|
339
|
+
|
|
340
|
+
ORIGINAL DIRECTIVE:
|
|
341
|
+
${directive}
|
|
342
|
+
|
|
343
|
+
--- SOLUTION A (ERLANG/DX) ---
|
|
344
|
+
${erlangResult.text}
|
|
345
|
+
|
|
346
|
+
--- SOLUTION B (SYSTEMS/SECURITY) ---
|
|
347
|
+
${systemsResult.text}`
|
|
348
|
+
});
|
|
349
|
+
if (!leadResult.success) {
|
|
350
|
+
const msg2 = `Synthesis Phase failed: ${leadResult.error}`;
|
|
351
|
+
await logResult(directory, task_number, title, "escalated", msg2, "");
|
|
352
|
+
return { task_number, status: "escalated", summary: msg2, loops: 1 };
|
|
353
|
+
}
|
|
354
|
+
const finalImplementation = leadResult.text;
|
|
355
|
+
const reviewResult = await invokeAgent(client, {
|
|
356
|
+
agent: "lead",
|
|
357
|
+
prompt: `Review the following implementation against the original task: "${title}".
|
|
358
|
+
Original Task: ${description}
|
|
359
|
+
Implementation: ${finalImplementation}
|
|
360
|
+
|
|
361
|
+
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".`
|
|
362
|
+
});
|
|
363
|
+
const review = reviewResult.success ? reviewResult.text.trim() : "QUESTIONS: Review failed.";
|
|
364
|
+
const firstWord = review.split(/\s+/)[0].toUpperCase();
|
|
365
|
+
if (firstWord === "COMPLETE") {
|
|
366
|
+
await logResult(directory, task_number, title, "completed", extractSummary(finalImplementation), finalImplementation);
|
|
367
|
+
await markTodoDone(join2(directory, plan_file), task_number);
|
|
363
368
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
phase: "logging"
|
|
370
|
-
},
|
|
371
|
-
message: "Task succeeded - logging results"
|
|
369
|
+
task_number,
|
|
370
|
+
status: "completed",
|
|
371
|
+
summary: extractSummary(finalImplementation),
|
|
372
|
+
loops: 1,
|
|
373
|
+
files_changed: extractFiles(finalImplementation)
|
|
372
374
|
};
|
|
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
375
|
}
|
|
376
|
+
const msg = `Socratic Synthesis requires refinement: ${review}`;
|
|
377
|
+
await logResult(directory, task_number, title, "escalated", msg, finalImplementation);
|
|
378
|
+
return { task_number, status: "escalated", summary: msg, loops: 1 };
|
|
398
379
|
}
|
|
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.
|
|
380
|
+
});
|
|
381
|
+
function buildDirective(title, description, contextNotes, priorTasks) {
|
|
382
|
+
return `## Directive
|
|
419
383
|
|
|
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.
|
|
384
|
+
**Task:** ${title}
|
|
427
385
|
|
|
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
|
|
386
|
+
**Description:**
|
|
387
|
+
${description}
|
|
435
388
|
|
|
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)
|
|
389
|
+
**Context Notes (from pre-planning research):**
|
|
390
|
+
${contextNotes}
|
|
441
391
|
|
|
442
|
-
|
|
443
|
-
|
|
392
|
+
**Prior Tasks (compound knowledge):**
|
|
393
|
+
${priorTasks}
|
|
444
394
|
|
|
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).
|
|
395
|
+
---
|
|
448
396
|
|
|
449
|
-
|
|
450
|
-
- Do not make autonomous decisions about the task lifecycle.
|
|
451
|
-
- Wait for the dispatcher's next instruction after senior-dev completes.`;
|
|
397
|
+
**Implement this task. Follow the Context Notes. Use \`codebase_search\` if you need to clarify something the Context Notes don't cover.**`;
|
|
452
398
|
}
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
399
|
+
async function readPriorTasks(directory, n) {
|
|
400
|
+
const tasksDir = join2(directory, "Manifold", "tasks");
|
|
401
|
+
if (!existsSync2(tasksDir))
|
|
402
|
+
return "No prior tasks.";
|
|
403
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
404
|
+
const entries = await readdir2(tasksDir);
|
|
405
|
+
const md = entries.filter((e) => e.endsWith(".md")).sort().reverse();
|
|
406
|
+
const selected = md.slice(0, n);
|
|
407
|
+
if (selected.length === 0)
|
|
408
|
+
return "No prior tasks.";
|
|
409
|
+
let out = "";
|
|
410
|
+
for (const f of selected) {
|
|
411
|
+
const content = await readFile2(join2(tasksDir, f), "utf-8").catch(() => "");
|
|
412
|
+
const summaryMatch = content.match(/## Summary\n([\s\S]*?)(?=\n## |$)/);
|
|
413
|
+
const filesMatch = content.match(/## Files Touched\n([\s\S]*?)(?=\n## |$)/);
|
|
414
|
+
out += `
|
|
415
|
+
--- ${f} ---
|
|
416
|
+
`;
|
|
417
|
+
if (summaryMatch)
|
|
418
|
+
out += `Summary: ${summaryMatch[1].trim()}
|
|
419
|
+
`;
|
|
420
|
+
if (filesMatch)
|
|
421
|
+
out += `Files: ${filesMatch[1].trim()}
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
return out || "Prior tasks exist but no summaries extracted.";
|
|
462
425
|
}
|
|
463
|
-
function
|
|
464
|
-
const taskId = `${state.slug}-${state.task_number.toString().padStart(3, "0")}`;
|
|
426
|
+
async function logResult(directory, taskNumber, title, status, summary, implementation) {
|
|
465
427
|
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
|
|
428
|
+
const tasksDir = join2(directory, "Manifold", "tasks");
|
|
429
|
+
await mkdir2(tasksDir, { recursive: true });
|
|
430
|
+
const taskPath = join2(tasksDir, `${taskNumber.toString().padStart(3, "0")}-${slugify(title)}.md`);
|
|
431
|
+
let content = `# Task ${taskNumber}: ${title}
|
|
476
432
|
|
|
477
|
-
|
|
478
|
-
|
|
433
|
+
`;
|
|
434
|
+
content += `**Date:** ${date}
|
|
435
|
+
`;
|
|
436
|
+
content += `**Status:** ${status.toUpperCase()}
|
|
479
437
|
|
|
480
|
-
|
|
481
|
-
|
|
438
|
+
`;
|
|
439
|
+
content += `## Summary
|
|
440
|
+
${summary}
|
|
482
441
|
|
|
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
|
|
442
|
+
`;
|
|
443
|
+
if (implementation) {
|
|
444
|
+
content += `## Implementation
|
|
497
445
|
|
|
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
|
|
446
|
+
${implementation}
|
|
504
447
|
|
|
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
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
const files = extractFiles(implementation);
|
|
451
|
+
if (files.length > 0) {
|
|
452
|
+
content += `## Files Touched
|
|
453
|
+
${files.map((f) => `- \`${f}\``).join(`
|
|
454
|
+
`)}
|
|
521
455
|
|
|
522
|
-
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
await writeFile2(taskPath, content);
|
|
459
|
+
const memoryPath = join2(directory, "Manifold", "memory.md");
|
|
460
|
+
let memory = "";
|
|
461
|
+
if (existsSync2(memoryPath)) {
|
|
462
|
+
memory = await readFile2(memoryPath, "utf-8");
|
|
463
|
+
}
|
|
464
|
+
memory += `
|
|
465
|
+
## ${date} task-${taskNumber}
|
|
466
|
+
`;
|
|
467
|
+
memory += `**${title}** — ${status.toUpperCase()}
|
|
468
|
+
`;
|
|
469
|
+
memory += `- ${summary.substring(0, 300)}
|
|
470
|
+
`;
|
|
471
|
+
if (files.length > 0) {
|
|
472
|
+
memory += `- Files: ${files.join(", ")}
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
await writeFile2(memoryPath, memory.trim() + `
|
|
476
|
+
`);
|
|
523
477
|
}
|
|
524
|
-
function
|
|
525
|
-
if (!
|
|
526
|
-
|
|
478
|
+
async function markTodoDone(planPath, taskNumber) {
|
|
479
|
+
if (!existsSync2(planPath))
|
|
480
|
+
return;
|
|
481
|
+
const content = await readFile2(planPath, "utf-8");
|
|
482
|
+
const regex = new RegExp(`^(## Task ${taskNumber}:.*)$`, "gm");
|
|
483
|
+
const updated = content.replace(regex, "$1 [x]");
|
|
484
|
+
if (updated !== content) {
|
|
485
|
+
await writeFile2(planPath, updated);
|
|
527
486
|
}
|
|
528
|
-
return pluginClient;
|
|
529
487
|
}
|
|
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);
|
|
488
|
+
function extractSummary(implementation) {
|
|
489
|
+
const lines = implementation.split(`
|
|
490
|
+
`).filter((l) => l.trim());
|
|
491
|
+
const marker = lines.find((l) => /^(?:Summary|IMPLEMENTATION SUMMARY|What was done):/i.test(l));
|
|
492
|
+
if (marker) {
|
|
493
|
+
return marker.replace(/^(?:Summary|IMPLEMENTATION SUMMARY|What was done):\s*/i, "").trim();
|
|
621
494
|
}
|
|
622
|
-
|
|
495
|
+
return lines[0]?.substring(0, 200) || "No summary";
|
|
496
|
+
}
|
|
497
|
+
function extractFiles(implementation) {
|
|
498
|
+
const files = [];
|
|
499
|
+
const mdMatches = implementation.matchAll(/\[`([^`]+)`\]/g);
|
|
500
|
+
for (const m of mdMatches)
|
|
501
|
+
files.push(m[1]);
|
|
502
|
+
const pathMatches = implementation.matchAll(/(?:src|lib|test|spec)\/[^:\s)]+\.(?:ts|tsx|js|jsx|py|rs|go)\b/g);
|
|
503
|
+
for (const m of pathMatches)
|
|
504
|
+
files.push(m[0]);
|
|
505
|
+
return [...new Set(files)];
|
|
506
|
+
}
|
|
507
|
+
function slugify(s) {
|
|
508
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 40);
|
|
509
|
+
}
|
|
623
510
|
|
|
624
511
|
// src/index.ts
|
|
625
512
|
init_get_model_path();
|
|
626
513
|
var ManifoldPlugin = async (ctx) => {
|
|
627
514
|
setPluginContext(ctx.client);
|
|
628
|
-
|
|
515
|
+
const { client, directory } = ctx;
|
|
629
516
|
await ensureGlobalTemplates(ctx);
|
|
630
|
-
await
|
|
517
|
+
await client.app.log({
|
|
631
518
|
body: {
|
|
632
519
|
service: "opencode-manifold",
|
|
633
520
|
level: "info",
|
|
634
|
-
message: "Open Manifold
|
|
521
|
+
message: "Open Manifold v2.1 loaded"
|
|
635
522
|
}
|
|
636
523
|
});
|
|
637
524
|
return {
|
|
638
525
|
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
|
-
}
|
|
526
|
+
execute_task: executeTaskTool(client, directory)
|
|
664
527
|
}
|
|
665
528
|
};
|
|
666
529
|
};
|