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/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,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 = createRequire(import.meta.url);
105
- var bundledTemplatesDir = join(__dirname2, "..", "src", "templates");
106
- 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";
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
- 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
- }
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
- 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}`
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
- if (!await dirHasContent(globalTemplatesDir)) {
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: `Global templates not found at ${globalTemplatesDir}. Plugin may not have loaded correctly.`
248
+ message: `Agent "${agent}" failed: ${message}`
224
249
  }
225
250
  });
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);
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 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);
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
- return {
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
- 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;
283
+ function extractText(parts) {
284
+ return parts.filter((p) => p.type === "text").map((p) => p.text).join("");
300
285
  }
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));
286
+ function sleep(ms) {
287
+ return new Promise((resolve) => setTimeout(resolve, ms));
310
288
  }
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 };
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
- 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) {
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
- 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"
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
- 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.
380
+ });
381
+ function buildDirective(title, description, contextNotes, priorTasks) {
382
+ return `## Directive
419
383
 
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.
384
+ **Task:** ${title}
427
385
 
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
386
+ **Description:**
387
+ ${description}
435
388
 
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)
389
+ **Context Notes (from pre-planning research):**
390
+ ${contextNotes}
441
391
 
442
- 5. CALL SENIOR-DEV
443
- Call @senior-dev with the finalized scoped prompt. Wait for response.
392
+ **Prior Tasks (compound knowledge):**
393
+ ${priorTasks}
444
394
 
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).
395
+ ---
448
396
 
449
- CONSTRAINTS:
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 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.`;
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 buildLoggingPrompt(state, taskDescription) {
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
- 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
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
- 2. Update index at \`Manifold/index.md\`:
478
- - Add: \`- [[${taskId}]] — ${taskDescription} | ${date} | COMPLETED\`
433
+ `;
434
+ content += `**Date:** ${date}
435
+ `;
436
+ content += `**Status:** ${status.toUpperCase()}
479
437
 
480
- 3. Append to log at \`Manifold/log.md\`:
481
- - Add: \`## [${date}] ${taskId} | ${taskDescription} | COMPLETED\`
438
+ `;
439
+ content += `## Summary
440
+ ${summary}
482
441
 
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
442
+ `;
443
+ if (implementation) {
444
+ content += `## Implementation
497
445
 
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
446
+ ${implementation}
504
447
 
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
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
- This task cannot be completed automatically. Human intervention required.`;
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 getClient() {
525
- if (!pluginClient) {
526
- throw new Error("Plugin client not initialized");
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
- 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);
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
- setPluginContext2(ctx.client);
515
+ const { client, directory } = ctx;
629
516
  await ensureGlobalTemplates(ctx);
630
- await ctx.client.app.log({
517
+ await client.app.log({
631
518
  body: {
632
519
  service: "opencode-manifold",
633
520
  level: "info",
634
- message: "Open Manifold plugin loaded"
521
+ message: "Open Manifold v2.1 loaded"
635
522
  }
636
523
  });
637
524
  return {
638
525
  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
- }
526
+ execute_task: executeTaskTool(client, directory)
664
527
  }
665
528
  };
666
529
  };