opencode-manifold 0.5.14 → 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/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: () => setPluginContext2,
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 setPluginContext2(client) {
75
- pluginClient2 = client;
76
+ function setPluginContext(client) {
77
+ pluginClient = client;
76
78
  }
77
79
  function getModelPathClient() {
78
- if (!pluginClient2) {
80
+ if (!pluginClient) {
79
81
  throw new Error("Plugin client not initialized");
80
82
  }
81
- return pluginClient2;
83
+ return pluginClient;
82
84
  }
83
- var pluginClient2 = null, getModelPathTool;
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 = createRequire(import.meta.url);
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
- async function initProject(directory, client) {
210
- const initialized = [];
211
- await client.app.log({
212
- body: {
213
- service: "opencode-manifold",
214
- level: "info",
215
- message: `Running /manifold-init in ${directory}`
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
- if (!await dirHasContent(globalTemplatesDir)) {
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: `Global templates not found at ${globalTemplatesDir}. Plugin may not have loaded correctly.`
236
+ message: `Agent "${agent}" failed: ${message}`
224
237
  }
225
238
  });
226
- return initialized;
227
- }
228
- const agentsCopied = await copyMissingFiles(join(globalTemplatesDir, "agents"), join(directory, ".opencode", "agents"));
229
- if (agentsCopied.length > 0) {
230
- initialized.push(`agents (${agentsCopied.join(", ")})`);
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 readSettings(directory) {
277
- const settingsPath = join2(directory, "Manifold", "settings.json");
278
- if (existsSync2(settingsPath)) {
279
- const content = await readFile2(settingsPath, "utf-8");
280
- return JSON.parse(content);
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
- return {
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
- async function readDispatcherState(directory, taskNumber) {
293
- const statePath = join2(directory, "Manifold", "dispatcher-state.json");
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
- async function writeDispatcherState(directory, state) {
302
- const statePath = join2(directory, "Manifold", "dispatcher-state.json");
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
- function extractTaskFromPlan(planContent, taskNumber) {
312
- const taskPattern = /###?\s*Task\s+(\d+):\s*(.+)/gi;
313
- let match;
314
- while ((match = taskPattern.exec(planContent)) !== null) {
315
- if (parseInt(match[1]) === taskNumber) {
316
- const description = match[2].trim();
317
- const slug = description.toLowerCase().replace(/[^a-z0-9]+/g, "-").substring(0, 30);
318
- return { description, slug };
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
- return null;
322
- }
323
- function runDispatcherLogic(args, state, settings, taskDescription) {
324
- const taskNumber = args.task_number || state?.task_number || 0;
325
- const slug = state?.slug || "";
326
- if (!state || state.phase === "init" || state.phase === "complete" || state.phase === "escalated") {
327
- const newState = {
328
- task_number: taskNumber,
329
- plan_file: args.plan_file || state?.plan_file || "",
330
- slug,
331
- phase: "awaiting_execution",
332
- senior_output: "",
333
- clerk_summary: "",
334
- loops: 0,
335
- clerk_retries: 0
336
- };
337
- return {
338
- route: "execute",
339
- agent: "clerk",
340
- prompt: buildExecutePrompt(taskNumber, taskDescription),
341
- state: {
342
- task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
343
- phase: "awaiting_execution"
344
- },
345
- message: "Ready to execute task"
346
- };
347
- }
348
- if (state.phase === "awaiting_execution") {
349
- return {
350
- route: "outcome_check",
351
- agent: "clerk",
352
- prompt: buildOutcomeCheckPrompt(),
353
- state: {
354
- task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
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
- route: "logging",
365
- agent: "clerk",
366
- prompt: buildLoggingPrompt(state, taskDescription),
367
- state: {
368
- task_id: `${slug}-${taskNumber.toString().padStart(3, "0")}`,
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
- return {
400
- route: "escalate",
401
- agent: null,
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
- 2. COMPOSE SCOPED PROMPT
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
- 3. APPLY FRAMING
429
- - Prefer "what data goes in and what comes out" framing when it makes the goal clearer
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
- 4. CREATE TASK LOG
437
- Write initial entry to \`Manifold/tasks/<slug>-<task-number>.md\` (e.g., auth-fix-001.md):
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
- 5. CALL SENIOR-DEV
443
- Call @senior-dev with the finalized scoped prompt. Wait for response.
380
+ **Prior Tasks (compound knowledge):**
381
+ ${priorTasks}
444
382
 
445
- 6. REPORT OUTCOME
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
- CONSTRAINTS:
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 buildOutcomeCheckPrompt() {
454
- return `CLERK - OUTCOME CHECK
455
- INSTRUCTIONS:
456
- 1. Based on the senior-dev's response you just received, determine the outcome:
457
- - If senior-dev said "task complete" (or similar), pass \`success: true\` on your next dispatchTask call
458
- - If senior-dev said "task failure" (or similar), pass \`success: false\` on your next dispatchTask call
459
-
460
- NEXT CALL:
461
- Call dispatchTask({ success: true }) or dispatchTask({ success: false }) based on the outcome.`;
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 buildLoggingPrompt(state, taskDescription) {
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
- return `CLERK - LOGGING PHASE
467
- TASK: ${taskDescription}
468
- TASK ID: ${taskId}
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
- 2. Update index at \`Manifold/index.md\`:
478
- - Add: \`- [[${taskId}]] — ${taskDescription} | ${date} | COMPLETED\`
421
+ `;
422
+ content += `**Date:** ${date}
423
+ `;
424
+ content += `**Status:** ${status.toUpperCase()}
479
425
 
480
- 3. Append to log at \`Manifold/log.md\`:
481
- - Add: \`## [${date}] ${taskId} | ${taskDescription} | COMPLETED\`
426
+ `;
427
+ content += `## Summary
428
+ ${summary}
482
429
 
483
- 4. Update graph files for touched files:
484
- - For each file touched, update/add \`Manifold/graph/<graph-name>.md\`
485
- - Add task ID to "Tasks That Edited" section
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
- INSTRUCTIONS:
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
- NEXT:
506
- After senior-dev completes, call dispatchTask() again to report outcome.`;
507
- }
508
- function buildEscalationPrompt(state, taskDescription) {
509
- return `CLERK - ESCALATION
510
- TASK: ${taskDescription}
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
- This task cannot be completed automatically. Human intervention required.`;
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 getClient() {
525
- if (!pluginClient) {
526
- throw new Error("Plugin client not initialized");
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
- var dispatchTaskTool = tool({
531
- description: "Dispatcher for the multi-agent development system. Returns routing instructions based on task state. Clerk calls this with no arguments to get next prompt.",
532
- args: {
533
- task_number: tool.schema.number().optional().describe("Task number"),
534
- plan_file: tool.schema.string().optional().describe("Path to plan document"),
535
- description: tool.schema.string().optional().describe("Task description"),
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
- setPluginContext2(ctx.client);
503
+ const { client, directory } = ctx;
629
504
  await ensureGlobalTemplates(ctx);
630
- await ctx.client.app.log({
505
+ await client.app.log({
631
506
  body: {
632
507
  service: "opencode-manifold",
633
508
  level: "info",
634
- message: "Open Manifold plugin loaded"
509
+ message: "Open Manifold v2.1 loaded"
635
510
  }
636
511
  });
637
512
  return {
638
513
  tool: {
639
- dispatchTask: dispatchTaskTool,
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
  };