opencode-manifold 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/README.md +189 -0
- package/dist/index.js +1468 -0
- package/package.json +33 -0
- package/src/templates/agents/clerk.md +74 -0
- package/src/templates/agents/debug.md +55 -0
- package/src/templates/agents/junior-dev.md +62 -0
- package/src/templates/agents/lead-dev.md +44 -0
- package/src/templates/agents/senior-dev.md +51 -0
- package/src/templates/config/opencode.json +48 -0
- package/src/templates/manifold/index.md +11 -0
- package/src/templates/manifold/log.md +10 -0
- package/src/templates/manifold/plans.json +1 -0
- package/src/templates/manifold/schema.md +164 -0
- package/src/templates/manifold/settings.json +9 -0
- package/src/templates/manifold/state.json +11 -0
- package/src/templates/skills/clerk-orchestration/SKILL.md +108 -0
- package/src/templates/skills/lead-dev-workflow/SKILL.md +114 -0
- package/src/templates/skills/wiki-ingest/SKILL.md +129 -0
- package/src/templates/skills/wiki-query/SKILL.md +95 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1468 @@
|
|
|
1
|
+
// src/setup.ts
|
|
2
|
+
import { readFile, writeFile, mkdir, readdir, stat } from "fs/promises";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
var __dirname = "/Users/don/gitthings/Open_Manifold_System/src";
|
|
6
|
+
async function fileExists(path) {
|
|
7
|
+
try {
|
|
8
|
+
await stat(path);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function copyFile(src, dest) {
|
|
15
|
+
const destDir = dirname(dest);
|
|
16
|
+
if (!existsSync(destDir)) {
|
|
17
|
+
await mkdir(destDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
const content = await readFile(src);
|
|
20
|
+
await writeFile(dest, content);
|
|
21
|
+
}
|
|
22
|
+
async function copyDirectory(src, dest) {
|
|
23
|
+
if (!existsSync(dest)) {
|
|
24
|
+
await mkdir(dest, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const srcPath = join(src, entry.name);
|
|
29
|
+
const destPath = join(dest, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
await copyDirectory(srcPath, destPath);
|
|
32
|
+
} else {
|
|
33
|
+
await copyFile(srcPath, destPath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function setupProject(directory, client) {
|
|
38
|
+
await client.app.log({
|
|
39
|
+
body: {
|
|
40
|
+
service: "opencode-manifold",
|
|
41
|
+
level: "info",
|
|
42
|
+
message: `Checking Open Manifold setup in ${directory}`
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const manifoldPath = join(directory, "Manifold");
|
|
46
|
+
const opencodeAgentsPath = join(directory, ".opencode", "agents");
|
|
47
|
+
const opencodeSkillsPath = join(directory, ".opencode", "skills");
|
|
48
|
+
const opencodeJsonPath = join(directory, "opencode.json");
|
|
49
|
+
const pluginSourceDir = join(directory, "..", "node_modules", "opencode-manifold", "src", "templates");
|
|
50
|
+
const manifestPath = join(pluginSourceDir, "..", "..");
|
|
51
|
+
const templateSourceDir = existsSync(manifestPath) ? manifestPath : join(__dirname, "..", "templates");
|
|
52
|
+
const manifoldExists = await fileExists(manifoldPath);
|
|
53
|
+
if (!manifoldExists) {
|
|
54
|
+
await client.app.log({
|
|
55
|
+
body: {
|
|
56
|
+
service: "opencode-manifold",
|
|
57
|
+
level: "info",
|
|
58
|
+
message: "Manifold directory not found, setting up..."
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await mkdir(manifoldPath, { recursive: true });
|
|
62
|
+
await mkdir(join(manifoldPath, ".obsidian"), { recursive: true });
|
|
63
|
+
await mkdir(join(manifoldPath, "tasks"), { recursive: true });
|
|
64
|
+
await mkdir(join(manifoldPath, "graph"), { recursive: true });
|
|
65
|
+
const templateManifold = join(templateSourceDir, "manifold");
|
|
66
|
+
if (existsSync(templateManifold)) {
|
|
67
|
+
const files = [
|
|
68
|
+
"settings.json",
|
|
69
|
+
"plans.json",
|
|
70
|
+
"state.json",
|
|
71
|
+
"index.md",
|
|
72
|
+
"log.md",
|
|
73
|
+
"schema.md"
|
|
74
|
+
];
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const src = join(templateManifold, file);
|
|
77
|
+
if (existsSync(src)) {
|
|
78
|
+
await copyFile(src, join(manifoldPath, file));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await client.app.log({
|
|
83
|
+
body: {
|
|
84
|
+
service: "opencode-manifold",
|
|
85
|
+
level: "info",
|
|
86
|
+
message: "Manifold directory created"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const agentsExist = await fileExists(opencodeAgentsPath);
|
|
91
|
+
if (!agentsExist) {
|
|
92
|
+
await client.app.log({
|
|
93
|
+
body: {
|
|
94
|
+
service: "opencode-manifold",
|
|
95
|
+
level: "info",
|
|
96
|
+
message: "Setting up agent definitions..."
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
await mkdir(opencodeAgentsPath, { recursive: true });
|
|
100
|
+
const templateAgents = join(templateSourceDir, "agents");
|
|
101
|
+
if (existsSync(templateAgents)) {
|
|
102
|
+
const agents = [
|
|
103
|
+
"lead-dev.md",
|
|
104
|
+
"clerk.md",
|
|
105
|
+
"senior-dev.md",
|
|
106
|
+
"junior-dev.md",
|
|
107
|
+
"debug.md"
|
|
108
|
+
];
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
const src = join(templateAgents, agent);
|
|
111
|
+
if (existsSync(src)) {
|
|
112
|
+
await copyFile(src, join(opencodeAgentsPath, agent));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await client.app.log({
|
|
117
|
+
body: {
|
|
118
|
+
service: "opencode-manifold",
|
|
119
|
+
level: "info",
|
|
120
|
+
message: "Agent definitions created"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const skillsExist = await fileExists(opencodeSkillsPath);
|
|
125
|
+
if (!skillsExist) {
|
|
126
|
+
await client.app.log({
|
|
127
|
+
body: {
|
|
128
|
+
service: "opencode-manifold",
|
|
129
|
+
level: "info",
|
|
130
|
+
message: "Setting up skills..."
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
await mkdir(opencodeSkillsPath, { recursive: true });
|
|
134
|
+
const templateSkills = join(templateSourceDir, "skills");
|
|
135
|
+
if (existsSync(templateSkills)) {
|
|
136
|
+
const skills = [
|
|
137
|
+
"clerk-orchestration",
|
|
138
|
+
"lead-dev-workflow",
|
|
139
|
+
"wiki-ingest",
|
|
140
|
+
"wiki-query"
|
|
141
|
+
];
|
|
142
|
+
for (const skill of skills) {
|
|
143
|
+
const skillDir = join(templateSkills, skill);
|
|
144
|
+
if (existsSync(skillDir)) {
|
|
145
|
+
await copyDirectory(skillDir, join(opencodeSkillsPath, skill));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await client.app.log({
|
|
150
|
+
body: {
|
|
151
|
+
service: "opencode-manifold",
|
|
152
|
+
level: "info",
|
|
153
|
+
message: "Skills created"
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const opencodeJsonExists = await fileExists(opencodeJsonPath);
|
|
158
|
+
if (!opencodeJsonExists) {
|
|
159
|
+
await client.app.log({
|
|
160
|
+
body: {
|
|
161
|
+
service: "opencode-manifold",
|
|
162
|
+
level: "info",
|
|
163
|
+
message: "Creating opencode.json configuration..."
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const templateConfig = join(templateSourceDir, "config", "opencode.json");
|
|
167
|
+
if (existsSync(templateConfig)) {
|
|
168
|
+
await copyFile(templateConfig, opencodeJsonPath);
|
|
169
|
+
} else {
|
|
170
|
+
const defaultConfig = {
|
|
171
|
+
$schema: "https://opencode.ai/config.json",
|
|
172
|
+
plugin: ["opencode-manifold", "opencode-codebase-index"],
|
|
173
|
+
agent: {
|
|
174
|
+
"lead-dev": {
|
|
175
|
+
model: "minimax/minimax-2.7",
|
|
176
|
+
skill: ["lead-dev-workflow"]
|
|
177
|
+
},
|
|
178
|
+
clerk: {
|
|
179
|
+
model: "minimax/minimax-2.7",
|
|
180
|
+
skill: ["clerk-orchestration", "wiki-ingest", "wiki-query"]
|
|
181
|
+
},
|
|
182
|
+
"senior-dev": {
|
|
183
|
+
model: "minimax/minimax-2.7"
|
|
184
|
+
},
|
|
185
|
+
"junior-dev": {
|
|
186
|
+
model: "qwen/qwen3.5-27b"
|
|
187
|
+
},
|
|
188
|
+
debug: {
|
|
189
|
+
model: "anthropic/claude-sonnet-4-6-20250514"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
await writeFile(opencodeJsonPath, JSON.stringify(defaultConfig, null, 2));
|
|
194
|
+
}
|
|
195
|
+
await client.app.log({
|
|
196
|
+
body: {
|
|
197
|
+
service: "opencode-manifold",
|
|
198
|
+
level: "info",
|
|
199
|
+
message: "opencode.json created"
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
await client.app.log({
|
|
204
|
+
body: {
|
|
205
|
+
service: "opencode-manifold",
|
|
206
|
+
level: "info",
|
|
207
|
+
message: "Open Manifold initialized successfully"
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/tools/dispatch-task.ts
|
|
213
|
+
import { tool } from "@opencode-ai/plugin";
|
|
214
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
215
|
+
import { existsSync as existsSync4 } from "fs";
|
|
216
|
+
import { join as join4 } from "path";
|
|
217
|
+
|
|
218
|
+
// src/state-machine.ts
|
|
219
|
+
import { readFile as readFile3, writeFile as writeFile3, readdir as readdir2 } from "fs/promises";
|
|
220
|
+
import { existsSync as existsSync3 } from "fs";
|
|
221
|
+
import { join as join3 } from "path";
|
|
222
|
+
|
|
223
|
+
// src/session-spawner.ts
|
|
224
|
+
async function waitForSessionIdle(client, sessionId, timeoutMs) {
|
|
225
|
+
const startTime = Date.now();
|
|
226
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
227
|
+
const statusResponse = await client.session.status({});
|
|
228
|
+
const statusData = statusResponse.data;
|
|
229
|
+
if (statusData && statusData[sessionId]) {
|
|
230
|
+
const status = statusData[sessionId];
|
|
231
|
+
if (status.type === "idle") {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (status.type === "retry") {
|
|
235
|
+
const waitTime = status.next - Date.now();
|
|
236
|
+
if (waitTime > 0) {
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(waitTime, 1000)));
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
async function getLastAssistantMessage(client, sessionId) {
|
|
247
|
+
const messagesResponse = await client.session.messages({
|
|
248
|
+
path: { id: sessionId }
|
|
249
|
+
});
|
|
250
|
+
const messages = messagesResponse.data;
|
|
251
|
+
if (!messages || !Array.isArray(messages)) {
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
255
|
+
const msg = messages[i];
|
|
256
|
+
if (msg.info && msg.info.role === "assistant") {
|
|
257
|
+
if (msg.parts && Array.isArray(msg.parts)) {
|
|
258
|
+
for (const part of msg.parts) {
|
|
259
|
+
if (part.type === "text" && part.text) {
|
|
260
|
+
return part.text;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
async function cleanupSession(client, sessionId) {
|
|
269
|
+
try {
|
|
270
|
+
await client.session.delete({ path: { id: sessionId } });
|
|
271
|
+
} catch {}
|
|
272
|
+
}
|
|
273
|
+
async function spawnSession(client, agent, prompt, timeoutSeconds) {
|
|
274
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
275
|
+
try {
|
|
276
|
+
const createResponse = await client.session.create({});
|
|
277
|
+
const session = createResponse.data;
|
|
278
|
+
if (!session || !session.id) {
|
|
279
|
+
return {
|
|
280
|
+
content: "",
|
|
281
|
+
success: false,
|
|
282
|
+
error: "Failed to create session"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const sessionId = session.id;
|
|
286
|
+
await client.app.log({
|
|
287
|
+
body: {
|
|
288
|
+
service: "opencode-manifold",
|
|
289
|
+
level: "info",
|
|
290
|
+
message: `Created session ${sessionId} for agent ${agent}`
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
await client.session.promptAsync({
|
|
295
|
+
path: { id: sessionId },
|
|
296
|
+
body: {
|
|
297
|
+
agent,
|
|
298
|
+
noReply: true,
|
|
299
|
+
parts: [{ type: "text", text: prompt }]
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const isIdle = await waitForSessionIdle(client, sessionId, timeoutMs);
|
|
303
|
+
if (!isIdle) {
|
|
304
|
+
await client.session.abort({ path: { id: sessionId } });
|
|
305
|
+
return {
|
|
306
|
+
content: "",
|
|
307
|
+
success: false,
|
|
308
|
+
error: `Session timed out after ${timeoutSeconds}s`
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const content = await getLastAssistantMessage(client, sessionId);
|
|
312
|
+
await client.app.log({
|
|
313
|
+
body: {
|
|
314
|
+
service: "opencode-manifold",
|
|
315
|
+
level: "info",
|
|
316
|
+
message: `Session ${sessionId} completed, content length: ${content.length}`
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return {
|
|
320
|
+
content,
|
|
321
|
+
success: true
|
|
322
|
+
};
|
|
323
|
+
} finally {
|
|
324
|
+
await cleanupSession(client, sessionId);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return {
|
|
328
|
+
content: "",
|
|
329
|
+
success: false,
|
|
330
|
+
error: error instanceof Error ? error.message : String(error)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function spawnClerkSession(client, prompt, agent, timeout) {
|
|
335
|
+
await client.app.log({
|
|
336
|
+
body: {
|
|
337
|
+
service: "opencode-manifold",
|
|
338
|
+
level: "info",
|
|
339
|
+
message: `Spawning Clerk session (agent: ${agent}, timeout: ${timeout}s)`
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return spawnSession(client, agent, prompt, timeout);
|
|
343
|
+
}
|
|
344
|
+
async function spawnSeniorDevSession(client, prompt, agent, timeout) {
|
|
345
|
+
await client.app.log({
|
|
346
|
+
body: {
|
|
347
|
+
service: "opencode-manifold",
|
|
348
|
+
level: "info",
|
|
349
|
+
message: `Spawning Senior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
return spawnSession(client, agent, prompt, timeout);
|
|
353
|
+
}
|
|
354
|
+
async function spawnJuniorDevSession(client, prompt, seniorOutput, agent, timeout) {
|
|
355
|
+
await client.app.log({
|
|
356
|
+
body: {
|
|
357
|
+
service: "opencode-manifold",
|
|
358
|
+
level: "info",
|
|
359
|
+
message: `Spawning Junior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
const fullPrompt = `${prompt}
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
Senior Dev's Implementation:
|
|
367
|
+
${seniorOutput}`;
|
|
368
|
+
return spawnSession(client, agent, fullPrompt, timeout);
|
|
369
|
+
}
|
|
370
|
+
async function spawnDebugSession(client, prompt, loopHistory, agent, timeout) {
|
|
371
|
+
await client.app.log({
|
|
372
|
+
body: {
|
|
373
|
+
service: "opencode-manifold",
|
|
374
|
+
level: "info",
|
|
375
|
+
message: `Spawning Debug session (agent: ${agent}, timeout: ${timeout}s)`
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const fullPrompt = `${prompt}
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
Loop History:
|
|
383
|
+
${loopHistory}`;
|
|
384
|
+
return spawnSession(client, agent, fullPrompt, timeout);
|
|
385
|
+
}
|
|
386
|
+
function parseJuniorFirstWord(response) {
|
|
387
|
+
const firstWord = response.trim().split(/\s+/)[0].toUpperCase();
|
|
388
|
+
if (firstWord === "COMPLETE") {
|
|
389
|
+
return "COMPLETE";
|
|
390
|
+
}
|
|
391
|
+
return "QUESTIONS";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/error-handler.ts
|
|
395
|
+
class ModelCallError extends Error {
|
|
396
|
+
agent;
|
|
397
|
+
attempt;
|
|
398
|
+
constructor(message, agent, attempt) {
|
|
399
|
+
super(message);
|
|
400
|
+
this.agent = agent;
|
|
401
|
+
this.attempt = attempt;
|
|
402
|
+
this.name = "ModelCallError";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function retryWithBackoff(fn, options) {
|
|
406
|
+
let lastError;
|
|
407
|
+
for (let attempt = 0;attempt <= options.maxRetries; attempt++) {
|
|
408
|
+
try {
|
|
409
|
+
return await fn();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
412
|
+
if (attempt < options.maxRetries) {
|
|
413
|
+
options.onRetry?.(attempt + 1, lastError);
|
|
414
|
+
const delay = Math.pow(2, attempt) * 100;
|
|
415
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
throw lastError;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/graph.ts
|
|
423
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
424
|
+
import { existsSync as existsSync2 } from "fs";
|
|
425
|
+
import { join as join2 } from "path";
|
|
426
|
+
function pathToGraphFilename(filePath) {
|
|
427
|
+
return filePath.replace(/[/.]/g, "_") + ".md";
|
|
428
|
+
}
|
|
429
|
+
function parseGraphContent(content) {
|
|
430
|
+
const lines = content.split(`
|
|
431
|
+
`);
|
|
432
|
+
let filePath = "";
|
|
433
|
+
let inSection = null;
|
|
434
|
+
const entry = {
|
|
435
|
+
filePath: "",
|
|
436
|
+
calls: [],
|
|
437
|
+
dependsOn: [],
|
|
438
|
+
tasksThatEdited: []
|
|
439
|
+
};
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
const trimmed = line.trim();
|
|
442
|
+
if (trimmed.startsWith("# ")) {
|
|
443
|
+
filePath = trimmed.substring(2).trim();
|
|
444
|
+
entry.filePath = filePath;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (trimmed === "## Calls") {
|
|
448
|
+
inSection = "calls";
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (trimmed === "## Depends On") {
|
|
452
|
+
inSection = "dependsOn";
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (trimmed === "## Tasks That Edited") {
|
|
456
|
+
inSection = "tasks";
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (trimmed.startsWith("- [[") && trimmed.endsWith("]]")) {
|
|
460
|
+
const ref = trimmed.slice(4, -2).trim();
|
|
461
|
+
if (inSection === "calls") {
|
|
462
|
+
entry.calls.push(ref);
|
|
463
|
+
} else if (inSection === "dependsOn") {
|
|
464
|
+
entry.dependsOn.push(ref);
|
|
465
|
+
} else if (inSection === "tasks") {
|
|
466
|
+
entry.tasksThatEdited.push(ref);
|
|
467
|
+
}
|
|
468
|
+
} else if (trimmed.startsWith("- `") && trimmed.endsWith("`")) {
|
|
469
|
+
const ref = trimmed.slice(3, -1).trim();
|
|
470
|
+
if (inSection === "calls" && !entry.calls.includes(ref)) {
|
|
471
|
+
entry.calls.push(ref);
|
|
472
|
+
} else if (inSection === "dependsOn" && !entry.dependsOn.includes(ref)) {
|
|
473
|
+
entry.dependsOn.push(ref);
|
|
474
|
+
}
|
|
475
|
+
} else if (trimmed.startsWith("- ")) {
|
|
476
|
+
const ref = trimmed.slice(2).trim();
|
|
477
|
+
if (inSection === "calls" && !entry.calls.includes(ref)) {
|
|
478
|
+
entry.calls.push(ref);
|
|
479
|
+
} else if (inSection === "dependsOn" && !entry.dependsOn.includes(ref)) {
|
|
480
|
+
entry.dependsOn.push(ref);
|
|
481
|
+
} else if (inSection === "tasks" && !entry.tasksThatEdited.includes(ref)) {
|
|
482
|
+
entry.tasksThatEdited.push(ref);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!entry.filePath) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
return entry;
|
|
490
|
+
}
|
|
491
|
+
function formatGraphContent(entry) {
|
|
492
|
+
const callsSection = entry.calls.length > 0 ? `## Calls
|
|
493
|
+
` + entry.calls.map((c) => `- ${c}`).join(`
|
|
494
|
+
`) : `## Calls
|
|
495
|
+
`;
|
|
496
|
+
const dependsSection = entry.dependsOn.length > 0 ? `## Depends On
|
|
497
|
+
` + entry.dependsOn.map((d) => `- ${d}`).join(`
|
|
498
|
+
`) : `## Depends On
|
|
499
|
+
`;
|
|
500
|
+
const tasksSection = entry.tasksThatEdited.length > 0 ? `## Tasks That Edited
|
|
501
|
+
` + entry.tasksThatEdited.map((t) => `- [[${t}]]`).join(`
|
|
502
|
+
`) : `## Tasks That Edited
|
|
503
|
+
`;
|
|
504
|
+
return `# ${entry.filePath}
|
|
505
|
+
|
|
506
|
+
${callsSection}
|
|
507
|
+
|
|
508
|
+
${dependsSection}
|
|
509
|
+
|
|
510
|
+
${tasksSection}
|
|
511
|
+
`.trim();
|
|
512
|
+
}
|
|
513
|
+
async function readGraphFile(directory, filePath) {
|
|
514
|
+
const filename = pathToGraphFilename(filePath);
|
|
515
|
+
const graphPath = join2(directory, "Manifold", "graph", filename);
|
|
516
|
+
if (!existsSync2(graphPath)) {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const content = await readFile2(graphPath, "utf-8");
|
|
521
|
+
return parseGraphContent(content);
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function updateGraphFile(directory, filePath, taskId, calls, dependsOn) {
|
|
527
|
+
const filename = pathToGraphFilename(filePath);
|
|
528
|
+
const graphPath = join2(directory, "Manifold", "graph", filename);
|
|
529
|
+
let entry;
|
|
530
|
+
if (existsSync2(graphPath)) {
|
|
531
|
+
const existing = await readGraphFile(directory, filePath);
|
|
532
|
+
entry = existing || {
|
|
533
|
+
filePath,
|
|
534
|
+
calls: [],
|
|
535
|
+
dependsOn: [],
|
|
536
|
+
tasksThatEdited: []
|
|
537
|
+
};
|
|
538
|
+
} else {
|
|
539
|
+
entry = {
|
|
540
|
+
filePath,
|
|
541
|
+
calls: calls || [],
|
|
542
|
+
dependsOn: dependsOn || [],
|
|
543
|
+
tasksThatEdited: []
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (calls) {
|
|
547
|
+
entry.calls = [...new Set([...entry.calls, ...calls])];
|
|
548
|
+
}
|
|
549
|
+
if (dependsOn) {
|
|
550
|
+
entry.dependsOn = [...new Set([...entry.dependsOn, ...dependsOn])];
|
|
551
|
+
}
|
|
552
|
+
if (!entry.tasksThatEdited.includes(taskId)) {
|
|
553
|
+
entry.tasksThatEdited.push(taskId);
|
|
554
|
+
}
|
|
555
|
+
const content = formatGraphContent(entry);
|
|
556
|
+
await writeFile2(graphPath, content, "utf-8");
|
|
557
|
+
}
|
|
558
|
+
async function updateGraphFilesForTask(directory, taskId, files) {
|
|
559
|
+
for (const file of files) {
|
|
560
|
+
try {
|
|
561
|
+
await updateGraphFile(directory, file, taskId);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
console.error(`Failed to update graph for ${file}:`, error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/state-machine.ts
|
|
569
|
+
async function readState(directory) {
|
|
570
|
+
const statePath = join3(directory, "Manifold", "state.json");
|
|
571
|
+
if (existsSync3(statePath)) {
|
|
572
|
+
const content = await readFile3(statePath, "utf-8");
|
|
573
|
+
return JSON.parse(content);
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
current_task: null,
|
|
577
|
+
state: "idle",
|
|
578
|
+
loop_count: 0,
|
|
579
|
+
clerk_retry_count: 0,
|
|
580
|
+
scoped_prompt: null,
|
|
581
|
+
senior_output: null,
|
|
582
|
+
junior_response: null,
|
|
583
|
+
debug_suggestion: null,
|
|
584
|
+
loop_history: []
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async function writeState(directory, state) {
|
|
588
|
+
const statePath = join3(directory, "Manifold", "state.json");
|
|
589
|
+
await writeFile3(statePath, JSON.stringify(state, null, 2));
|
|
590
|
+
}
|
|
591
|
+
function buildLoopHistory(state) {
|
|
592
|
+
if (state.loop_history.length === 0) {
|
|
593
|
+
return "No previous loops.";
|
|
594
|
+
}
|
|
595
|
+
return state.loop_history.map((entry, i) => `### Loop ${i + 1}
|
|
596
|
+
${entry}`).join(`
|
|
597
|
+
|
|
598
|
+
`);
|
|
599
|
+
}
|
|
600
|
+
async function readRecentTaskLogs(directory, count) {
|
|
601
|
+
const tasksDir = join3(directory, "Manifold", "tasks");
|
|
602
|
+
if (!existsSync3(tasksDir)) {
|
|
603
|
+
return [];
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const files = await readdir2(tasksDir);
|
|
607
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
608
|
+
const recentFiles = mdFiles.slice(-count);
|
|
609
|
+
const tasks = await Promise.all(recentFiles.map(async (filename) => {
|
|
610
|
+
const content = await readFile3(join3(tasksDir, filename), "utf-8");
|
|
611
|
+
return { filename, content };
|
|
612
|
+
}));
|
|
613
|
+
return tasks;
|
|
614
|
+
} catch {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function runClerkLoggingPhase(client, task, state, settings, directory) {
|
|
619
|
+
state.state = "clerk_logging";
|
|
620
|
+
await writeState(directory, state);
|
|
621
|
+
await client.app.log({
|
|
622
|
+
body: {
|
|
623
|
+
service: "opencode-manifold",
|
|
624
|
+
level: "info",
|
|
625
|
+
message: "State: clerk_logging - Spawning Clerk for wiki logging"
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
const taskId = `${task.slug}-${task.task_number.toString().padStart(3, "0")}`;
|
|
629
|
+
const date = new Date().toISOString().split("T")[0];
|
|
630
|
+
const clerkLoggingPrompt = `You are the Clerk. Log the completed task to the project wiki.
|
|
631
|
+
|
|
632
|
+
Task ID: ${taskId}
|
|
633
|
+
Task Description: ${task.description}
|
|
634
|
+
Date: ${date}
|
|
635
|
+
Status: COMPLETED
|
|
636
|
+
Loops: ${state.loop_count}
|
|
637
|
+
|
|
638
|
+
Scoped Prompt Used:
|
|
639
|
+
${state.scoped_prompt}
|
|
640
|
+
|
|
641
|
+
Senior Developer's Final Implementation:
|
|
642
|
+
${state.senior_output}
|
|
643
|
+
|
|
644
|
+
Junior Developer's Approval Response:
|
|
645
|
+
${state.junior_response}
|
|
646
|
+
|
|
647
|
+
Loop History:
|
|
648
|
+
${buildLoopHistory(state)}
|
|
649
|
+
|
|
650
|
+
Please perform the following logging actions:
|
|
651
|
+
|
|
652
|
+
1. Create/Update Task File at \`Manifold/tasks/${taskId}.md\`:
|
|
653
|
+
- Header with date, status, loops, task description
|
|
654
|
+
- Scoped Prompt section
|
|
655
|
+
- Design Decisions section (extract from Senior's implementation reasoning)
|
|
656
|
+
- Files Touched section (extract file paths from Senior's implementation)
|
|
657
|
+
- Complete Loop History section
|
|
658
|
+
|
|
659
|
+
2. Update \`Manifold/index.md\`:
|
|
660
|
+
- Add entry under the plan's section:
|
|
661
|
+
\`- [[${taskId}]] — ${task.description} | ${date} | COMPLETED\`
|
|
662
|
+
|
|
663
|
+
3. Append to \`Manifold/log.md\`:
|
|
664
|
+
\`## [${date}] ${taskId} | ${task.description} | COMPLETED | ${state.loop_count} loops\`
|
|
665
|
+
|
|
666
|
+
4. Update graph files for each touched file:
|
|
667
|
+
- Read the implementation and identify all files that were created or modified
|
|
668
|
+
- For each file, create or update \`Manifold/graph/<graph-name>.md\`
|
|
669
|
+
- Add the task ID to the "Tasks That Edited" section
|
|
670
|
+
- Graph filename format: replace \`/\` and \`.\` with \`_\`, append \`.md\`
|
|
671
|
+
- Example: \`src/middleware/auth.ts\` → \`src_middleware_auth_ts.md\`
|
|
672
|
+
|
|
673
|
+
Extract the list of files touched from the Senior's implementation and include it in your response.`;
|
|
674
|
+
const clerkLoggingResult = await retryWithBackoff(() => spawnClerkSession(client, clerkLoggingPrompt, "clerk", settings.timeout), {
|
|
675
|
+
maxRetries: settings.maxRetries,
|
|
676
|
+
onRetry: (attempt, error) => {
|
|
677
|
+
client.app.log({
|
|
678
|
+
body: {
|
|
679
|
+
service: "opencode-manifold",
|
|
680
|
+
level: "warn",
|
|
681
|
+
message: `Clerk logging retry ${attempt}: ${error.message}`
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
if (!clerkLoggingResult.success) {
|
|
687
|
+
await client.app.log({
|
|
688
|
+
body: {
|
|
689
|
+
service: "opencode-manifold",
|
|
690
|
+
level: "error",
|
|
691
|
+
message: `Clerk logging failed: ${clerkLoggingResult.error}. Task completed but wiki not updated.`
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
return { files_changed: [] };
|
|
695
|
+
}
|
|
696
|
+
const filesChanged = extractFilesFromResponse(clerkLoggingResult.content);
|
|
697
|
+
if (filesChanged.length > 0) {
|
|
698
|
+
await client.app.log({
|
|
699
|
+
body: {
|
|
700
|
+
service: "opencode-manifold",
|
|
701
|
+
level: "info",
|
|
702
|
+
message: `Updating graph files for ${filesChanged.length} files...`
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
try {
|
|
706
|
+
await updateGraphFilesForTask(directory, taskId, filesChanged);
|
|
707
|
+
await client.app.log({
|
|
708
|
+
body: {
|
|
709
|
+
service: "opencode-manifold",
|
|
710
|
+
level: "info",
|
|
711
|
+
message: `Graph files updated successfully`
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
} catch (error) {
|
|
715
|
+
await client.app.log({
|
|
716
|
+
body: {
|
|
717
|
+
service: "opencode-manifold",
|
|
718
|
+
level: "warn",
|
|
719
|
+
message: `Graph file update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
await client.app.log({
|
|
725
|
+
body: {
|
|
726
|
+
service: "opencode-manifold",
|
|
727
|
+
level: "info",
|
|
728
|
+
message: `Clerk logging complete. Files touched: ${filesChanged.join(", ") || "none"}`
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
return { files_changed: filesChanged };
|
|
732
|
+
}
|
|
733
|
+
function extractFilesFromResponse(content) {
|
|
734
|
+
const files = [];
|
|
735
|
+
const patterns = [
|
|
736
|
+
/\[\[([^\]]+\.[a-zA-Z]+)\]\]/g,
|
|
737
|
+
/`([^`]+\.[a-zA-Z]+)`/g,
|
|
738
|
+
/(?:src|lib|app|components|utils|helpers)[^\s]*/gi
|
|
739
|
+
];
|
|
740
|
+
for (const pattern of patterns) {
|
|
741
|
+
const matches = content.matchAll(pattern);
|
|
742
|
+
for (const match of matches) {
|
|
743
|
+
const file = match[1] || match[0];
|
|
744
|
+
if (file && !files.includes(file)) {
|
|
745
|
+
files.push(file);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return files;
|
|
750
|
+
}
|
|
751
|
+
function extractScopedPrompt(content) {
|
|
752
|
+
const startMarker = "===SCOPED_PROMPT_START===";
|
|
753
|
+
const endMarker = "===SCOPED_PROMPT_END===";
|
|
754
|
+
const startIdx = content.indexOf(startMarker);
|
|
755
|
+
const endIdx = content.indexOf(endMarker);
|
|
756
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
757
|
+
return content.substring(startIdx + startMarker.length, endIdx).trim();
|
|
758
|
+
}
|
|
759
|
+
const lines = content.split(`
|
|
760
|
+
`);
|
|
761
|
+
const promptLines = [];
|
|
762
|
+
let inPrompt = false;
|
|
763
|
+
for (const line of lines) {
|
|
764
|
+
if (line.includes("SCOPED_PROMPT_END")) {
|
|
765
|
+
inPrompt = false;
|
|
766
|
+
}
|
|
767
|
+
if (inPrompt) {
|
|
768
|
+
promptLines.push(line);
|
|
769
|
+
}
|
|
770
|
+
if (line.includes("SCOPED_PROMPT_START")) {
|
|
771
|
+
inPrompt = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (promptLines.length > 0) {
|
|
775
|
+
return promptLines.join(`
|
|
776
|
+
`).trim();
|
|
777
|
+
}
|
|
778
|
+
return content;
|
|
779
|
+
}
|
|
780
|
+
async function runStateMachine(task, settings, context) {
|
|
781
|
+
const { client, directory } = context;
|
|
782
|
+
let state = await readState(directory);
|
|
783
|
+
state.current_task = task.task_number;
|
|
784
|
+
state.state = "idle";
|
|
785
|
+
state.loop_count = 0;
|
|
786
|
+
state.clerk_retry_count = 0;
|
|
787
|
+
state.scoped_prompt = null;
|
|
788
|
+
state.senior_output = null;
|
|
789
|
+
state.junior_response = null;
|
|
790
|
+
state.debug_suggestion = null;
|
|
791
|
+
state.loop_history = [];
|
|
792
|
+
await writeState(directory, state);
|
|
793
|
+
await client.app.log({
|
|
794
|
+
body: {
|
|
795
|
+
service: "opencode-manifold",
|
|
796
|
+
level: "info",
|
|
797
|
+
message: `Starting state machine for task ${task.task_number}: ${task.description}`
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
state.state = "clerk_researched";
|
|
801
|
+
await writeState(directory, state);
|
|
802
|
+
await client.app.log({
|
|
803
|
+
body: {
|
|
804
|
+
service: "opencode-manifold",
|
|
805
|
+
level: "info",
|
|
806
|
+
message: "State: clerk_researched - Pre-reading wiki context for Clerk"
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
const recentTasks = await readRecentTaskLogs(directory, settings.recentTaskCount);
|
|
810
|
+
let recentTasksContext = "";
|
|
811
|
+
if (recentTasks.length > 0) {
|
|
812
|
+
recentTasksContext = `
|
|
813
|
+
|
|
814
|
+
Recent Task Logs from Manifold:
|
|
815
|
+
` + recentTasks.map((t) => `--- ${t.filename} ---
|
|
816
|
+
${t.content}`).join(`
|
|
817
|
+
|
|
818
|
+
`);
|
|
819
|
+
} else {
|
|
820
|
+
recentTasksContext = `
|
|
821
|
+
|
|
822
|
+
(No prior task logs found - this appears to be a new project.)`;
|
|
823
|
+
}
|
|
824
|
+
await client.app.log({
|
|
825
|
+
body: {
|
|
826
|
+
service: "opencode-manifold",
|
|
827
|
+
level: "info",
|
|
828
|
+
message: `State: clerk_researched - Spawning Clerk with ${recentTasks.length} recent tasks`
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
const clerkPrompt = `You are the Clerk for this project. Research the following task and compose a scoped prompt for the Senior Developer.
|
|
832
|
+
|
|
833
|
+
Task: ${task.description}
|
|
834
|
+
${recentTasksContext}
|
|
835
|
+
|
|
836
|
+
IMPORTANT: You also have access to:
|
|
837
|
+
- The codebase-index tool for semantic code search (use it to find relevant code)
|
|
838
|
+
- File reading tools to read additional context if needed
|
|
839
|
+
|
|
840
|
+
Research steps:
|
|
841
|
+
1. Use codebase-index to search for relevant code (max ${settings.maxResults} results)
|
|
842
|
+
2. Read graph files from Manifold/graph/ for relevant files if you find they exist
|
|
843
|
+
3. Compose a scoped prompt with: task goal, relevant code snippets, prior decisions from above, design guidelines
|
|
844
|
+
|
|
845
|
+
Output Format - STRICTLY FOLLOW THIS:
|
|
846
|
+
|
|
847
|
+
===SCOPED_PROMPT_START===
|
|
848
|
+
[Your composed scoped prompt for the Senior Developer - include task goal, code snippets, prior decisions, design guidelines]
|
|
849
|
+
===SCOPED_PROMPT_END===
|
|
850
|
+
|
|
851
|
+
===CONTEXT_USED_START===
|
|
852
|
+
[List of context documents you used - file paths and brief descriptions]
|
|
853
|
+
===CONTEXT_USED_END===`;
|
|
854
|
+
const clerkResult = await retryWithBackoff(() => spawnClerkSession(client, clerkPrompt, "clerk", settings.timeout), {
|
|
855
|
+
maxRetries: settings.maxRetries,
|
|
856
|
+
onRetry: (attempt, error) => {
|
|
857
|
+
client.app.log({
|
|
858
|
+
body: {
|
|
859
|
+
service: "opencode-manifold",
|
|
860
|
+
level: "warn",
|
|
861
|
+
message: `Clerk retry ${attempt}: ${error.message}`
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
if (!clerkResult.success) {
|
|
867
|
+
state.state = "escalate_user";
|
|
868
|
+
await writeState(directory, state);
|
|
869
|
+
return {
|
|
870
|
+
status: "escalated_user",
|
|
871
|
+
summary: `Task ${task.task_number}: Clerk failed - ${clerkResult.error}`,
|
|
872
|
+
files_changed: [],
|
|
873
|
+
loops: state.loop_count,
|
|
874
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
state.scoped_prompt = extractScopedPrompt(clerkResult.content);
|
|
878
|
+
await writeState(directory, state);
|
|
879
|
+
let finalResult;
|
|
880
|
+
const runSeniorJuniorLoop = async () => {
|
|
881
|
+
state.state = "senior_done";
|
|
882
|
+
await writeState(directory, state);
|
|
883
|
+
await client.app.log({
|
|
884
|
+
body: {
|
|
885
|
+
service: "opencode-manifold",
|
|
886
|
+
level: "info",
|
|
887
|
+
message: "State: senior_done - Spawning Senior Dev"
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
const seniorPrompt = state.scoped_prompt;
|
|
891
|
+
const seniorResult = await retryWithBackoff(() => spawnSeniorDevSession(client, seniorPrompt, "senior-dev", settings.timeout), {
|
|
892
|
+
maxRetries: settings.maxRetries,
|
|
893
|
+
onRetry: (attempt, error) => {
|
|
894
|
+
client.app.log({
|
|
895
|
+
body: {
|
|
896
|
+
service: "opencode-manifold",
|
|
897
|
+
level: "warn",
|
|
898
|
+
message: `Senior Dev retry ${attempt}: ${error.message}`
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
if (!seniorResult.success) {
|
|
904
|
+
throw new ModelCallError(`Senior Dev failed: ${seniorResult.error}`, "senior-dev", 1);
|
|
905
|
+
}
|
|
906
|
+
state.senior_output = seniorResult.content;
|
|
907
|
+
await writeState(directory, state);
|
|
908
|
+
state.state = "junior_review";
|
|
909
|
+
await writeState(directory, state);
|
|
910
|
+
await client.app.log({
|
|
911
|
+
body: {
|
|
912
|
+
service: "opencode-manifold",
|
|
913
|
+
level: "info",
|
|
914
|
+
message: "State: junior_review - Spawning Junior Dev"
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
const juniorPrompt = `You are the Junior Developer reviewing the Senior Developer's implementation.
|
|
918
|
+
|
|
919
|
+
Task Description:
|
|
920
|
+
${state.scoped_prompt}
|
|
921
|
+
|
|
922
|
+
Senior Developer's Implementation:
|
|
923
|
+
${seniorResult.content}
|
|
924
|
+
|
|
925
|
+
Review the implementation against the task requirements. Your response MUST begin with exactly "COMPLETE" or "QUESTIONS" as the first word.
|
|
926
|
+
|
|
927
|
+
If COMPLETE: Briefly explain why the implementation is acceptable.
|
|
928
|
+
If QUESTIONS: List specific issues blocking approval with actionable feedback.`;
|
|
929
|
+
const juniorResult = await retryWithBackoff(() => spawnJuniorDevSession(client, juniorPrompt, seniorResult.content, "junior-dev", settings.timeout), {
|
|
930
|
+
maxRetries: settings.maxRetries,
|
|
931
|
+
onRetry: (attempt, error) => {
|
|
932
|
+
client.app.log({
|
|
933
|
+
body: {
|
|
934
|
+
service: "opencode-manifold",
|
|
935
|
+
level: "warn",
|
|
936
|
+
message: `Junior Dev retry ${attempt}: ${error.message}`
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
if (!juniorResult.success) {
|
|
942
|
+
throw new ModelCallError(`Junior Dev failed: ${juniorResult.error}`, "junior-dev", 1);
|
|
943
|
+
}
|
|
944
|
+
state.junior_response = juniorResult.content;
|
|
945
|
+
state.loop_history.push(`**Senior:** ${seniorResult.content.substring(0, 200)}...
|
|
946
|
+
**Junior:** ${juniorResult.content}`);
|
|
947
|
+
await writeState(directory, state);
|
|
948
|
+
const firstWord = parseJuniorFirstWord(juniorResult.content);
|
|
949
|
+
if (firstWord === "COMPLETE") {
|
|
950
|
+
state.state = "junior_approved";
|
|
951
|
+
await writeState(directory, state);
|
|
952
|
+
return { approved: true, seniorOutput: seniorResult.content, juniorResponse: juniorResult.content };
|
|
953
|
+
} else {
|
|
954
|
+
state.state = "junior_rejected";
|
|
955
|
+
await writeState(directory, state);
|
|
956
|
+
return { approved: false, seniorOutput: seniorResult.content, juniorResponse: juniorResult.content };
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
let loopResult = await runSeniorJuniorLoop();
|
|
960
|
+
while (!loopResult.approved) {
|
|
961
|
+
state.loop_count++;
|
|
962
|
+
await client.app.log({
|
|
963
|
+
body: {
|
|
964
|
+
service: "opencode-manifold",
|
|
965
|
+
level: "info",
|
|
966
|
+
message: `Junior rejected, loop_count: ${state.loop_count}/${settings.maxLoops}`
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
if (state.loop_count >= settings.maxLoops) {
|
|
970
|
+
state.state = "escalate_debug";
|
|
971
|
+
await writeState(directory, state);
|
|
972
|
+
await client.app.log({
|
|
973
|
+
body: {
|
|
974
|
+
service: "opencode-manifold",
|
|
975
|
+
level: "info",
|
|
976
|
+
message: "State: escalate_debug - Spawning Debug agent"
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
const debugPrompt = `You are the Debug Agent. The Senior/Junior loop has failed ${settings.maxLoops} times.
|
|
980
|
+
|
|
981
|
+
Task: ${task.description}
|
|
982
|
+
|
|
983
|
+
Loop History:
|
|
984
|
+
${buildLoopHistory(state)}
|
|
985
|
+
|
|
986
|
+
Analyze why the loop is stuck and suggest a concrete alternative approach.`;
|
|
987
|
+
const debugResult = await retryWithBackoff(() => spawnDebugSession(client, debugPrompt, buildLoopHistory(state), "debug", settings.timeout), {
|
|
988
|
+
maxRetries: settings.maxRetries,
|
|
989
|
+
onRetry: (attempt, error) => {
|
|
990
|
+
client.app.log({
|
|
991
|
+
body: {
|
|
992
|
+
service: "opencode-manifold",
|
|
993
|
+
level: "warn",
|
|
994
|
+
message: `Debug retry ${attempt}: ${error.message}`
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
if (!debugResult.success) {
|
|
1000
|
+
state.state = "escalate_user";
|
|
1001
|
+
await writeState(directory, state);
|
|
1002
|
+
return {
|
|
1003
|
+
status: "escalated_user",
|
|
1004
|
+
summary: `Task ${task.task_number}: Debug failed - ${debugResult.error}`,
|
|
1005
|
+
files_changed: [],
|
|
1006
|
+
loops: state.loop_count,
|
|
1007
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
state.debug_suggestion = debugResult.content;
|
|
1011
|
+
state.loop_history.push(`**Debug Analysis:** ${debugResult.content}`);
|
|
1012
|
+
await writeState(directory, state);
|
|
1013
|
+
state.state = "debug_done";
|
|
1014
|
+
await writeState(directory, state);
|
|
1015
|
+
await client.app.log({
|
|
1016
|
+
body: {
|
|
1017
|
+
service: "opencode-manifold",
|
|
1018
|
+
level: "info",
|
|
1019
|
+
message: "State: debug_done - Senior implements Debug suggestion"
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
const debugSeniorPrompt = `You are the Senior Developer. A Debug agent has identified a root cause issue.
|
|
1023
|
+
|
|
1024
|
+
Original Task: ${task.description}
|
|
1025
|
+
|
|
1026
|
+
Debug Agent's Suggestion:
|
|
1027
|
+
${debugResult.content}
|
|
1028
|
+
|
|
1029
|
+
Please implement the task following the Debug agent's suggestion.`;
|
|
1030
|
+
const debugSeniorResult = await retryWithBackoff(() => spawnSeniorDevSession(client, debugSeniorPrompt, "senior-dev", settings.timeout), {
|
|
1031
|
+
maxRetries: settings.maxRetries,
|
|
1032
|
+
onRetry: (attempt, error) => {
|
|
1033
|
+
client.app.log({
|
|
1034
|
+
body: {
|
|
1035
|
+
service: "opencode-manifold",
|
|
1036
|
+
level: "warn",
|
|
1037
|
+
message: `Debug Senior retry ${attempt}: ${error.message}`
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
if (!debugSeniorResult.success) {
|
|
1043
|
+
state.state = "escalate_user";
|
|
1044
|
+
await writeState(directory, state);
|
|
1045
|
+
return {
|
|
1046
|
+
status: "escalated_user",
|
|
1047
|
+
summary: `Task ${task.task_number}: Debug Senior failed - ${debugSeniorResult.error}`,
|
|
1048
|
+
files_changed: [],
|
|
1049
|
+
loops: state.loop_count,
|
|
1050
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
state.senior_output = debugSeniorResult.content;
|
|
1054
|
+
state.loop_history.push(`**Debug Senior:** ${debugSeniorResult.content.substring(0, 200)}...`);
|
|
1055
|
+
await writeState(directory, state);
|
|
1056
|
+
state.state = "debug_review";
|
|
1057
|
+
await writeState(directory, state);
|
|
1058
|
+
await client.app.log({
|
|
1059
|
+
body: {
|
|
1060
|
+
service: "opencode-manifold",
|
|
1061
|
+
level: "info",
|
|
1062
|
+
message: "State: debug_review - Debug reviews implementation"
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
const debugReviewPrompt = `You are the Debug Agent reviewing the Senior Developer's implementation of your suggestion.
|
|
1066
|
+
|
|
1067
|
+
Task: ${task.description}
|
|
1068
|
+
|
|
1069
|
+
Your Original Suggestion:
|
|
1070
|
+
${state.debug_suggestion}
|
|
1071
|
+
|
|
1072
|
+
Senior Developer's Implementation:
|
|
1073
|
+
${debugSeniorResult.content}
|
|
1074
|
+
|
|
1075
|
+
Does the implementation follow your suggestion correctly? Does it resolve the issue? Respond with COMPLETE if acceptable, or QUESTIONS if there are still problems.`;
|
|
1076
|
+
const debugReviewResult = await retryWithBackoff(() => spawnJuniorDevSession(client, debugReviewPrompt, debugSeniorResult.content, "debug", settings.timeout), {
|
|
1077
|
+
maxRetries: settings.maxRetries,
|
|
1078
|
+
onRetry: (attempt, error) => {
|
|
1079
|
+
client.app.log({
|
|
1080
|
+
body: {
|
|
1081
|
+
service: "opencode-manifold",
|
|
1082
|
+
level: "warn",
|
|
1083
|
+
message: `Debug review retry ${attempt}: ${error.message}`
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
if (!debugReviewResult.success) {
|
|
1089
|
+
state.state = "escalate_user";
|
|
1090
|
+
await writeState(directory, state);
|
|
1091
|
+
return {
|
|
1092
|
+
status: "escalated_user",
|
|
1093
|
+
summary: `Task ${task.task_number}: Debug review failed - ${debugReviewResult.error}`,
|
|
1094
|
+
files_changed: [],
|
|
1095
|
+
loops: state.loop_count,
|
|
1096
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
const debugReviewWord = parseJuniorFirstWord(debugReviewResult.content);
|
|
1100
|
+
if (debugReviewWord === "COMPLETE") {
|
|
1101
|
+
state.state = "junior_approved";
|
|
1102
|
+
await writeState(directory, state);
|
|
1103
|
+
loopResult = {
|
|
1104
|
+
approved: true,
|
|
1105
|
+
seniorOutput: debugSeniorResult.content,
|
|
1106
|
+
juniorResponse: debugReviewResult.content
|
|
1107
|
+
};
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
if (settings.clerkRetryEnabled && state.clerk_retry_count === 0) {
|
|
1111
|
+
state.state = "escalate_clerk_retry";
|
|
1112
|
+
await writeState(directory, state);
|
|
1113
|
+
await client.app.log({
|
|
1114
|
+
body: {
|
|
1115
|
+
service: "opencode-manifold",
|
|
1116
|
+
level: "info",
|
|
1117
|
+
message: "State: escalate_clerk_retry - Clerk re-researches with failure context"
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
state.clerk_retry_count++;
|
|
1121
|
+
state.loop_count = 0;
|
|
1122
|
+
state.loop_history = [];
|
|
1123
|
+
await writeState(directory, state);
|
|
1124
|
+
const clerkRetryPrompt = `You are the Clerk. The previous implementation attempt failed. Re-research with failure context.
|
|
1125
|
+
|
|
1126
|
+
Original Task: ${task.description}
|
|
1127
|
+
|
|
1128
|
+
Previous Implementation (failed):
|
|
1129
|
+
${state.senior_output}
|
|
1130
|
+
|
|
1131
|
+
Junior Dev's Feedback:
|
|
1132
|
+
${state.junior_response}
|
|
1133
|
+
|
|
1134
|
+
Debug Agent's Suggestion (also didn't work):
|
|
1135
|
+
${state.debug_suggestion}
|
|
1136
|
+
|
|
1137
|
+
Please re-research the task with this failure context and compose a new scoped prompt that addresses the underlying issues.`;
|
|
1138
|
+
const clerkRetryResult = await retryWithBackoff(() => spawnClerkSession(client, clerkRetryPrompt, "clerk", settings.timeout), {
|
|
1139
|
+
maxRetries: settings.maxRetries,
|
|
1140
|
+
onRetry: (attempt, error) => {
|
|
1141
|
+
client.app.log({
|
|
1142
|
+
body: {
|
|
1143
|
+
service: "opencode-manifold",
|
|
1144
|
+
level: "warn",
|
|
1145
|
+
message: `Clerk retry retry ${attempt}: ${error.message}`
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
if (!clerkRetryResult.success) {
|
|
1151
|
+
state.state = "escalate_user";
|
|
1152
|
+
await writeState(directory, state);
|
|
1153
|
+
return {
|
|
1154
|
+
status: "escalated_user",
|
|
1155
|
+
summary: `Task ${task.task_number}: Clerk retry failed - ${clerkRetryResult.error}`,
|
|
1156
|
+
files_changed: [],
|
|
1157
|
+
loops: state.loop_count,
|
|
1158
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
state.scoped_prompt = clerkRetryResult.content;
|
|
1162
|
+
state.state = "clerk_retry";
|
|
1163
|
+
await writeState(directory, state);
|
|
1164
|
+
await client.app.log({
|
|
1165
|
+
body: {
|
|
1166
|
+
service: "opencode-manifold",
|
|
1167
|
+
level: "info",
|
|
1168
|
+
message: "State: clerk_retry - Starting fresh loop with new prompt"
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
loopResult = await runSeniorJuniorLoop();
|
|
1172
|
+
} else {
|
|
1173
|
+
state.state = "escalate_user";
|
|
1174
|
+
await writeState(directory, state);
|
|
1175
|
+
return {
|
|
1176
|
+
status: "escalated_user",
|
|
1177
|
+
summary: `Task ${task.task_number}: All loops exhausted. Last feedback: ${state.junior_response}`,
|
|
1178
|
+
files_changed: [],
|
|
1179
|
+
loops: state.loop_count,
|
|
1180
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
state.state = "junior_rejected";
|
|
1185
|
+
await writeState(directory, state);
|
|
1186
|
+
await client.app.log({
|
|
1187
|
+
body: {
|
|
1188
|
+
service: "opencode-manifold",
|
|
1189
|
+
level: "info",
|
|
1190
|
+
message: `Junior rejected, looping back to Senior with feedback (loop ${state.loop_count + 1})`
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
const basePrompt = state.scoped_prompt ?? "";
|
|
1194
|
+
const feedbackSection = `
|
|
1195
|
+
|
|
1196
|
+
---
|
|
1197
|
+
|
|
1198
|
+
Junior Developer Feedback (must address):
|
|
1199
|
+
` + (state.junior_response ?? "") + `
|
|
1200
|
+
|
|
1201
|
+
Please address these issues and resubmit your implementation.`;
|
|
1202
|
+
const seniorRetryPrompt = basePrompt + feedbackSection;
|
|
1203
|
+
state.scoped_prompt = seniorRetryPrompt;
|
|
1204
|
+
await writeState(directory, state);
|
|
1205
|
+
loopResult = await runSeniorJuniorLoop();
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const { files_changed } = await runClerkLoggingPhase(client, task, state, settings, directory);
|
|
1209
|
+
state.state = "complete";
|
|
1210
|
+
await writeState(directory, state);
|
|
1211
|
+
await client.app.log({
|
|
1212
|
+
body: {
|
|
1213
|
+
service: "opencode-manifold",
|
|
1214
|
+
level: "info",
|
|
1215
|
+
message: `State: complete - Task ${task.task_number} completed in ${state.loop_count} loops`
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
return {
|
|
1219
|
+
status: "completed",
|
|
1220
|
+
summary: `Task ${task.task_number}: ${task.description} - Completed successfully`,
|
|
1221
|
+
files_changed,
|
|
1222
|
+
loops: state.loop_count,
|
|
1223
|
+
wiki_entry: `Manifold/tasks/${task.slug}-${task.task_number.toString().padStart(3, "0")}.md`
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/test-runner.ts
|
|
1228
|
+
import { spawn } from "child_process";
|
|
1229
|
+
async function runTest(directory, taskDescription, options) {
|
|
1230
|
+
const testCommand = extractTestCommand(taskDescription) || options.testCommand;
|
|
1231
|
+
if (!testCommand) {
|
|
1232
|
+
return { skip: true };
|
|
1233
|
+
}
|
|
1234
|
+
return new Promise((resolve) => {
|
|
1235
|
+
const child = spawn(testCommand, [], {
|
|
1236
|
+
shell: true,
|
|
1237
|
+
cwd: directory
|
|
1238
|
+
});
|
|
1239
|
+
let stdout = "";
|
|
1240
|
+
let stderr = "";
|
|
1241
|
+
child.stdout?.on("data", (data) => {
|
|
1242
|
+
stdout += data.toString();
|
|
1243
|
+
});
|
|
1244
|
+
child.stderr?.on("data", (data) => {
|
|
1245
|
+
stderr += data.toString();
|
|
1246
|
+
});
|
|
1247
|
+
const timeout = setTimeout(() => {
|
|
1248
|
+
child.kill();
|
|
1249
|
+
resolve({
|
|
1250
|
+
skip: false,
|
|
1251
|
+
result: {
|
|
1252
|
+
passed: false,
|
|
1253
|
+
output: `Test timed out after ${options.timeout}s`,
|
|
1254
|
+
exitCode: 124
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
}, options.timeout * 1000);
|
|
1258
|
+
child.on("close", (code) => {
|
|
1259
|
+
clearTimeout(timeout);
|
|
1260
|
+
const exitCode = code ?? 0;
|
|
1261
|
+
const output = stdout + (stderr ? `
|
|
1262
|
+
STDERR:
|
|
1263
|
+
` + stderr : "");
|
|
1264
|
+
resolve({
|
|
1265
|
+
skip: false,
|
|
1266
|
+
result: {
|
|
1267
|
+
passed: exitCode === 0,
|
|
1268
|
+
output: output || "No output",
|
|
1269
|
+
exitCode
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
child.on("error", (error) => {
|
|
1274
|
+
clearTimeout(timeout);
|
|
1275
|
+
resolve({
|
|
1276
|
+
skip: false,
|
|
1277
|
+
result: {
|
|
1278
|
+
passed: false,
|
|
1279
|
+
output: error.message,
|
|
1280
|
+
exitCode: 1
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function extractTestCommand(taskDescription) {
|
|
1287
|
+
const testPatterns = [
|
|
1288
|
+
/test:\s*(.+)/i,
|
|
1289
|
+
/run:\s*(.+)/i,
|
|
1290
|
+
/execute:\s*(.+)/i,
|
|
1291
|
+
/```bash\n(.+)\n```/,
|
|
1292
|
+
/`([^`]+)`/
|
|
1293
|
+
];
|
|
1294
|
+
for (const pattern of testPatterns) {
|
|
1295
|
+
const match = taskDescription.match(pattern);
|
|
1296
|
+
if (match && match[1]) {
|
|
1297
|
+
return match[1].trim();
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/tools/dispatch-task.ts
|
|
1304
|
+
var pluginClient = null;
|
|
1305
|
+
function setPluginContext(client) {
|
|
1306
|
+
pluginClient = client;
|
|
1307
|
+
}
|
|
1308
|
+
async function readSettings(directory) {
|
|
1309
|
+
const settingsPath = join4(directory, "Manifold", "settings.json");
|
|
1310
|
+
if (existsSync4(settingsPath)) {
|
|
1311
|
+
const content = await readFile4(settingsPath, "utf-8");
|
|
1312
|
+
return JSON.parse(content);
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
maxLoops: 3,
|
|
1316
|
+
maxRetries: 1,
|
|
1317
|
+
maxResults: 10,
|
|
1318
|
+
recentTaskCount: 3,
|
|
1319
|
+
clerkRetryEnabled: true,
|
|
1320
|
+
timeout: 300,
|
|
1321
|
+
testCommand: null
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
async function updatePlansRegistry(directory, planFile) {
|
|
1325
|
+
const plansPath = join4(directory, "Manifold", "plans.json");
|
|
1326
|
+
let plans = {};
|
|
1327
|
+
if (existsSync4(plansPath)) {
|
|
1328
|
+
const content = await readFile4(plansPath, "utf-8");
|
|
1329
|
+
plans = JSON.parse(content);
|
|
1330
|
+
}
|
|
1331
|
+
const planSlug = planFile.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().substring(0, 30);
|
|
1332
|
+
if (!plans[planSlug]) {
|
|
1333
|
+
plans[planSlug] = {
|
|
1334
|
+
full_title: planFile,
|
|
1335
|
+
task_count: 0,
|
|
1336
|
+
status: "in_progress",
|
|
1337
|
+
created: new Date().toISOString().split("T")[0],
|
|
1338
|
+
plan_file: planFile
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
plans[planSlug].task_count++;
|
|
1342
|
+
await writeFile4(plansPath, JSON.stringify(plans, null, 2));
|
|
1343
|
+
return plans[planSlug].task_count;
|
|
1344
|
+
}
|
|
1345
|
+
function getClient() {
|
|
1346
|
+
if (!pluginClient) {
|
|
1347
|
+
throw new Error("Plugin client not initialized");
|
|
1348
|
+
}
|
|
1349
|
+
return pluginClient;
|
|
1350
|
+
}
|
|
1351
|
+
var dispatchTaskTool = tool({
|
|
1352
|
+
description: "Dispatch a task to the multi-agent development system. The Lead Dev agent uses this to execute tasks from a plan.",
|
|
1353
|
+
args: {
|
|
1354
|
+
task_number: tool.schema.number().describe("Sequential task number"),
|
|
1355
|
+
description: tool.schema.string().describe("One-line task description"),
|
|
1356
|
+
plan_file: tool.schema.string().describe("Path to the plan document")
|
|
1357
|
+
},
|
|
1358
|
+
async execute(args) {
|
|
1359
|
+
const { task_number, description, plan_file } = args;
|
|
1360
|
+
const client = getClient();
|
|
1361
|
+
await client.app.log({
|
|
1362
|
+
body: {
|
|
1363
|
+
service: "opencode-manifold",
|
|
1364
|
+
level: "info",
|
|
1365
|
+
message: `dispatchTask called: task ${task_number} - ${description}`
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
const directory = process.cwd();
|
|
1369
|
+
const settings = await readSettings(directory);
|
|
1370
|
+
await updatePlansRegistry(directory, plan_file);
|
|
1371
|
+
await client.app.log({
|
|
1372
|
+
body: {
|
|
1373
|
+
service: "opencode-manifold",
|
|
1374
|
+
level: "info",
|
|
1375
|
+
message: `Settings loaded: maxLoops=${settings.maxLoops}, timeout=${settings.timeout}s`
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
const slug = plan_file.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().substring(0, 30);
|
|
1379
|
+
const stateMachineResult = await runStateMachine({
|
|
1380
|
+
task_number,
|
|
1381
|
+
description,
|
|
1382
|
+
plan_file,
|
|
1383
|
+
slug
|
|
1384
|
+
}, settings, { client, directory });
|
|
1385
|
+
await client.app.log({
|
|
1386
|
+
body: {
|
|
1387
|
+
service: "opencode-manifold",
|
|
1388
|
+
level: "info",
|
|
1389
|
+
message: `State machine completed: ${stateMachineResult.status}`
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
const testResult = await runTest(directory, description, { testCommand: settings.testCommand, timeout: settings.timeout });
|
|
1393
|
+
if (!testResult.skip && testResult.result) {
|
|
1394
|
+
if (testResult.result.passed) {
|
|
1395
|
+
await client.app.log({
|
|
1396
|
+
body: {
|
|
1397
|
+
service: "opencode-manifold",
|
|
1398
|
+
level: "info",
|
|
1399
|
+
message: "Test passed"
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
} else {
|
|
1403
|
+
await client.app.log({
|
|
1404
|
+
body: {
|
|
1405
|
+
service: "opencode-manifold",
|
|
1406
|
+
level: "error",
|
|
1407
|
+
message: `Test failed: ${testResult.result.output}`
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
return JSON.stringify({
|
|
1411
|
+
status: "escalated_user",
|
|
1412
|
+
summary: `Task ${task_number}: ${description} - Test failed
|
|
1413
|
+
|
|
1414
|
+
${testResult.result.output}`,
|
|
1415
|
+
files_changed: stateMachineResult.files_changed,
|
|
1416
|
+
loops: stateMachineResult.loops,
|
|
1417
|
+
wiki_entry: stateMachineResult.wiki_entry
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
await client.app.log({
|
|
1422
|
+
body: {
|
|
1423
|
+
service: "opencode-manifold",
|
|
1424
|
+
level: "info",
|
|
1425
|
+
message: `dispatchTask completed: task ${task_number}`
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
return JSON.stringify({
|
|
1429
|
+
status: stateMachineResult.status,
|
|
1430
|
+
summary: stateMachineResult.summary,
|
|
1431
|
+
files_changed: stateMachineResult.files_changed,
|
|
1432
|
+
loops: stateMachineResult.loops,
|
|
1433
|
+
wiki_entry: stateMachineResult.wiki_entry
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
// src/index.ts
|
|
1439
|
+
var ManifoldPlugin = async (ctx) => {
|
|
1440
|
+
setPluginContext(ctx.client);
|
|
1441
|
+
await ctx.client.app.log({
|
|
1442
|
+
body: {
|
|
1443
|
+
service: "opencode-manifold",
|
|
1444
|
+
level: "info",
|
|
1445
|
+
message: "Open Manifold plugin loaded"
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
return {
|
|
1449
|
+
tool: {
|
|
1450
|
+
dispatchTask: dispatchTaskTool
|
|
1451
|
+
},
|
|
1452
|
+
event: async ({ event }) => {
|
|
1453
|
+
if (event.type === "session.created") {
|
|
1454
|
+
await ctx.client.app.log({
|
|
1455
|
+
body: {
|
|
1456
|
+
service: "opencode-manifold",
|
|
1457
|
+
level: "info",
|
|
1458
|
+
message: "Session created, checking for Open Manifold setup"
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
await setupProject(ctx.directory, ctx.client);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
};
|
|
1466
|
+
export {
|
|
1467
|
+
ManifoldPlugin
|
|
1468
|
+
};
|