@xqli02/mneme 0.1.12 → 0.1.14
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/bin/mneme.mjs +4 -6
- package/package.json +1 -1
- package/src/commands/auto.mjs +129 -1976
- package/src/opencode-client.mjs +9 -5
package/src/commands/auto.mjs
CHANGED
|
@@ -1,103 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mneme auto —
|
|
2
|
+
* mneme auto — Launch opencode with oh-my-opencode agents and mneme tools.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. exec opencode attach (foreground TUI)
|
|
4
|
+
* This is a simple launcher that:
|
|
5
|
+
* 1. Ensures Dolt is running (required for beads task management)
|
|
6
|
+
* 2. Launches opencode TUI (default) or headless mode (--headless)
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Use --headless for the original CLI mode (no TUI, streaming to stdout).
|
|
14
|
-
*
|
|
15
|
-
* Uses two agents in the same opencode session:
|
|
16
|
-
* - Planner (default: gpt-4.1): analyzes goal, breaks down tasks, reviews results
|
|
17
|
-
* - Executor (default: claude-opus-4.6): writes code, runs commands, implements changes
|
|
8
|
+
* Agent orchestration (planning, delegation, auto-continuation) is handled
|
|
9
|
+
* entirely by oh-my-opencode's built-in system (Sisyphus, Ralph Loop, etc.).
|
|
10
|
+
* Mneme provides supplementary tools (beads/ledger) via the plugin in
|
|
11
|
+
* .opencode/plugins/mneme.ts.
|
|
18
12
|
*
|
|
19
13
|
* Usage:
|
|
20
|
-
* mneme auto
|
|
21
|
-
* mneme auto "Build auth module"
|
|
22
|
-
* mneme auto --headless
|
|
23
|
-
* mneme auto --
|
|
24
|
-
* mneme auto --port 4096
|
|
25
|
-
* mneme auto --planner github-copilot/gpt-4.1 --executor github-copilot/claude-opus-4.6
|
|
14
|
+
* mneme auto # Launch opencode TUI
|
|
15
|
+
* mneme auto "Build auth module" # TUI with initial goal
|
|
16
|
+
* mneme auto --headless "Fix bug" # Headless mode (opencode run)
|
|
17
|
+
* mneme auto --headless # Headless, prompts for goal
|
|
18
|
+
* mneme auto --port 4096 # Specify serve port
|
|
26
19
|
*/
|
|
27
20
|
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { fork, execSync } from "node:child_process";
|
|
32
|
-
import {
|
|
33
|
-
startOpencodeServer,
|
|
34
|
-
attachOpencodeServer,
|
|
35
|
-
parseModelSpec,
|
|
36
|
-
findOpencodeProcess,
|
|
37
|
-
} from "../opencode-server.mjs";
|
|
38
|
-
import { createClient } from "../opencode-client.mjs";
|
|
39
|
-
import { color, log, run, has } from "../utils.mjs";
|
|
40
|
-
|
|
41
|
-
// ── Default models ──────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
const DEFAULT_PLANNER = "github-copilot/gpt-4.1";
|
|
44
|
-
const DEFAULT_EXECUTOR = "github-copilot/claude-opus-4.6";
|
|
45
|
-
|
|
46
|
-
// ── Log file path ───────────────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
const LOG_FILE = ".mneme-auto.log";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { isPortOpen, startDoltServer } from "../dolt.mjs";
|
|
23
|
+
import { has, log, color } from "../utils.mjs";
|
|
49
24
|
|
|
50
25
|
// ── Argument parsing ────────────────────────────────────────────────────────
|
|
51
26
|
|
|
52
27
|
function parseArgs(argv) {
|
|
53
28
|
const opts = {
|
|
54
29
|
goal: null,
|
|
55
|
-
|
|
56
|
-
port:
|
|
57
|
-
maxCycles: 50, // planner-executor cycles
|
|
58
|
-
planner: DEFAULT_PLANNER,
|
|
59
|
-
executor: DEFAULT_EXECUTOR,
|
|
60
|
-
headless: false, // --headless: use original CLI mode
|
|
61
|
-
_daemon: false, // --_daemon: internal flag for forked daemon process
|
|
62
|
-
_daemonUrl: null, // --_daemon-url: server URL passed to daemon
|
|
63
|
-
_daemonSessionId: null, // --_daemon-session: session ID passed to daemon
|
|
30
|
+
headless: false,
|
|
31
|
+
port: null, // let opencode pick a port unless specified
|
|
64
32
|
};
|
|
65
33
|
const positional = [];
|
|
66
34
|
|
|
67
35
|
for (let i = 0; i < argv.length; i++) {
|
|
68
36
|
const arg = argv[i];
|
|
69
|
-
if (arg === "--
|
|
70
|
-
opts.
|
|
71
|
-
} else if (arg.startsWith("--attach=")) {
|
|
72
|
-
opts.attach = arg.split("=").slice(1).join("=");
|
|
37
|
+
if (arg === "--headless" || arg === "-H") {
|
|
38
|
+
opts.headless = true;
|
|
73
39
|
} else if (arg === "--port" && argv[i + 1]) {
|
|
74
40
|
opts.port = parseInt(argv[++i], 10);
|
|
75
41
|
} else if (arg.startsWith("--port=")) {
|
|
76
42
|
opts.port = parseInt(arg.split("=")[1], 10);
|
|
77
|
-
} else if (arg === "--max-cycles" && argv[i + 1]) {
|
|
78
|
-
opts.maxCycles = parseInt(argv[++i], 10);
|
|
79
|
-
} else if (arg === "--planner" && argv[i + 1]) {
|
|
80
|
-
opts.planner = argv[++i];
|
|
81
|
-
} else if (arg.startsWith("--planner=")) {
|
|
82
|
-
opts.planner = arg.split("=").slice(1).join("=");
|
|
83
|
-
} else if (arg === "--executor" && argv[i + 1]) {
|
|
84
|
-
opts.executor = argv[++i];
|
|
85
|
-
} else if (arg.startsWith("--executor=")) {
|
|
86
|
-
opts.executor = arg.split("=").slice(1).join("=");
|
|
87
|
-
} else if (arg === "--headless") {
|
|
88
|
-
opts.headless = true;
|
|
89
|
-
} else if (arg === "--_daemon") {
|
|
90
|
-
opts._daemon = true;
|
|
91
|
-
} else if (arg === "--_daemon-url" && argv[i + 1]) {
|
|
92
|
-
opts._daemonUrl = argv[++i];
|
|
93
|
-
} else if (arg.startsWith("--_daemon-url=")) {
|
|
94
|
-
opts._daemonUrl = arg.split("=").slice(1).join("=");
|
|
95
|
-
} else if (arg === "--_daemon-session" && argv[i + 1]) {
|
|
96
|
-
opts._daemonSessionId = argv[++i];
|
|
97
|
-
} else if (arg.startsWith("--_daemon-session=")) {
|
|
98
|
-
opts._daemonSessionId = arg.split("=").slice(1).join("=");
|
|
99
43
|
} else if (arg === "--help" || arg === "-h") {
|
|
100
|
-
|
|
44
|
+
printHelp();
|
|
101
45
|
process.exit(0);
|
|
102
46
|
} else if (!arg.startsWith("-")) {
|
|
103
47
|
positional.push(arg);
|
|
@@ -111,1927 +55,136 @@ function parseArgs(argv) {
|
|
|
111
55
|
return opts;
|
|
112
56
|
}
|
|
113
57
|
|
|
114
|
-
function
|
|
58
|
+
function printHelp() {
|
|
115
59
|
console.log(`
|
|
116
|
-
${color.bold("mneme auto")} —
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
mneme auto
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
--headless
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Type any message Inject feedback (sent to planner next turn)
|
|
146
|
-
/go Finish goal discussion and start execution
|
|
147
|
-
/status Show bead status
|
|
148
|
-
/skip Skip current bead
|
|
149
|
-
/abort Abort current turn
|
|
150
|
-
/quit Stop and exit
|
|
60
|
+
${color.bold("mneme auto")} — Launch opencode with mneme tools and oh-my-opencode agents
|
|
61
|
+
|
|
62
|
+
${color.bold("USAGE")}
|
|
63
|
+
mneme auto [options] [goal]
|
|
64
|
+
|
|
65
|
+
${color.bold("OPTIONS")}
|
|
66
|
+
--headless, -H Run in headless mode (opencode run, no TUI)
|
|
67
|
+
--port <num> Port for opencode serve (default: auto)
|
|
68
|
+
-h, --help Show this help
|
|
69
|
+
|
|
70
|
+
${color.bold("EXAMPLES")}
|
|
71
|
+
mneme auto # Launch TUI
|
|
72
|
+
mneme auto "Build auth module" # TUI with initial message
|
|
73
|
+
mneme auto --headless "Fix bug" # Headless single run
|
|
74
|
+
mneme auto -H # Headless, interactive
|
|
75
|
+
|
|
76
|
+
${color.bold("AGENT SYSTEM")}
|
|
77
|
+
oh-my-opencode provides multi-agent orchestration:
|
|
78
|
+
- ${color.blue("Sisyphus")} Primary orchestrator (Tab to switch agents)
|
|
79
|
+
- ${color.blue("Hephaestus")} Deep coding agent
|
|
80
|
+
- ${color.blue("Prometheus")} Planning agent
|
|
81
|
+
- ${color.blue("Atlas")} Execution conductor
|
|
82
|
+
|
|
83
|
+
Mneme provides supplementary tools:
|
|
84
|
+
- ${color.blue("mneme_ready")} List available tasks
|
|
85
|
+
- ${color.blue("mneme_facts")} Read ledger facts
|
|
86
|
+
- ${color.blue("mneme_update")} Update task status/notes
|
|
87
|
+
- ${color.blue("mneme_propose_fact")} Propose new facts
|
|
88
|
+
- ... and more (see .opencode/plugins/mneme.ts)
|
|
151
89
|
`);
|
|
152
90
|
}
|
|
153
91
|
|
|
154
|
-
// ──
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Create a file-based logger for daemon mode.
|
|
158
|
-
* Replaces all console output. Truncates the log file on start.
|
|
159
|
-
*/
|
|
160
|
-
function createFileLogger(logPath) {
|
|
161
|
-
// Truncate on start
|
|
162
|
-
writeFileSync(logPath, `[mneme auto daemon] Started at ${new Date().toISOString()}\n`);
|
|
163
|
-
|
|
164
|
-
function write(level, msg) {
|
|
165
|
-
const ts = new Date().toISOString().slice(11, 23); // HH:MM:SS.mmm
|
|
166
|
-
appendFileSync(logPath, `[${ts}] [${level}] ${msg}\n`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
info: (msg) => write("INFO", msg),
|
|
171
|
-
ok: (msg) => write("OK", msg),
|
|
172
|
-
warn: (msg) => write("WARN", msg),
|
|
173
|
-
fail: (msg) => write("FAIL", msg),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ── Bead management ─────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
function getReadyBeads() {
|
|
180
|
-
const output = run("bd ready --json");
|
|
181
|
-
if (!output) return [];
|
|
182
|
-
try {
|
|
183
|
-
return JSON.parse(output);
|
|
184
|
-
} catch {
|
|
185
|
-
return parseBeadText(run("bd ready") || "");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function getInProgressBeads() {
|
|
190
|
-
const output = run("bd list --status=in_progress --json");
|
|
191
|
-
if (!output) return [];
|
|
192
|
-
try {
|
|
193
|
-
return JSON.parse(output);
|
|
194
|
-
} catch {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function getOpenBeads() {
|
|
200
|
-
const output = run("bd list --status=open --json");
|
|
201
|
-
if (!output) return [];
|
|
202
|
-
try {
|
|
203
|
-
return JSON.parse(output);
|
|
204
|
-
} catch {
|
|
205
|
-
return [];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function getBeadDetails(id) {
|
|
210
|
-
return run(`bd show ${id}`) || "";
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function parseBeadText(text) {
|
|
214
|
-
if (!text || text.includes("No ready work")) return [];
|
|
215
|
-
return text
|
|
216
|
-
.split("\n")
|
|
217
|
-
.filter((l) => l.trim())
|
|
218
|
-
.map((line) => {
|
|
219
|
-
const idMatch = line.match(/([\w-]+)\s/);
|
|
220
|
-
return idMatch ? { id: idMatch[1], raw: line } : null;
|
|
221
|
-
})
|
|
222
|
-
.filter(Boolean);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ── Prompt composition ──────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
function readFacts() {
|
|
228
|
-
const factsDir = ".ledger/facts";
|
|
229
|
-
if (!existsSync(factsDir)) return "";
|
|
230
|
-
const files = readdirSync(factsDir).filter((f) => f.endsWith(".md"));
|
|
231
|
-
const parts = [];
|
|
232
|
-
for (const file of files) {
|
|
233
|
-
const content = readFileSync(join(factsDir, file), "utf-8");
|
|
234
|
-
parts.push(`## ${file}\n\n${content}`);
|
|
235
|
-
}
|
|
236
|
-
return parts.join("\n\n---\n\n");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function readAgentsRules() {
|
|
240
|
-
if (existsSync("AGENTS.md")) {
|
|
241
|
-
return readFileSync("AGENTS.md", "utf-8");
|
|
242
|
-
}
|
|
243
|
-
return "";
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Build the initial system context (sent as first message, noReply).
|
|
248
|
-
*/
|
|
249
|
-
function buildSystemContext(opts) {
|
|
250
|
-
let ctx = "# Session Context (injected by mneme auto)\n\n";
|
|
251
|
-
ctx +=
|
|
252
|
-
"This session uses a dual-agent architecture:\n";
|
|
253
|
-
ctx += ` - **Planner** (${opts.planner}): analyzes goals, breaks down tasks, reviews results, decides next steps\n`;
|
|
254
|
-
ctx += ` - **Executor** (${opts.executor}): implements code changes, runs commands, updates beads\n\n`;
|
|
255
|
-
ctx +=
|
|
256
|
-
"The planner and executor alternate turns. Both agents can see the full conversation.\n";
|
|
257
|
-
ctx +=
|
|
258
|
-
"The planner should output structured instructions. The executor should follow them.\n\n";
|
|
259
|
-
|
|
260
|
-
const agents = readAgentsRules();
|
|
261
|
-
if (agents) {
|
|
262
|
-
ctx += "## Agent Rules (AGENTS.md)\n\n" + agents + "\n\n";
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const facts = readFacts();
|
|
266
|
-
if (facts) {
|
|
267
|
-
ctx += "## Long-term Facts (Ledger)\n\n" + facts + "\n\n";
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return ctx;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Build the planner's initial prompt for a bead.
|
|
275
|
-
*/
|
|
276
|
-
function buildPlannerBeadPrompt(beadId) {
|
|
277
|
-
const details = getBeadDetails(beadId);
|
|
278
|
-
return `## Role: Planner
|
|
279
|
-
|
|
280
|
-
You are the PLANNER. Your job is to analyze the task, break it into concrete steps, and give clear instructions to the Executor.
|
|
281
|
-
|
|
282
|
-
## Current Task (Bead: ${beadId})
|
|
283
|
-
|
|
284
|
-
\`\`\`
|
|
285
|
-
${details}
|
|
286
|
-
\`\`\`
|
|
287
|
-
|
|
288
|
-
## Instructions
|
|
289
|
-
|
|
290
|
-
1. Analyze this task carefully
|
|
291
|
-
2. Break it into specific, actionable steps
|
|
292
|
-
3. Give the Executor clear instructions for what to implement FIRST
|
|
293
|
-
4. Be specific about file paths, function names, and expected behavior
|
|
294
|
-
5. Use \`mneme update ${beadId} --notes="..."\` to track progress
|
|
295
|
-
6. When the task is fully complete, include "TASK_DONE" in your response
|
|
296
|
-
|
|
297
|
-
Output your plan and the first instruction for the Executor.`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Build the planner's initial prompt for a user-specified goal.
|
|
302
|
-
*/
|
|
303
|
-
function buildPlannerGoalPrompt(goal) {
|
|
304
|
-
return `## Role: Planner
|
|
305
|
-
|
|
306
|
-
You are the PLANNER. Your job is to analyze the goal, create a plan, and give clear instructions to the Executor.
|
|
307
|
-
|
|
308
|
-
## Goal
|
|
309
|
-
|
|
310
|
-
> ${goal}
|
|
311
|
-
|
|
312
|
-
## Instructions
|
|
313
|
-
|
|
314
|
-
1. Check existing beads with \`mneme ready\` and \`mneme list --status=open\`
|
|
315
|
-
2. If this maps to an existing bead, claim it: \`mneme update <id> --status=in_progress\`
|
|
316
|
-
3. If not, create a new bead: \`mneme create --title="..." --description="..." --type=task -p 2\`
|
|
317
|
-
4. Break the goal into specific, actionable steps
|
|
318
|
-
5. Give the Executor clear instructions for what to implement FIRST
|
|
319
|
-
6. When all work is complete, include "TASK_DONE" in your response
|
|
320
|
-
|
|
321
|
-
Output your plan and the first instruction for the Executor.`;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Build the planner's review prompt (after seeing executor results).
|
|
326
|
-
*/
|
|
327
|
-
function buildPlannerReviewPrompt(userFeedback) {
|
|
328
|
-
let prompt = `## Role: Planner
|
|
329
|
-
|
|
330
|
-
Review the Executor's work above. Then decide:
|
|
331
|
-
|
|
332
|
-
1. If more work is needed: give the next specific instruction for the Executor
|
|
333
|
-
2. If there were errors: explain what went wrong and how to fix it
|
|
334
|
-
3. If the task is fully complete: say "TASK_DONE" and summarize what was accomplished
|
|
335
|
-
|
|
336
|
-
Be specific and actionable.`;
|
|
337
|
-
|
|
338
|
-
if (userFeedback) {
|
|
339
|
-
prompt += `\n\n## User Feedback\n\nThe user has provided input that takes priority:\n\n> ${userFeedback}\n\nIncorporate this into your next instruction.`;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return prompt;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Build the planner's discovery prompt — for interactive goal discussion
|
|
347
|
-
* when no goal was provided on the command line.
|
|
348
|
-
*/
|
|
349
|
-
function buildPlannerDiscoveryPrompt() {
|
|
350
|
-
// Gather current project state for context
|
|
351
|
-
const readyBeads = run("bd ready") || "";
|
|
352
|
-
const openBeads = run("bd list --status=open") || "";
|
|
353
|
-
const inProgressBeads = run("bd list --status=in_progress") || "";
|
|
354
|
-
|
|
355
|
-
let beadContext = "";
|
|
356
|
-
if (inProgressBeads && !inProgressBeads.includes("No ")) {
|
|
357
|
-
beadContext += `### In-progress tasks:\n\`\`\`\n${inProgressBeads}\n\`\`\`\n\n`;
|
|
358
|
-
}
|
|
359
|
-
if (readyBeads && !readyBeads.includes("No ready")) {
|
|
360
|
-
beadContext += `### Ready tasks (unblocked):\n\`\`\`\n${readyBeads}\n\`\`\`\n\n`;
|
|
361
|
-
}
|
|
362
|
-
if (openBeads && !openBeads.includes("No ")) {
|
|
363
|
-
beadContext += `### Open tasks:\n\`\`\`\n${openBeads}\n\`\`\`\n\n`;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return `## Role: Planner (Goal Discovery)
|
|
367
|
-
|
|
368
|
-
You are the PLANNER in discovery mode. No goal was provided, so your job is to help the user decide what to work on.
|
|
369
|
-
|
|
370
|
-
${beadContext ? `## Current Task State\n\n${beadContext}` : "## No existing tasks found.\n\n"}## Instructions
|
|
371
|
-
|
|
372
|
-
1. Review the project state above (existing tasks, facts, codebase)
|
|
373
|
-
2. Suggest 2-3 concrete goals the user could work on, prioritized by impact
|
|
374
|
-
3. For each suggestion, explain WHY it's a good next step
|
|
375
|
-
4. Ask the user which direction they'd like to go, or if they have something else in mind
|
|
376
|
-
|
|
377
|
-
Keep your suggestions specific and actionable. The user will discuss with you and then type **\`/go\`** when they're ready to start execution.
|
|
378
|
-
|
|
379
|
-
**Important**: This is a conversation. Respond to the user's input naturally. When they type \`/go\`, the system will finalize the goal and begin the planner-executor loop.`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Build a prompt to finalize the goal after /go is received.
|
|
384
|
-
* The planner should summarize the agreed goal and produce the first
|
|
385
|
-
* executor instruction.
|
|
386
|
-
*/
|
|
387
|
-
function buildPlannerFinalizeGoalPrompt() {
|
|
388
|
-
return `## Role: Planner (Finalize Goal)
|
|
389
|
-
|
|
390
|
-
The user has typed \`/go\`, signaling they're ready to start execution.
|
|
391
|
-
|
|
392
|
-
Based on our discussion above, do the following:
|
|
393
|
-
|
|
394
|
-
1. Summarize the agreed goal in 1-2 sentences
|
|
395
|
-
2. Check existing beads with \`mneme ready\` and \`mneme list --status=open\`
|
|
396
|
-
3. If this maps to an existing bead, claim it: \`mneme update <id> --status=in_progress\`
|
|
397
|
-
4. If not, create a new bead: \`mneme create --title="..." --description="..." --type=task -p 2\`
|
|
398
|
-
5. Break the goal into specific, actionable steps
|
|
399
|
-
6. Give the Executor clear instructions for what to implement FIRST
|
|
400
|
-
7. When all work is complete (in future turns), include "TASK_DONE" in your response
|
|
401
|
-
|
|
402
|
-
Output your plan and the first instruction for the Executor.`;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Build the executor's prompt (wrapping the planner's output).
|
|
407
|
-
*/
|
|
408
|
-
function buildExecutorPrompt() {
|
|
409
|
-
return `## Role: Executor
|
|
410
|
-
|
|
411
|
-
You are the EXECUTOR. Follow the Planner's instructions above.
|
|
412
|
-
|
|
413
|
-
Rules:
|
|
414
|
-
- Implement exactly what the Planner asked for
|
|
415
|
-
- Run tests/builds if the Planner requested it
|
|
416
|
-
- Use \`mneme update <id> --notes="..."\` to record progress
|
|
417
|
-
- Use \`mneme close <id> --reason="..."\` when told the task is done
|
|
418
|
-
- Commit changes with clear messages
|
|
419
|
-
- Report what you did when finished so the Planner can review`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// ── User input handling (headless mode only) ────────────────────────────────
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Non-blocking stdin reader with message queue.
|
|
426
|
-
* Only used in --headless mode.
|
|
427
|
-
*/
|
|
428
|
-
function createInputQueue() {
|
|
429
|
-
const queue = [];
|
|
430
|
-
let rl = null;
|
|
431
|
-
let closed = false;
|
|
432
|
-
|
|
433
|
-
function start() {
|
|
434
|
-
if (!process.stdin.isTTY) return;
|
|
435
|
-
rl = createInterface({
|
|
436
|
-
input: process.stdin,
|
|
437
|
-
output: process.stdout,
|
|
438
|
-
prompt: "",
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
rl.on("line", (line) => {
|
|
442
|
-
const trimmed = line.trim();
|
|
443
|
-
if (!trimmed) return;
|
|
444
|
-
|
|
445
|
-
if (trimmed === "/quit" || trimmed === "/exit" || trimmed === "/stop") {
|
|
446
|
-
console.log(color.dim(" -> quitting after current turn..."));
|
|
447
|
-
queue.push({ type: "quit" });
|
|
448
|
-
} else if (trimmed === "/status") {
|
|
449
|
-
queue.push({ type: "status" });
|
|
450
|
-
} else if (trimmed === "/skip") {
|
|
451
|
-
console.log(
|
|
452
|
-
color.dim(" -> will skip current bead after this cycle"),
|
|
453
|
-
);
|
|
454
|
-
queue.push({ type: "skip" });
|
|
455
|
-
} else if (trimmed === "/abort") {
|
|
456
|
-
console.log(color.dim(" -> aborting current turn..."));
|
|
457
|
-
queue.push({ type: "abort" });
|
|
458
|
-
} else {
|
|
459
|
-
queue.push({ type: "message", text: trimmed });
|
|
460
|
-
console.log(
|
|
461
|
-
color.dim(" -> queued, will send to planner next cycle"),
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
rl.on("close", () => {
|
|
467
|
-
closed = true;
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function drain() {
|
|
472
|
-
return queue.splice(0);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function pushBack(item) {
|
|
476
|
-
queue.unshift(item);
|
|
477
|
-
}
|
|
92
|
+
// ── Ensure Dolt is running ──────────────────────────────────────────────────
|
|
478
93
|
|
|
479
|
-
|
|
480
|
-
|
|
94
|
+
function ensureDolt() {
|
|
95
|
+
if (!has("dolt")) {
|
|
96
|
+
log.warn("dolt is not installed — beads task management will not work");
|
|
97
|
+
return false;
|
|
481
98
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
99
|
+
if (!isPortOpen()) {
|
|
100
|
+
log.info("Starting Dolt server...");
|
|
101
|
+
const ok = startDoltServer();
|
|
102
|
+
if (!ok) {
|
|
103
|
+
log.warn("Failed to start Dolt server — beads tools may not work");
|
|
104
|
+
return false;
|
|
487
105
|
}
|
|
488
|
-
|
|
106
|
+
log.ok("Dolt server started");
|
|
489
107
|
}
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
start,
|
|
493
|
-
drain,
|
|
494
|
-
pushBack,
|
|
495
|
-
hasMessages,
|
|
496
|
-
stop,
|
|
497
|
-
get closed() {
|
|
498
|
-
return closed;
|
|
499
|
-
},
|
|
500
|
-
};
|
|
108
|
+
return true;
|
|
501
109
|
}
|
|
502
110
|
|
|
503
|
-
// ──
|
|
111
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
504
112
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
* Returns an object with methods to control display and detect turn completion.
|
|
508
|
-
*
|
|
509
|
-
* Also tracks `lastOutputTime` so callers can detect stalls.
|
|
510
|
-
* Only used in --headless mode.
|
|
511
|
-
*/
|
|
512
|
-
function createEventDisplay(client) {
|
|
513
|
-
let running = false;
|
|
514
|
-
let connected = false;
|
|
515
|
-
let turnResolve = null;
|
|
516
|
-
let currentRole = null;
|
|
517
|
-
let lastOutputTime = 0;
|
|
518
|
-
let hasReceivedAny = false;
|
|
113
|
+
export async function auto(argv) {
|
|
114
|
+
const opts = parseArgs(argv);
|
|
519
115
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const deltaParts = new Set();
|
|
116
|
+
// Ensure Dolt is running for beads
|
|
117
|
+
ensureDolt();
|
|
523
118
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
119
|
+
if (opts.headless) {
|
|
120
|
+
// Headless mode: opencode run
|
|
121
|
+
const args = ["run"];
|
|
122
|
+
if (opts.port) args.push("--port", String(opts.port));
|
|
123
|
+
if (opts.goal) {
|
|
124
|
+
args.push(opts.goal);
|
|
125
|
+
} else {
|
|
126
|
+
// No goal provided — ask interactively
|
|
127
|
+
const { createInterface } = await import("node:readline");
|
|
128
|
+
const rl = createInterface({
|
|
129
|
+
input: process.stdin,
|
|
130
|
+
output: process.stdout,
|
|
131
|
+
});
|
|
132
|
+
const goal = await new Promise((resolve) => {
|
|
133
|
+
rl.question(
|
|
134
|
+
`${color.cyan("Goal")} ${color.dim("(what should the agent work on?)")}\n> `,
|
|
135
|
+
(answer) => {
|
|
136
|
+
rl.close();
|
|
137
|
+
resolve(answer.trim());
|
|
138
|
+
},
|
|
541
139
|
);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function handleEvent(event) {
|
|
552
|
-
const type = event.type || "";
|
|
553
|
-
const props = event.properties || {};
|
|
554
|
-
|
|
555
|
-
switch (type) {
|
|
556
|
-
case "message.part.delta": {
|
|
557
|
-
const partId = props.partID || props.partId;
|
|
558
|
-
if (partId) deltaParts.add(partId);
|
|
559
|
-
if (props.field === "text" && props.delta) {
|
|
560
|
-
process.stdout.write(props.delta);
|
|
561
|
-
lastOutputTime = Date.now();
|
|
562
|
-
}
|
|
563
|
-
break;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
case "message.part.updated": {
|
|
567
|
-
if (!props.part) break;
|
|
568
|
-
const part = props.part;
|
|
569
|
-
const partId = part.id || `${props.messageID}-${props.index}`;
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
part.type === "tool-invocation" ||
|
|
573
|
-
part.type === "tool-result"
|
|
574
|
-
) {
|
|
575
|
-
displayToolPart(part, partId);
|
|
576
|
-
lastOutputTime = Date.now();
|
|
577
|
-
}
|
|
578
|
-
if (part.type === "text" && part.text) {
|
|
579
|
-
const prev = printedTextLengths.get(partId) || 0;
|
|
580
|
-
if (prev === 0 && !deltaParts.has(partId)) {
|
|
581
|
-
process.stdout.write(part.text);
|
|
582
|
-
lastOutputTime = Date.now();
|
|
583
|
-
}
|
|
584
|
-
printedTextLengths.set(partId, part.text.length);
|
|
585
|
-
}
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
case "session.status": {
|
|
590
|
-
const status = props.status?.type || props.status;
|
|
591
|
-
if (status && status !== "busy" && status !== "pending") {
|
|
592
|
-
if (turnResolve) {
|
|
593
|
-
turnResolve(status);
|
|
594
|
-
turnResolve = null;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
case "session.updated": {
|
|
601
|
-
const info = props.info || props.session || {};
|
|
602
|
-
const status = info.status?.type || info.status;
|
|
603
|
-
if (status && status !== "busy" && status !== "running" && status !== "pending") {
|
|
604
|
-
if (turnResolve) {
|
|
605
|
-
turnResolve(status);
|
|
606
|
-
turnResolve = null;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
break;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
case "message.updated": {
|
|
613
|
-
const info = props.info || {};
|
|
614
|
-
if (info.finish && info.finish !== "pending") {
|
|
615
|
-
lastOutputTime = Date.now();
|
|
616
|
-
}
|
|
617
|
-
break;
|
|
140
|
+
});
|
|
141
|
+
if (!goal) {
|
|
142
|
+
log.fail("No goal provided. Exiting.");
|
|
143
|
+
process.exit(1);
|
|
618
144
|
}
|
|
619
|
-
|
|
620
|
-
default:
|
|
621
|
-
break;
|
|
145
|
+
args.push(goal);
|
|
622
146
|
}
|
|
623
|
-
}
|
|
624
147
|
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
148
|
+
log.info(`Running: opencode ${args.join(" ")}`);
|
|
149
|
+
const result = spawnSync("opencode", args, {
|
|
150
|
+
stdio: "inherit",
|
|
151
|
+
cwd: process.cwd(),
|
|
152
|
+
});
|
|
153
|
+
process.exit(result.status ?? 0);
|
|
154
|
+
} else {
|
|
155
|
+
// TUI mode: opencode [goal]
|
|
156
|
+
// If a goal is provided, we pass it as positional arg
|
|
157
|
+
// opencode TUI doesn't accept a message arg directly,
|
|
158
|
+
// so we start TUI and let the user type or the prompt.md guides the agent
|
|
159
|
+
const args = [];
|
|
160
|
+
if (opts.port) args.push("--port", String(opts.port));
|
|
161
|
+
|
|
162
|
+
if (opts.goal) {
|
|
163
|
+
// Use 'opencode run' even in "TUI" mode when goal is provided,
|
|
164
|
+
// or better: launch TUI and let the user paste the goal.
|
|
165
|
+
// Actually, opencode TUI doesn't take an initial message.
|
|
166
|
+
// So for goal-based usage, we should tell the user.
|
|
633
167
|
console.log(
|
|
634
|
-
`\n${color.bold(
|
|
168
|
+
`\n${color.bold("Goal:")} ${opts.goal}\n` +
|
|
169
|
+
`${color.dim("Paste the goal into the TUI prompt to begin.\n")}`,
|
|
635
170
|
);
|
|
636
|
-
displayedToolStates.set(partId, "call");
|
|
637
|
-
} else if (state === "result" && lastState !== "result") {
|
|
638
|
-
const result = inv.result ?? part.result ?? "";
|
|
639
|
-
const resultStr =
|
|
640
|
-
typeof result === "string" ? result : JSON.stringify(result);
|
|
641
|
-
if (resultStr) {
|
|
642
|
-
const lines = resultStr.split("\n");
|
|
643
|
-
const preview = lines.slice(0, 8);
|
|
644
|
-
if (lines.length > 8)
|
|
645
|
-
preview.push(
|
|
646
|
-
color.dim(` ... (${lines.length - 8} more lines)`),
|
|
647
|
-
);
|
|
648
|
-
console.log(
|
|
649
|
-
color.dim(preview.map((l) => " " + l).join("\n")),
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
displayedToolStates.set(partId, "result");
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function summarizeArgs(args) {
|
|
657
|
-
if (!args) return "";
|
|
658
|
-
if (typeof args === "string") {
|
|
659
|
-
return args.length > 80 ? args.slice(0, 80) + "..." : args;
|
|
660
|
-
}
|
|
661
|
-
const pairs = [];
|
|
662
|
-
for (const [k, v] of Object.entries(args)) {
|
|
663
|
-
const val =
|
|
664
|
-
typeof v === "string"
|
|
665
|
-
? v.length > 50
|
|
666
|
-
? v.slice(0, 50) + "..."
|
|
667
|
-
: v
|
|
668
|
-
: JSON.stringify(v);
|
|
669
|
-
pairs.push(`${k}=${val}`);
|
|
670
|
-
}
|
|
671
|
-
const str = pairs.join(" ");
|
|
672
|
-
return str.length > 120 ? str.slice(0, 120) + "..." : str;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function waitForTurnEnd() {
|
|
676
|
-
return new Promise((resolve) => {
|
|
677
|
-
turnResolve = resolve;
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function resetTurn(role) {
|
|
682
|
-
currentRole = role;
|
|
683
|
-
printedTextLengths.clear();
|
|
684
|
-
displayedToolStates.clear();
|
|
685
|
-
deltaParts.clear();
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function stop() {
|
|
689
|
-
running = false;
|
|
690
|
-
if (turnResolve) {
|
|
691
|
-
turnResolve("stopped");
|
|
692
|
-
turnResolve = null;
|
|
693
171
|
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
return {
|
|
697
|
-
start,
|
|
698
|
-
stop,
|
|
699
|
-
waitForTurnEnd,
|
|
700
|
-
resetTurn,
|
|
701
|
-
get lastOutputTime() { return lastOutputTime; },
|
|
702
|
-
get connected() { return connected; },
|
|
703
|
-
get hasReceivedAny() { return hasReceivedAny; },
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// ── Daemon event monitor (silent, for daemon mode) ──────────────────────────
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* SSE listener for daemon mode — tracks turn completion and detects
|
|
711
|
-
* user-initiated messages, but produces NO stdout output.
|
|
712
|
-
* All logging goes to file.
|
|
713
|
-
*/
|
|
714
|
-
function createDaemonEventMonitor(client, dlog) {
|
|
715
|
-
let running = false;
|
|
716
|
-
let connected = false;
|
|
717
|
-
let turnResolve = null;
|
|
718
|
-
let lastOutputTime = 0;
|
|
719
|
-
let hasReceivedAny = false;
|
|
720
|
-
|
|
721
|
-
// Track known prompt texts we sent — to distinguish user messages
|
|
722
|
-
const knownPromptTexts = new Set();
|
|
723
|
-
|
|
724
|
-
// Callback for user message detection
|
|
725
|
-
let onUserMessage = null;
|
|
726
|
-
|
|
727
|
-
async function start() {
|
|
728
|
-
running = true;
|
|
729
|
-
try {
|
|
730
|
-
const iterator = await client.events.subscribe();
|
|
731
|
-
connected = true;
|
|
732
|
-
hasReceivedAny = false;
|
|
733
|
-
dlog.ok("SSE event stream connected (daemon)");
|
|
734
|
-
for await (const event of iterator) {
|
|
735
|
-
if (!running) break;
|
|
736
|
-
if (!hasReceivedAny) hasReceivedAny = true;
|
|
737
|
-
handleEvent(event);
|
|
738
|
-
}
|
|
739
|
-
} catch (err) {
|
|
740
|
-
connected = false;
|
|
741
|
-
if (running) {
|
|
742
|
-
dlog.warn(`SSE stream error: ${err.message}`);
|
|
743
|
-
await sleep(2000);
|
|
744
|
-
if (running) {
|
|
745
|
-
dlog.info("Reconnecting SSE...");
|
|
746
|
-
start().catch(() => {});
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function handleEvent(event) {
|
|
753
|
-
const type = event.type || "";
|
|
754
|
-
const props = event.properties || {};
|
|
755
172
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (props.part) {
|
|
767
|
-
lastOutputTime = Date.now();
|
|
768
|
-
}
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
case "session.status": {
|
|
773
|
-
const status = props.status?.type || props.status;
|
|
774
|
-
if (status && status !== "busy" && status !== "pending") {
|
|
775
|
-
if (turnResolve) {
|
|
776
|
-
turnResolve(status);
|
|
777
|
-
turnResolve = null;
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
break;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
case "session.updated": {
|
|
784
|
-
const info = props.info || props.session || {};
|
|
785
|
-
const status = info.status?.type || info.status;
|
|
786
|
-
if (status && status !== "busy" && status !== "running" && status !== "pending") {
|
|
787
|
-
if (turnResolve) {
|
|
788
|
-
turnResolve(status);
|
|
789
|
-
turnResolve = null;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
case "message.updated": {
|
|
796
|
-
const info = props.info || {};
|
|
797
|
-
// Detect user-initiated messages: role === "user" with text we didn't send
|
|
798
|
-
const role = info.role;
|
|
799
|
-
if (role === "user" && info.parts) {
|
|
800
|
-
const text = info.parts
|
|
801
|
-
.filter((p) => p.type === "text")
|
|
802
|
-
.map((p) => p.text || "")
|
|
803
|
-
.join("\n")
|
|
804
|
-
.trim();
|
|
805
|
-
if (text && !knownPromptTexts.has(text)) {
|
|
806
|
-
dlog.info(`User message detected: "${text.slice(0, 80)}..."`);
|
|
807
|
-
if (onUserMessage) onUserMessage(text);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
if (info.finish && info.finish !== "pending") {
|
|
811
|
-
lastOutputTime = Date.now();
|
|
812
|
-
}
|
|
813
|
-
break;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
default:
|
|
817
|
-
break;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
173
|
+
log.info("Launching opencode TUI...");
|
|
174
|
+
console.log(
|
|
175
|
+
color.dim(
|
|
176
|
+
" Agents: Sisyphus, Hephaestus, Prometheus, Atlas (Tab to switch)",
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
console.log(
|
|
180
|
+
color.dim(" Mneme tools: mneme_ready, mneme_facts, mneme_update, ..."),
|
|
181
|
+
);
|
|
182
|
+
console.log();
|
|
820
183
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
184
|
+
const result = spawnSync("opencode", args, {
|
|
185
|
+
stdio: "inherit",
|
|
186
|
+
cwd: process.cwd(),
|
|
824
187
|
});
|
|
188
|
+
process.exit(result.status ?? 0);
|
|
825
189
|
}
|
|
826
|
-
|
|
827
|
-
function resetTurn() {
|
|
828
|
-
// Nothing visual to reset in daemon mode
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function registerPrompt(text) {
|
|
832
|
-
// Register a prompt text so we can distinguish our prompts from user's
|
|
833
|
-
knownPromptTexts.add(text.trim());
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function stop() {
|
|
837
|
-
running = false;
|
|
838
|
-
if (turnResolve) {
|
|
839
|
-
turnResolve("stopped");
|
|
840
|
-
turnResolve = null;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return {
|
|
845
|
-
start,
|
|
846
|
-
stop,
|
|
847
|
-
waitForTurnEnd,
|
|
848
|
-
resetTurn,
|
|
849
|
-
registerPrompt,
|
|
850
|
-
set onUserMessage(fn) { onUserMessage = fn; },
|
|
851
|
-
get onUserMessage() { return onUserMessage; },
|
|
852
|
-
get lastOutputTime() { return lastOutputTime; },
|
|
853
|
-
get connected() { return connected; },
|
|
854
|
-
get hasReceivedAny() { return hasReceivedAny; },
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// ── Turn execution (headless mode) ──────────────────────────────────────────
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* Send a message and wait for the turn to complete (headless mode).
|
|
862
|
-
* Handles /abort and /quit from input queue during execution.
|
|
863
|
-
* Prints heartbeat every 15s when no output is flowing.
|
|
864
|
-
*
|
|
865
|
-
* @returns {{ status: string, aborted: boolean, quit: boolean }}
|
|
866
|
-
*/
|
|
867
|
-
async function executeTurnHeadless(
|
|
868
|
-
client,
|
|
869
|
-
sessionId,
|
|
870
|
-
prompt,
|
|
871
|
-
modelSpec,
|
|
872
|
-
eventDisplay,
|
|
873
|
-
inputQueue,
|
|
874
|
-
) {
|
|
875
|
-
eventDisplay.resetTurn();
|
|
876
|
-
|
|
877
|
-
const body = {
|
|
878
|
-
parts: [{ type: "text", text: prompt }],
|
|
879
|
-
};
|
|
880
|
-
if (modelSpec) {
|
|
881
|
-
body.model = modelSpec;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
await client.session.promptAsync(sessionId, body);
|
|
885
|
-
|
|
886
|
-
// Quick check for immediate model errors
|
|
887
|
-
await sleep(2000);
|
|
888
|
-
try {
|
|
889
|
-
const sessions = await client.session.list();
|
|
890
|
-
const s = sessions?.find?.((ss) => ss.id === sessionId);
|
|
891
|
-
if (s && s.status && s.status !== "running" && s.status !== "pending") {
|
|
892
|
-
const msgs = await client.session.messages(sessionId);
|
|
893
|
-
const lastMsg = msgs?.[msgs.length - 1];
|
|
894
|
-
const errInfo = lastMsg?.info?.error;
|
|
895
|
-
if (errInfo) {
|
|
896
|
-
const errMsg = errInfo.data?.message || errInfo.name || "unknown";
|
|
897
|
-
log.fail(`Model error: ${errMsg}`);
|
|
898
|
-
return { status: "error", aborted: true, quit: false };
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
} catch {
|
|
902
|
-
// Ignore probe failures
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const HEARTBEAT_INTERVAL = 15_000;
|
|
906
|
-
const SILENCE_WARN = 30_000;
|
|
907
|
-
const SILENCE_ABORT = 120_000;
|
|
908
|
-
const turnStartTime = Date.now();
|
|
909
|
-
|
|
910
|
-
return new Promise((resolve) => {
|
|
911
|
-
let resolved = false;
|
|
912
|
-
let warnedSilence = false;
|
|
913
|
-
|
|
914
|
-
const done = (result) => {
|
|
915
|
-
if (resolved) return;
|
|
916
|
-
resolved = true;
|
|
917
|
-
clearInterval(pollId);
|
|
918
|
-
clearInterval(heartbeatId);
|
|
919
|
-
resolve(result);
|
|
920
|
-
};
|
|
921
|
-
|
|
922
|
-
eventDisplay.waitForTurnEnd().then((status) => {
|
|
923
|
-
done({ status, aborted: false, quit: false });
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
const heartbeatId = setInterval(() => {
|
|
927
|
-
if (resolved) return;
|
|
928
|
-
const now = Date.now();
|
|
929
|
-
const elapsed = Math.round((now - turnStartTime) / 1000);
|
|
930
|
-
const lastOut = eventDisplay.lastOutputTime;
|
|
931
|
-
const silenceMs = lastOut > 0 ? now - lastOut : now - turnStartTime;
|
|
932
|
-
|
|
933
|
-
if (silenceMs >= SILENCE_ABORT) {
|
|
934
|
-
console.log(
|
|
935
|
-
color.dim(`\n [${elapsed}s] no output for ${Math.round(silenceMs / 1000)}s, auto-aborting`),
|
|
936
|
-
);
|
|
937
|
-
client.session.abort(sessionId).catch(() => {});
|
|
938
|
-
done({ status: "aborted", aborted: true, quit: false });
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (silenceMs >= SILENCE_WARN && !warnedSilence) {
|
|
943
|
-
warnedSilence = true;
|
|
944
|
-
console.log(
|
|
945
|
-
color.dim(`\n [${elapsed}s] no output for ${Math.round(silenceMs / 1000)}s (will abort at ${SILENCE_ABORT / 1000}s)`),
|
|
946
|
-
);
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (silenceMs >= HEARTBEAT_INTERVAL) {
|
|
951
|
-
process.stdout.write(
|
|
952
|
-
color.dim(` [${elapsed}s] `),
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
}, HEARTBEAT_INTERVAL);
|
|
956
|
-
|
|
957
|
-
const pollId = setInterval(() => {
|
|
958
|
-
if (!inputQueue.hasMessages()) return;
|
|
959
|
-
const items = inputQueue.drain();
|
|
960
|
-
for (const item of items) {
|
|
961
|
-
if (item.type === "quit") {
|
|
962
|
-
client.session.abort(sessionId).catch(() => {});
|
|
963
|
-
done({ status: "quit", aborted: false, quit: true });
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
if (item.type === "abort") {
|
|
967
|
-
client.session.abort(sessionId).catch(() => {});
|
|
968
|
-
done({ status: "aborted", aborted: true, quit: false });
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
if (item.type === "status") {
|
|
972
|
-
showBeadStatus();
|
|
973
|
-
}
|
|
974
|
-
if (item.type === "message" || item.type === "skip") {
|
|
975
|
-
inputQueue.pushBack(item);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}, 200);
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// ── Turn execution (daemon mode) ────────────────────────────────────────────
|
|
983
|
-
|
|
984
|
-
/**
|
|
985
|
-
* Send a message and wait for the turn to complete (daemon mode).
|
|
986
|
-
* No stdout output — all logging to file. No input queue.
|
|
987
|
-
*
|
|
988
|
-
* @returns {{ status: string, aborted: boolean }}
|
|
989
|
-
*/
|
|
990
|
-
async function executeTurnDaemon(
|
|
991
|
-
client,
|
|
992
|
-
sessionId,
|
|
993
|
-
prompt,
|
|
994
|
-
modelSpec,
|
|
995
|
-
monitor,
|
|
996
|
-
dlog,
|
|
997
|
-
) {
|
|
998
|
-
monitor.resetTurn();
|
|
999
|
-
monitor.registerPrompt(prompt);
|
|
1000
|
-
|
|
1001
|
-
const body = {
|
|
1002
|
-
parts: [{ type: "text", text: prompt }],
|
|
1003
|
-
};
|
|
1004
|
-
if (modelSpec) {
|
|
1005
|
-
body.model = modelSpec;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
await client.session.promptAsync(sessionId, body);
|
|
1009
|
-
|
|
1010
|
-
// Quick check for immediate model errors
|
|
1011
|
-
await sleep(2000);
|
|
1012
|
-
try {
|
|
1013
|
-
const sessions = await client.session.list();
|
|
1014
|
-
const s = sessions?.find?.((ss) => ss.id === sessionId);
|
|
1015
|
-
if (s && s.status && s.status !== "running" && s.status !== "pending") {
|
|
1016
|
-
const msgs = await client.session.messages(sessionId);
|
|
1017
|
-
const lastMsg = msgs?.[msgs.length - 1];
|
|
1018
|
-
const errInfo = lastMsg?.info?.error;
|
|
1019
|
-
if (errInfo) {
|
|
1020
|
-
const errMsg = errInfo.data?.message || errInfo.name || "unknown";
|
|
1021
|
-
dlog.fail(`Model error: ${errMsg}`);
|
|
1022
|
-
return { status: "error", aborted: true };
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
} catch {
|
|
1026
|
-
// Ignore probe failures
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const SILENCE_ABORT = 120_000;
|
|
1030
|
-
const turnStartTime = Date.now();
|
|
1031
|
-
|
|
1032
|
-
return new Promise((resolve) => {
|
|
1033
|
-
let resolved = false;
|
|
1034
|
-
|
|
1035
|
-
const done = (result) => {
|
|
1036
|
-
if (resolved) return;
|
|
1037
|
-
resolved = true;
|
|
1038
|
-
clearInterval(silenceCheckId);
|
|
1039
|
-
resolve(result);
|
|
1040
|
-
};
|
|
1041
|
-
|
|
1042
|
-
monitor.waitForTurnEnd().then((status) => {
|
|
1043
|
-
done({ status, aborted: false });
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
// Check for silence timeouts
|
|
1047
|
-
const silenceCheckId = setInterval(() => {
|
|
1048
|
-
if (resolved) return;
|
|
1049
|
-
const now = Date.now();
|
|
1050
|
-
const elapsed = Math.round((now - turnStartTime) / 1000);
|
|
1051
|
-
const lastOut = monitor.lastOutputTime;
|
|
1052
|
-
const silenceMs = lastOut > 0 ? now - lastOut : now - turnStartTime;
|
|
1053
|
-
|
|
1054
|
-
if (silenceMs >= SILENCE_ABORT) {
|
|
1055
|
-
dlog.warn(`${elapsed}s elapsed, no output for ${Math.round(silenceMs / 1000)}s — auto-aborting`);
|
|
1056
|
-
client.session.abort(sessionId).catch(() => {});
|
|
1057
|
-
done({ status: "aborted", aborted: true });
|
|
1058
|
-
}
|
|
1059
|
-
}, 15_000);
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// ── Status display (headless only) ─────────────────────────────────────────
|
|
1064
|
-
|
|
1065
|
-
function showBeadStatus() {
|
|
1066
|
-
console.log(`\n${color.bold("-- Status --")}`);
|
|
1067
|
-
const ready = run("bd ready") || " (none)";
|
|
1068
|
-
const inProgress = run("bd list --status=in_progress") || " (none)";
|
|
1069
|
-
console.log(` ${color.bold("Ready:")} ${ready}`);
|
|
1070
|
-
console.log(` ${color.bold("In Progress:")} ${inProgress}`);
|
|
1071
|
-
console.log("");
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// ── Supervisor loop (headless mode — original CLI) ──────────────────────────
|
|
1075
|
-
|
|
1076
|
-
async function supervisorLoopHeadless(client, opts, inputQueue) {
|
|
1077
|
-
const plannerModel = parseModelSpec(opts.planner);
|
|
1078
|
-
const executorModel = parseModelSpec(opts.executor);
|
|
1079
|
-
|
|
1080
|
-
log.info("Creating session...");
|
|
1081
|
-
const session = await client.session.create({ title: "mneme auto" });
|
|
1082
|
-
const sessionId = session.id;
|
|
1083
|
-
log.ok(`Session: ${sessionId}`);
|
|
1084
|
-
|
|
1085
|
-
// Validate models
|
|
1086
|
-
log.info("Validating models (API probe)...");
|
|
1087
|
-
await validateModels(client, sessionId, opts, log);
|
|
1088
|
-
|
|
1089
|
-
// Start SSE event display
|
|
1090
|
-
const eventDisplay = createEventDisplay(client);
|
|
1091
|
-
eventDisplay.start().catch(() => {});
|
|
1092
|
-
|
|
1093
|
-
// Inject system context
|
|
1094
|
-
const systemContext = buildSystemContext(opts);
|
|
1095
|
-
try {
|
|
1096
|
-
await client.session.prompt(sessionId, {
|
|
1097
|
-
noReply: true,
|
|
1098
|
-
parts: [{ type: "text", text: systemContext }],
|
|
1099
|
-
});
|
|
1100
|
-
log.ok("Context injected");
|
|
1101
|
-
} catch (err) {
|
|
1102
|
-
log.warn(`Context injection: ${err.message}`);
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
let cycle = 0;
|
|
1106
|
-
let startMode = "beads"; // default: pick from beads
|
|
1107
|
-
|
|
1108
|
-
// If no explicit goal, ask user whether to pick from beads or discuss a plan.
|
|
1109
|
-
if (!opts.goal) {
|
|
1110
|
-
const choice = await askStartModeHeadless(inputQueue);
|
|
1111
|
-
if (choice === "quit") {
|
|
1112
|
-
eventDisplay.stop();
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
startMode = choice; // "beads" or "discuss"
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// If user chose to discuss, enter goal discussion before main loop.
|
|
1119
|
-
if (startMode === "discuss") {
|
|
1120
|
-
const goResult = await goalDiscussionHeadless(
|
|
1121
|
-
client, sessionId, plannerModel, eventDisplay, inputQueue,
|
|
1122
|
-
);
|
|
1123
|
-
if (!goResult) {
|
|
1124
|
-
log.info("User quit during goal discussion.");
|
|
1125
|
-
eventDisplay.stop();
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
// Goal discussion complete — planner finalize prompt has produced
|
|
1129
|
-
// the first executor instruction. Jump straight to executor turn.
|
|
1130
|
-
cycle++;
|
|
1131
|
-
console.log(
|
|
1132
|
-
`\n${color.bold(`-- Cycle ${cycle} / Executor`)} ${color.dim(`(${opts.executor})`)} ${color.bold("-------------------")}`,
|
|
1133
|
-
);
|
|
1134
|
-
const executorPrompt = buildExecutorPrompt();
|
|
1135
|
-
log.info(`Sending prompt to Executor (${opts.executor})...`);
|
|
1136
|
-
eventDisplay.resetTurn("executor");
|
|
1137
|
-
const executorResult = await executeTurnHeadless(
|
|
1138
|
-
client, sessionId, executorPrompt, executorModel,
|
|
1139
|
-
eventDisplay, inputQueue,
|
|
1140
|
-
);
|
|
1141
|
-
console.log("");
|
|
1142
|
-
if (executorResult.quit) { log.info("User requested quit."); eventDisplay.stop(); return; }
|
|
1143
|
-
if (executorResult.aborted) { log.info("Executor turn aborted."); }
|
|
1144
|
-
await sleep(1000);
|
|
1145
|
-
// Fall through to the main loop (cycle is now 1, will get planner review)
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
try {
|
|
1149
|
-
while (cycle < opts.maxCycles) {
|
|
1150
|
-
let userFeedback = null;
|
|
1151
|
-
let shouldSkip = false;
|
|
1152
|
-
|
|
1153
|
-
if (inputQueue.hasMessages()) {
|
|
1154
|
-
const items = inputQueue.drain();
|
|
1155
|
-
for (const item of items) {
|
|
1156
|
-
if (item.type === "quit") {
|
|
1157
|
-
log.info("User requested quit.");
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (item.type === "skip") shouldSkip = true;
|
|
1161
|
-
if (item.type === "status") showBeadStatus();
|
|
1162
|
-
if (item.type === "message") userFeedback = item.text;
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
if (shouldSkip) {
|
|
1167
|
-
log.info("Skipping current bead...");
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
let plannerPrompt = null;
|
|
1171
|
-
|
|
1172
|
-
if (cycle === 0) {
|
|
1173
|
-
if (opts.goal) {
|
|
1174
|
-
plannerPrompt = buildPlannerGoalPrompt(opts.goal);
|
|
1175
|
-
} else {
|
|
1176
|
-
// No explicit goal, but beads exist — pick from beads
|
|
1177
|
-
plannerPrompt = pickBeadForPlanner(log);
|
|
1178
|
-
}
|
|
1179
|
-
} else {
|
|
1180
|
-
plannerPrompt = buildPlannerReviewPrompt(userFeedback);
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (!plannerPrompt) {
|
|
1184
|
-
const open = getOpenBeads();
|
|
1185
|
-
if (open.length === 0) {
|
|
1186
|
-
log.ok("All beads completed! Nothing left to do.");
|
|
1187
|
-
break;
|
|
1188
|
-
}
|
|
1189
|
-
log.warn("All beads blocked. Waiting for user input...");
|
|
1190
|
-
console.log(color.dim(" Type a message or /quit to exit."));
|
|
1191
|
-
await waitForInput(inputQueue);
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
cycle++;
|
|
1196
|
-
|
|
1197
|
-
// Planner turn
|
|
1198
|
-
console.log(
|
|
1199
|
-
`\n${color.bold(`-- Cycle ${cycle} / Planner`)} ${color.dim(`(${opts.planner})`)} ${color.bold("--------------------")}`,
|
|
1200
|
-
);
|
|
1201
|
-
|
|
1202
|
-
log.info(`Sending prompt to Planner (${opts.planner})...`);
|
|
1203
|
-
eventDisplay.resetTurn("planner");
|
|
1204
|
-
const plannerResult = await executeTurnHeadless(
|
|
1205
|
-
client, sessionId, plannerPrompt, plannerModel,
|
|
1206
|
-
eventDisplay, inputQueue,
|
|
1207
|
-
);
|
|
1208
|
-
console.log("");
|
|
1209
|
-
|
|
1210
|
-
if (plannerResult.quit) { log.info("User requested quit."); return; }
|
|
1211
|
-
if (plannerResult.aborted) { log.info("Planner turn aborted."); continue; }
|
|
1212
|
-
|
|
1213
|
-
// Check TASK_DONE
|
|
1214
|
-
let plannerSaidDone = false;
|
|
1215
|
-
try {
|
|
1216
|
-
const messages = await client.session.messages(sessionId);
|
|
1217
|
-
if (messages && messages.length > 0) {
|
|
1218
|
-
const lastMsg = messages[messages.length - 1];
|
|
1219
|
-
const text = extractMessageText(lastMsg);
|
|
1220
|
-
if (text.includes("TASK_DONE")) plannerSaidDone = true;
|
|
1221
|
-
}
|
|
1222
|
-
} catch { /* proceed */ }
|
|
1223
|
-
|
|
1224
|
-
if (plannerSaidDone) {
|
|
1225
|
-
log.ok("Planner declared task complete.");
|
|
1226
|
-
cycle = 0;
|
|
1227
|
-
const nextBead = pickBeadForPlanner(log);
|
|
1228
|
-
if (!nextBead) { log.ok("No more tasks. Finished."); break; }
|
|
1229
|
-
continue;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Executor turn
|
|
1233
|
-
console.log(
|
|
1234
|
-
`\n${color.bold(`-- Cycle ${cycle} / Executor`)} ${color.dim(`(${opts.executor})`)} ${color.bold("-------------------")}`,
|
|
1235
|
-
);
|
|
1236
|
-
|
|
1237
|
-
const executorPrompt = buildExecutorPrompt();
|
|
1238
|
-
log.info(`Sending prompt to Executor (${opts.executor})...`);
|
|
1239
|
-
eventDisplay.resetTurn("executor");
|
|
1240
|
-
const executorResult = await executeTurnHeadless(
|
|
1241
|
-
client, sessionId, executorPrompt, executorModel,
|
|
1242
|
-
eventDisplay, inputQueue,
|
|
1243
|
-
);
|
|
1244
|
-
console.log("");
|
|
1245
|
-
|
|
1246
|
-
if (executorResult.quit) { log.info("User requested quit."); return; }
|
|
1247
|
-
if (executorResult.aborted) { log.info("Executor turn aborted."); }
|
|
1248
|
-
|
|
1249
|
-
await sleep(1000);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
if (cycle >= opts.maxCycles) {
|
|
1253
|
-
log.warn(`Reached max cycles (${opts.maxCycles}). Stopping.`);
|
|
1254
|
-
}
|
|
1255
|
-
} finally {
|
|
1256
|
-
eventDisplay.stop();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// ── Supervisor loop (daemon mode) ───────────────────────────────────────────
|
|
1261
|
-
|
|
1262
|
-
async function supervisorLoopDaemon(client, sessionId, opts, dlog) {
|
|
1263
|
-
const plannerModel = parseModelSpec(opts.planner);
|
|
1264
|
-
const executorModel = parseModelSpec(opts.executor);
|
|
1265
|
-
|
|
1266
|
-
// Start SSE event monitor (silent)
|
|
1267
|
-
const monitor = createDaemonEventMonitor(client, dlog);
|
|
1268
|
-
|
|
1269
|
-
// Track whether user is interacting via TUI
|
|
1270
|
-
let userInteracting = false;
|
|
1271
|
-
let userTurnResolve = null;
|
|
1272
|
-
|
|
1273
|
-
monitor.onUserMessage = (text) => {
|
|
1274
|
-
dlog.info(`User typed in TUI, pausing auto loop...`);
|
|
1275
|
-
userInteracting = true;
|
|
1276
|
-
// The user message triggers a model response. We need to wait for
|
|
1277
|
-
// that response to complete before resuming our auto loop.
|
|
1278
|
-
// The next session.status=idle will signal completion.
|
|
1279
|
-
};
|
|
1280
|
-
|
|
1281
|
-
monitor.start().catch(() => {});
|
|
1282
|
-
|
|
1283
|
-
// Inject system context
|
|
1284
|
-
const systemContext = buildSystemContext(opts);
|
|
1285
|
-
monitor.registerPrompt(systemContext);
|
|
1286
|
-
try {
|
|
1287
|
-
await client.session.prompt(sessionId, {
|
|
1288
|
-
noReply: true,
|
|
1289
|
-
parts: [{ type: "text", text: systemContext }],
|
|
1290
|
-
});
|
|
1291
|
-
dlog.ok("Context injected");
|
|
1292
|
-
} catch (err) {
|
|
1293
|
-
dlog.warn(`Context injection: ${err.message}`);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
let cycle = 0;
|
|
1297
|
-
|
|
1298
|
-
// If no explicit goal, enter goal discussion with planner.
|
|
1299
|
-
// The discovery prompt lists existing beads (if any) and suggestions.
|
|
1300
|
-
// User discusses in TUI, types /go when ready. If they want to pick
|
|
1301
|
-
// from beads, the planner will incorporate that into its plan.
|
|
1302
|
-
if (!opts.goal) {
|
|
1303
|
-
dlog.info("No goal specified — entering goal discussion with Planner.");
|
|
1304
|
-
const goResult = await goalDiscussionDaemon(
|
|
1305
|
-
client, sessionId, plannerModel, monitor, dlog,
|
|
1306
|
-
);
|
|
1307
|
-
if (!goResult) {
|
|
1308
|
-
dlog.info("Goal discussion ended without /go. Exiting.");
|
|
1309
|
-
monitor.stop();
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
// Goal discussion complete — planner finalize prompt has produced
|
|
1313
|
-
// the first executor instruction. Jump straight to executor turn.
|
|
1314
|
-
cycle++;
|
|
1315
|
-
dlog.info(`Cycle ${cycle} / Executor (${opts.executor}) [post-goal-discussion]`);
|
|
1316
|
-
const executorPrompt = buildExecutorPrompt();
|
|
1317
|
-
const executorResult = await executeTurnDaemon(
|
|
1318
|
-
client, sessionId, executorPrompt, executorModel, monitor, dlog,
|
|
1319
|
-
);
|
|
1320
|
-
if (executorResult.aborted) {
|
|
1321
|
-
dlog.warn("Executor turn aborted.");
|
|
1322
|
-
}
|
|
1323
|
-
await sleep(1000);
|
|
1324
|
-
// Fall through to the main loop (cycle is now 1, will get planner review)
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
while (cycle < opts.maxCycles) {
|
|
1329
|
-
// If user is interacting, wait for the model to finish responding
|
|
1330
|
-
// to their message before we send the next auto prompt
|
|
1331
|
-
if (userInteracting) {
|
|
1332
|
-
dlog.info("Waiting for user's turn to complete...");
|
|
1333
|
-
await monitor.waitForTurnEnd();
|
|
1334
|
-
userInteracting = false;
|
|
1335
|
-
dlog.info("User turn complete, resuming auto loop.");
|
|
1336
|
-
// After user intervention, planner should review
|
|
1337
|
-
// (fall through to planner review prompt)
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
let plannerPrompt = null;
|
|
1341
|
-
|
|
1342
|
-
if (cycle === 0) {
|
|
1343
|
-
if (opts.goal) {
|
|
1344
|
-
plannerPrompt = buildPlannerGoalPrompt(opts.goal);
|
|
1345
|
-
} else {
|
|
1346
|
-
// No explicit goal, but beads exist — pick from beads
|
|
1347
|
-
plannerPrompt = pickBeadForPlanner(dlog);
|
|
1348
|
-
}
|
|
1349
|
-
} else {
|
|
1350
|
-
plannerPrompt = buildPlannerReviewPrompt(null);
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (!plannerPrompt) {
|
|
1354
|
-
const open = getOpenBeads();
|
|
1355
|
-
if (open.length === 0) {
|
|
1356
|
-
dlog.ok("All beads completed! Nothing left to do.");
|
|
1357
|
-
break;
|
|
1358
|
-
}
|
|
1359
|
-
dlog.warn("All beads blocked. Waiting 30s before retry...");
|
|
1360
|
-
await sleep(30_000);
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
cycle++;
|
|
1365
|
-
|
|
1366
|
-
// Planner turn
|
|
1367
|
-
dlog.info(`Cycle ${cycle} / Planner (${opts.planner})`);
|
|
1368
|
-
const plannerResult = await executeTurnDaemon(
|
|
1369
|
-
client, sessionId, plannerPrompt, plannerModel, monitor, dlog,
|
|
1370
|
-
);
|
|
1371
|
-
|
|
1372
|
-
if (plannerResult.aborted) {
|
|
1373
|
-
dlog.warn("Planner turn aborted.");
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Check TASK_DONE
|
|
1378
|
-
let plannerSaidDone = false;
|
|
1379
|
-
try {
|
|
1380
|
-
const messages = await client.session.messages(sessionId);
|
|
1381
|
-
if (messages && messages.length > 0) {
|
|
1382
|
-
const lastMsg = messages[messages.length - 1];
|
|
1383
|
-
const text = extractMessageText(lastMsg);
|
|
1384
|
-
if (text.includes("TASK_DONE")) plannerSaidDone = true;
|
|
1385
|
-
}
|
|
1386
|
-
} catch { /* proceed */ }
|
|
1387
|
-
|
|
1388
|
-
if (plannerSaidDone) {
|
|
1389
|
-
dlog.ok("Planner declared task complete.");
|
|
1390
|
-
cycle = 0;
|
|
1391
|
-
const nextBead = pickBeadForPlanner(dlog);
|
|
1392
|
-
if (!nextBead) { dlog.ok("No more tasks. Finished."); break; }
|
|
1393
|
-
continue;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// Executor turn
|
|
1397
|
-
dlog.info(`Cycle ${cycle} / Executor (${opts.executor})`);
|
|
1398
|
-
const executorPrompt = buildExecutorPrompt();
|
|
1399
|
-
const executorResult = await executeTurnDaemon(
|
|
1400
|
-
client, sessionId, executorPrompt, executorModel, monitor, dlog,
|
|
1401
|
-
);
|
|
1402
|
-
|
|
1403
|
-
if (executorResult.aborted) {
|
|
1404
|
-
dlog.warn("Executor turn aborted.");
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
await sleep(1000);
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
if (cycle >= opts.maxCycles) {
|
|
1411
|
-
dlog.warn(`Reached max cycles (${opts.maxCycles}). Stopping.`);
|
|
1412
|
-
}
|
|
1413
|
-
} finally {
|
|
1414
|
-
monitor.stop();
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
1419
|
-
|
|
1420
|
-
/**
|
|
1421
|
-
* Validate models by sending a real test prompt.
|
|
1422
|
-
* Works with both console logger (log) and file logger (dlog).
|
|
1423
|
-
*/
|
|
1424
|
-
async function validateModels(client, sessionId, opts, logger) {
|
|
1425
|
-
const plannerModel = parseModelSpec(opts.planner);
|
|
1426
|
-
const executorModel = parseModelSpec(opts.executor);
|
|
1427
|
-
const probeModels = [
|
|
1428
|
-
{ label: "Planner", spec: opts.planner, parsed: plannerModel },
|
|
1429
|
-
{ label: "Executor", spec: opts.executor, parsed: executorModel },
|
|
1430
|
-
];
|
|
1431
|
-
const seen = new Set();
|
|
1432
|
-
for (const m of probeModels) {
|
|
1433
|
-
if (seen.has(m.spec)) continue;
|
|
1434
|
-
seen.add(m.spec);
|
|
1435
|
-
try {
|
|
1436
|
-
const result = await client.session.prompt(sessionId, {
|
|
1437
|
-
parts: [{ type: "text", text: "Say OK" }],
|
|
1438
|
-
model: m.parsed,
|
|
1439
|
-
});
|
|
1440
|
-
const err = result?.info?.error;
|
|
1441
|
-
if (err) {
|
|
1442
|
-
const msg = err.data?.message || err.name || "unknown error";
|
|
1443
|
-
logger.fail(`${m.label} model "${m.spec}" rejected by provider: ${msg}`);
|
|
1444
|
-
throw new Error(`${m.label} model unavailable: ${msg}`);
|
|
1445
|
-
}
|
|
1446
|
-
logger.ok(`${m.label} model verified: ${m.spec}`);
|
|
1447
|
-
} catch (probeErr) {
|
|
1448
|
-
if (probeErr.message.includes("unavailable") || probeErr.message.includes("rejected")) {
|
|
1449
|
-
throw probeErr;
|
|
1450
|
-
}
|
|
1451
|
-
logger.warn(`${m.label} model probe inconclusive: ${probeErr.message}`);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
/**
|
|
1457
|
-
* Try to pick a bead and return a planner prompt for it.
|
|
1458
|
-
* Returns null if no beads available.
|
|
1459
|
-
*/
|
|
1460
|
-
function pickBeadForPlanner(logger) {
|
|
1461
|
-
const inProgress = getInProgressBeads();
|
|
1462
|
-
if (inProgress.length > 0) {
|
|
1463
|
-
const beadId = extractBeadId(inProgress[0]);
|
|
1464
|
-
if (beadId) {
|
|
1465
|
-
logger.info(`Resuming: ${beadId}`);
|
|
1466
|
-
return buildPlannerBeadPrompt(beadId);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
const ready = getReadyBeads();
|
|
1471
|
-
if (ready.length === 0) return null;
|
|
1472
|
-
|
|
1473
|
-
const beadId = extractBeadId(ready[0]);
|
|
1474
|
-
if (!beadId) return null;
|
|
1475
|
-
|
|
1476
|
-
run(`bd update ${beadId} --status=in_progress`);
|
|
1477
|
-
logger.info(`Picked: ${beadId}`);
|
|
1478
|
-
return buildPlannerBeadPrompt(beadId);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
function extractBeadId(bead) {
|
|
1482
|
-
return bead.id || bead.raw?.match(/([\w-]+)/)?.[1] || null;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function extractMessageText(msg) {
|
|
1486
|
-
if (!msg) return "";
|
|
1487
|
-
if (typeof msg === "string") return msg;
|
|
1488
|
-
if (msg.content) {
|
|
1489
|
-
if (typeof msg.content === "string") return msg.content;
|
|
1490
|
-
if (Array.isArray(msg.content)) {
|
|
1491
|
-
return msg.content
|
|
1492
|
-
.filter((p) => p.type === "text")
|
|
1493
|
-
.map((p) => p.text || "")
|
|
1494
|
-
.join("\n");
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
if (msg.parts) {
|
|
1498
|
-
return msg.parts
|
|
1499
|
-
.filter((p) => p.type === "text")
|
|
1500
|
-
.map((p) => p.text || "")
|
|
1501
|
-
.join("\n");
|
|
1502
|
-
}
|
|
1503
|
-
return "";
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
async function waitForInput(inputQueue) {
|
|
1507
|
-
while (!inputQueue.hasMessages() && !inputQueue.closed) {
|
|
1508
|
-
await sleep(500);
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
/**
|
|
1513
|
-
* Ask the user whether to pick from beads or discuss a plan (headless mode).
|
|
1514
|
-
* Shows current beads state and waits for the user to choose.
|
|
1515
|
-
*
|
|
1516
|
-
* @returns {"beads"|"discuss"|"quit"}
|
|
1517
|
-
*/
|
|
1518
|
-
async function askStartModeHeadless(inputQueue) {
|
|
1519
|
-
const inProgress = getInProgressBeads();
|
|
1520
|
-
const ready = getReadyBeads();
|
|
1521
|
-
const hasBeads = inProgress.length > 0 || ready.length > 0;
|
|
1522
|
-
|
|
1523
|
-
console.log(`\n${color.bold("-- What would you like to do?")} ${color.bold("--")}`);
|
|
1524
|
-
if (hasBeads) {
|
|
1525
|
-
if (inProgress.length > 0) {
|
|
1526
|
-
console.log(color.dim(` In-progress beads: ${inProgress.length}`));
|
|
1527
|
-
}
|
|
1528
|
-
if (ready.length > 0) {
|
|
1529
|
-
console.log(color.dim(` Ready beads: ${ready.length}`));
|
|
1530
|
-
}
|
|
1531
|
-
} else {
|
|
1532
|
-
console.log(color.dim(" No beads available."));
|
|
1533
|
-
}
|
|
1534
|
-
console.log("");
|
|
1535
|
-
if (hasBeads) {
|
|
1536
|
-
console.log(` ${color.bold("1")} Pick from beads (auto-select a task)`);
|
|
1537
|
-
}
|
|
1538
|
-
console.log(` ${color.bold("2")} Discuss with Planner (plan before execution)`);
|
|
1539
|
-
console.log(color.dim("\n Type 1 or 2, or /quit to exit.\n"));
|
|
1540
|
-
|
|
1541
|
-
while (true) {
|
|
1542
|
-
await waitForInput(inputQueue);
|
|
1543
|
-
const items = inputQueue.drain();
|
|
1544
|
-
for (const item of items) {
|
|
1545
|
-
if (item.type === "quit") return "quit";
|
|
1546
|
-
if (item.type === "message") {
|
|
1547
|
-
const t = item.text.trim();
|
|
1548
|
-
if (t === "1" && hasBeads) return "beads";
|
|
1549
|
-
if (t === "2") return "discuss";
|
|
1550
|
-
console.log(color.dim(` Please type ${hasBeads ? "1 or 2" : "2"}, or /quit to exit.`));
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
// ── Goal discussion (headless mode) ─────────────────────────────────────────
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Interactive goal discussion in headless mode.
|
|
1560
|
-
* Planner suggests goals, user discusses, user types /go to proceed.
|
|
1561
|
-
*
|
|
1562
|
-
* @returns {boolean} true if /go was received and discussion completed,
|
|
1563
|
-
* false if user quit.
|
|
1564
|
-
*/
|
|
1565
|
-
async function goalDiscussionHeadless(client, sessionId, plannerModel, eventDisplay, inputQueue) {
|
|
1566
|
-
console.log(
|
|
1567
|
-
`\n${color.bold("-- Goal Discussion")} ${color.dim("(type /go when ready to start execution)")} ${color.bold("--")}`,
|
|
1568
|
-
);
|
|
1569
|
-
console.log(color.dim(" Discuss with the Planner what to work on. Type /go to begin.\n"));
|
|
1570
|
-
|
|
1571
|
-
// Send discovery prompt to planner
|
|
1572
|
-
const discoveryPrompt = buildPlannerDiscoveryPrompt();
|
|
1573
|
-
log.info("Sending discovery prompt to Planner...");
|
|
1574
|
-
eventDisplay.resetTurn("planner");
|
|
1575
|
-
const discoveryResult = await executeTurnHeadless(
|
|
1576
|
-
client, sessionId, discoveryPrompt, plannerModel,
|
|
1577
|
-
eventDisplay, inputQueue,
|
|
1578
|
-
);
|
|
1579
|
-
console.log("");
|
|
1580
|
-
|
|
1581
|
-
if (discoveryResult.quit) return false;
|
|
1582
|
-
if (discoveryResult.aborted) {
|
|
1583
|
-
log.warn("Discovery prompt aborted. Retrying...");
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
// Discussion loop: user talks to planner until /go
|
|
1587
|
-
while (true) {
|
|
1588
|
-
// Wait for user input
|
|
1589
|
-
console.log(color.dim("\n [waiting for your input... type /go to start, /quit to exit]\n"));
|
|
1590
|
-
await waitForInput(inputQueue);
|
|
1591
|
-
|
|
1592
|
-
const items = inputQueue.drain();
|
|
1593
|
-
let userText = null;
|
|
1594
|
-
let goReceived = false;
|
|
1595
|
-
let quitReceived = false;
|
|
1596
|
-
|
|
1597
|
-
for (const item of items) {
|
|
1598
|
-
if (item.type === "quit") {
|
|
1599
|
-
quitReceived = true;
|
|
1600
|
-
break;
|
|
1601
|
-
}
|
|
1602
|
-
if (item.type === "message") {
|
|
1603
|
-
// Check if the user typed /go
|
|
1604
|
-
if (item.text.toLowerCase().trim() === "/go") {
|
|
1605
|
-
goReceived = true;
|
|
1606
|
-
break;
|
|
1607
|
-
}
|
|
1608
|
-
userText = item.text;
|
|
1609
|
-
}
|
|
1610
|
-
if (item.type === "abort") {
|
|
1611
|
-
// Ignore /abort during discussion
|
|
1612
|
-
}
|
|
1613
|
-
if (item.type === "status") {
|
|
1614
|
-
showBeadStatus();
|
|
1615
|
-
}
|
|
1616
|
-
if (item.type === "skip") {
|
|
1617
|
-
// Ignore /skip during discussion
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
if (quitReceived) return false;
|
|
1622
|
-
|
|
1623
|
-
if (goReceived) {
|
|
1624
|
-
// Send finalize prompt to planner
|
|
1625
|
-
console.log(
|
|
1626
|
-
`\n${color.bold("-- Finalizing Goal")} ${color.bold("--------------------")}`,
|
|
1627
|
-
);
|
|
1628
|
-
log.info("Sending finalize prompt to Planner...");
|
|
1629
|
-
eventDisplay.resetTurn("planner");
|
|
1630
|
-
const finalizeResult = await executeTurnHeadless(
|
|
1631
|
-
client, sessionId, buildPlannerFinalizeGoalPrompt(), plannerModel,
|
|
1632
|
-
eventDisplay, inputQueue,
|
|
1633
|
-
);
|
|
1634
|
-
console.log("");
|
|
1635
|
-
|
|
1636
|
-
if (finalizeResult.quit) return false;
|
|
1637
|
-
return true;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
if (userText) {
|
|
1641
|
-
// Send user's message to planner for continued discussion
|
|
1642
|
-
const discussPrompt = `## Role: Planner (Goal Discussion)
|
|
1643
|
-
|
|
1644
|
-
The user says:
|
|
1645
|
-
|
|
1646
|
-
> ${userText}
|
|
1647
|
-
|
|
1648
|
-
Continue the goal discussion. Help the user refine what to work on.
|
|
1649
|
-
When they're ready, remind them to type \`/go\` to begin execution.`;
|
|
1650
|
-
|
|
1651
|
-
eventDisplay.resetTurn("planner");
|
|
1652
|
-
const discussResult = await executeTurnHeadless(
|
|
1653
|
-
client, sessionId, discussPrompt, plannerModel,
|
|
1654
|
-
eventDisplay, inputQueue,
|
|
1655
|
-
);
|
|
1656
|
-
console.log("");
|
|
1657
|
-
|
|
1658
|
-
if (discussResult.quit) return false;
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// ── Goal discussion (daemon mode) ───────────────────────────────────────────
|
|
1664
|
-
|
|
1665
|
-
/**
|
|
1666
|
-
* Goal discussion in daemon+TUI mode.
|
|
1667
|
-
* Sends a discovery prompt to planner, then waits for the user to
|
|
1668
|
-
* type /go in the TUI. While waiting, the user can freely discuss
|
|
1669
|
-
* with the planner via the TUI — the daemon stays paused.
|
|
1670
|
-
*
|
|
1671
|
-
* @returns {boolean} true if /go was received, false if daemon should exit.
|
|
1672
|
-
*/
|
|
1673
|
-
async function goalDiscussionDaemon(client, sessionId, plannerModel, monitor, dlog) {
|
|
1674
|
-
dlog.info("Starting goal discussion (no goal provided)");
|
|
1675
|
-
|
|
1676
|
-
// Send discovery prompt
|
|
1677
|
-
const discoveryPrompt = buildPlannerDiscoveryPrompt();
|
|
1678
|
-
dlog.info("Sending discovery prompt to Planner...");
|
|
1679
|
-
const discoveryResult = await executeTurnDaemon(
|
|
1680
|
-
client, sessionId, discoveryPrompt, plannerModel, monitor, dlog,
|
|
1681
|
-
);
|
|
1682
|
-
|
|
1683
|
-
if (discoveryResult.aborted) {
|
|
1684
|
-
dlog.warn("Discovery prompt aborted");
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Now wait for user to type /go in the TUI.
|
|
1688
|
-
// The monitor detects user messages via SSE. We listen for /go specifically.
|
|
1689
|
-
dlog.info("Waiting for user to type /go in TUI...");
|
|
1690
|
-
|
|
1691
|
-
return new Promise((resolve) => {
|
|
1692
|
-
let resolved = false;
|
|
1693
|
-
|
|
1694
|
-
// Save previous onUserMessage handler
|
|
1695
|
-
const prevHandler = monitor.onUserMessage;
|
|
1696
|
-
|
|
1697
|
-
monitor.onUserMessage = (text) => {
|
|
1698
|
-
if (resolved) return;
|
|
1699
|
-
const trimmed = text.trim().toLowerCase();
|
|
1700
|
-
|
|
1701
|
-
if (trimmed === "/go") {
|
|
1702
|
-
dlog.info("User typed /go — finalizing goal");
|
|
1703
|
-
resolved = true;
|
|
1704
|
-
|
|
1705
|
-
// Restore previous handler
|
|
1706
|
-
monitor.onUserMessage = prevHandler || null;
|
|
1707
|
-
|
|
1708
|
-
// Send finalize prompt
|
|
1709
|
-
(async () => {
|
|
1710
|
-
const finalizePrompt = buildPlannerFinalizeGoalPrompt();
|
|
1711
|
-
monitor.registerPrompt(finalizePrompt);
|
|
1712
|
-
dlog.info("Sending finalize prompt to Planner...");
|
|
1713
|
-
const finalizeResult = await executeTurnDaemon(
|
|
1714
|
-
client, sessionId, finalizePrompt, plannerModel, monitor, dlog,
|
|
1715
|
-
);
|
|
1716
|
-
if (finalizeResult.aborted) {
|
|
1717
|
-
dlog.warn("Finalize prompt aborted");
|
|
1718
|
-
}
|
|
1719
|
-
resolve(true);
|
|
1720
|
-
})();
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
// Any other user message: the user is discussing with the planner via
|
|
1725
|
-
// TUI. The TUI + opencode handle this automatically (user types →
|
|
1726
|
-
// model responds). The daemon just needs to wait for those turns to
|
|
1727
|
-
// complete before checking for /go again.
|
|
1728
|
-
dlog.info(`User discussing in TUI: "${text.slice(0, 60)}..."`);
|
|
1729
|
-
// Wait for the model's response to finish
|
|
1730
|
-
monitor.waitForTurnEnd().then(() => {
|
|
1731
|
-
dlog.info("Planner response to user complete, still waiting for /go");
|
|
1732
|
-
});
|
|
1733
|
-
};
|
|
1734
|
-
});
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// ── Main entry point ────────────────────────────────────────────────────────
|
|
1738
|
-
|
|
1739
|
-
export async function auto(argv) {
|
|
1740
|
-
const opts = parseArgs(argv);
|
|
1741
|
-
|
|
1742
|
-
if (!has("opencode")) {
|
|
1743
|
-
log.fail(
|
|
1744
|
-
"opencode is not installed. Run: curl -fsSL https://opencode.ai/install | bash",
|
|
1745
|
-
);
|
|
1746
|
-
process.exit(1);
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
// ── Path 1: Internal daemon process (forked by main process) ──
|
|
1750
|
-
if (opts._daemon) {
|
|
1751
|
-
return runDaemonProcess(opts);
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
// ── Path 2: Headless mode (original CLI) ──
|
|
1755
|
-
if (opts.headless) {
|
|
1756
|
-
return runHeadlessMode(opts);
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// ── Path 3: Default — daemon + TUI ──
|
|
1760
|
-
return runDaemonTuiMode(opts);
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
// ── Path 1: Daemon process ──────────────────────────────────────────────────
|
|
1764
|
-
|
|
1765
|
-
async function runDaemonProcess(opts) {
|
|
1766
|
-
const dlog = createFileLogger(LOG_FILE);
|
|
1767
|
-
dlog.info(`Daemon starting — goal: ${opts.goal || "(auto-pick)"}`);
|
|
1768
|
-
dlog.info(`Planner: ${opts.planner}, Executor: ${opts.executor}`);
|
|
1769
|
-
|
|
1770
|
-
const url = opts._daemonUrl;
|
|
1771
|
-
if (!url) {
|
|
1772
|
-
dlog.fail("No server URL provided (--_daemon-url)");
|
|
1773
|
-
process.exit(1);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// Connect to the opencode serve instance
|
|
1777
|
-
const client = createClient(url);
|
|
1778
|
-
try {
|
|
1779
|
-
const health = await client.health();
|
|
1780
|
-
if (!health?.healthy) throw new Error("not healthy");
|
|
1781
|
-
dlog.ok(`Connected to server at ${url}`);
|
|
1782
|
-
} catch (err) {
|
|
1783
|
-
dlog.fail(`Cannot connect to server at ${url}: ${err.message}`);
|
|
1784
|
-
process.exit(1);
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Create or reuse session
|
|
1788
|
-
let sessionId = opts._daemonSessionId;
|
|
1789
|
-
if (!sessionId) {
|
|
1790
|
-
dlog.info("Creating session...");
|
|
1791
|
-
const session = await client.session.create({ title: "mneme auto" });
|
|
1792
|
-
sessionId = session.id;
|
|
1793
|
-
dlog.ok(`Session: ${sessionId}`);
|
|
1794
|
-
|
|
1795
|
-
// Validate models in new session
|
|
1796
|
-
dlog.info("Validating models (API probe)...");
|
|
1797
|
-
try {
|
|
1798
|
-
await validateModels(client, sessionId, opts, dlog);
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
dlog.fail(`Model validation failed: ${err.message}`);
|
|
1801
|
-
process.exit(1);
|
|
1802
|
-
}
|
|
1803
|
-
} else {
|
|
1804
|
-
dlog.ok(`Reusing session: ${sessionId}`);
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
// Handle signals for clean shutdown
|
|
1808
|
-
let shuttingDown = false;
|
|
1809
|
-
const shutdown = (signal) => {
|
|
1810
|
-
if (shuttingDown) return;
|
|
1811
|
-
shuttingDown = true;
|
|
1812
|
-
dlog.info(`Received ${signal}, shutting down daemon...`);
|
|
1813
|
-
process.exit(0);
|
|
1814
|
-
};
|
|
1815
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1816
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1817
|
-
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
1818
|
-
|
|
1819
|
-
// Monitor parent process (the TUI). When it exits, we should too.
|
|
1820
|
-
// In Node.js, when the parent exits, the daemon gets SIGHUP if not
|
|
1821
|
-
// fully detached. We also poll as a fallback.
|
|
1822
|
-
if (process.ppid) {
|
|
1823
|
-
const parentPid = process.ppid;
|
|
1824
|
-
const parentCheckId = setInterval(() => {
|
|
1825
|
-
try {
|
|
1826
|
-
// Signal 0 checks if process exists
|
|
1827
|
-
process.kill(parentPid, 0);
|
|
1828
|
-
} catch {
|
|
1829
|
-
dlog.info("Parent process exited, daemon shutting down.");
|
|
1830
|
-
clearInterval(parentCheckId);
|
|
1831
|
-
process.exit(0);
|
|
1832
|
-
}
|
|
1833
|
-
}, 5000);
|
|
1834
|
-
parentCheckId.unref?.();
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
// Run the daemon supervisor loop
|
|
1838
|
-
try {
|
|
1839
|
-
await supervisorLoopDaemon(client, sessionId, opts, dlog);
|
|
1840
|
-
} catch (err) {
|
|
1841
|
-
dlog.fail(`Daemon supervisor error: ${err.message}`);
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
dlog.ok("Daemon finished.");
|
|
1845
|
-
process.exit(0);
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
// ── Path 2: Headless mode (original CLI) ────────────────────────────────────
|
|
1849
|
-
|
|
1850
|
-
async function runHeadlessMode(opts) {
|
|
1851
|
-
console.log(
|
|
1852
|
-
`\n${color.bold("mneme auto")} — dual-agent autonomous supervisor (headless)\n`,
|
|
1853
|
-
);
|
|
1854
|
-
console.log(` ${color.bold("Planner:")} ${opts.planner}`);
|
|
1855
|
-
console.log(` ${color.bold("Executor:")} ${opts.executor}\n`);
|
|
1856
|
-
console.log(color.dim("Commands while running:"));
|
|
1857
|
-
console.log(color.dim(" Type any message -> inject feedback to planner"));
|
|
1858
|
-
console.log(color.dim(" /status -> show bead status"));
|
|
1859
|
-
console.log(color.dim(" /skip -> skip current bead"));
|
|
1860
|
-
console.log(color.dim(" /abort -> abort current turn"));
|
|
1861
|
-
console.log(color.dim(" /quit -> stop and exit\n"));
|
|
1862
|
-
|
|
1863
|
-
let serverCtx;
|
|
1864
|
-
try {
|
|
1865
|
-
if (opts.attach) {
|
|
1866
|
-
serverCtx = await attachOpencodeServer(opts.attach);
|
|
1867
|
-
log.ok(`Attached to ${serverCtx.url} (v${serverCtx.version})`);
|
|
1868
|
-
} else {
|
|
1869
|
-
serverCtx = await startOpencodeServer({ port: opts.port });
|
|
1870
|
-
if (serverCtx.alreadyRunning) {
|
|
1871
|
-
log.ok(`Server already running at ${serverCtx.url} (v${serverCtx.version})`);
|
|
1872
|
-
} else {
|
|
1873
|
-
log.ok(`Server started at ${serverCtx.url} (v${serverCtx.version})`);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
} catch (err) {
|
|
1877
|
-
log.fail(err.message);
|
|
1878
|
-
process.exit(1);
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
const inputQueue = createInputQueue();
|
|
1882
|
-
inputQueue.start();
|
|
1883
|
-
|
|
1884
|
-
try {
|
|
1885
|
-
await supervisorLoopHeadless(serverCtx.client, opts, inputQueue);
|
|
1886
|
-
} catch (err) {
|
|
1887
|
-
log.fail(`Supervisor error: ${err.message}`);
|
|
1888
|
-
} finally {
|
|
1889
|
-
inputQueue.stop();
|
|
1890
|
-
if (serverCtx.serverProcess) {
|
|
1891
|
-
log.info("Shutting down server...");
|
|
1892
|
-
serverCtx.serverProcess.kill("SIGTERM");
|
|
1893
|
-
}
|
|
1894
|
-
log.ok("mneme auto finished.");
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
// ── Path 3: Daemon + TUI mode (default) ────────────────────────────────────
|
|
1899
|
-
|
|
1900
|
-
async function runDaemonTuiMode(opts) {
|
|
1901
|
-
console.log(
|
|
1902
|
-
`\n${color.bold("mneme auto")} — dual-agent autonomous supervisor\n`,
|
|
1903
|
-
);
|
|
1904
|
-
console.log(` ${color.bold("Planner:")} ${opts.planner}`);
|
|
1905
|
-
console.log(` ${color.bold("Executor:")} ${opts.executor}`);
|
|
1906
|
-
console.log(` ${color.bold("Mode:")} daemon + TUI\n`);
|
|
1907
|
-
|
|
1908
|
-
// Step 1: Start opencode serve (if not running)
|
|
1909
|
-
let serverUrl;
|
|
1910
|
-
let weStartedServer = false;
|
|
1911
|
-
|
|
1912
|
-
if (opts.attach) {
|
|
1913
|
-
// User provided a URL
|
|
1914
|
-
serverUrl = opts.attach;
|
|
1915
|
-
log.info(`Using provided server: ${serverUrl}`);
|
|
1916
|
-
try {
|
|
1917
|
-
const client = createClient(serverUrl);
|
|
1918
|
-
const health = await client.health();
|
|
1919
|
-
if (!health?.healthy) throw new Error("not healthy");
|
|
1920
|
-
log.ok(`Server healthy (v${health.version})`);
|
|
1921
|
-
} catch (err) {
|
|
1922
|
-
log.fail(`Cannot connect to ${serverUrl}: ${err.message}`);
|
|
1923
|
-
process.exit(1);
|
|
1924
|
-
}
|
|
1925
|
-
} else {
|
|
1926
|
-
serverUrl = `http://127.0.0.1:${opts.port}`;
|
|
1927
|
-
// Check if already running
|
|
1928
|
-
try {
|
|
1929
|
-
const client = createClient(serverUrl);
|
|
1930
|
-
const health = await client.health();
|
|
1931
|
-
if (health?.healthy) {
|
|
1932
|
-
log.ok(`Server already running at ${serverUrl} (v${health.version})`);
|
|
1933
|
-
} else {
|
|
1934
|
-
throw new Error("not healthy");
|
|
1935
|
-
}
|
|
1936
|
-
} catch {
|
|
1937
|
-
// Need to start it
|
|
1938
|
-
log.info(`Starting opencode serve on port ${opts.port}...`);
|
|
1939
|
-
try {
|
|
1940
|
-
const ctx = await startOpencodeServer({ port: opts.port, detached: true });
|
|
1941
|
-
weStartedServer = !ctx.alreadyRunning;
|
|
1942
|
-
log.ok(`Server started at ${ctx.url} (v${ctx.version})`);
|
|
1943
|
-
} catch (err) {
|
|
1944
|
-
log.fail(`Failed to start server: ${err.message}`);
|
|
1945
|
-
process.exit(1);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
// Step 2: Create session and validate models before forking daemon
|
|
1951
|
-
log.info("Creating session and validating models...");
|
|
1952
|
-
const client = createClient(serverUrl);
|
|
1953
|
-
let sessionId;
|
|
1954
|
-
try {
|
|
1955
|
-
const session = await client.session.create({ title: "mneme auto" });
|
|
1956
|
-
sessionId = session.id;
|
|
1957
|
-
log.ok(`Session: ${sessionId}`);
|
|
1958
|
-
await validateModels(client, sessionId, opts, log);
|
|
1959
|
-
} catch (err) {
|
|
1960
|
-
log.fail(`Setup failed: ${err.message}`);
|
|
1961
|
-
if (weStartedServer) {
|
|
1962
|
-
log.info("Stopping server we started...");
|
|
1963
|
-
run(`kill $(ps aux | grep 'opencode.*serve.*--port.*${opts.port}' | grep -v grep | awk '{print $2}') 2>/dev/null`);
|
|
1964
|
-
}
|
|
1965
|
-
process.exit(1);
|
|
1966
|
-
}
|
|
1967
|
-
|
|
1968
|
-
// Step 3: Fork daemon process
|
|
1969
|
-
log.info("Forking daemon process...");
|
|
1970
|
-
|
|
1971
|
-
// Build daemon argv
|
|
1972
|
-
const daemonArgs = [
|
|
1973
|
-
"auto",
|
|
1974
|
-
"--_daemon",
|
|
1975
|
-
"--_daemon-url", serverUrl,
|
|
1976
|
-
"--_daemon-session", sessionId,
|
|
1977
|
-
"--planner", opts.planner,
|
|
1978
|
-
"--executor", opts.executor,
|
|
1979
|
-
"--max-cycles", String(opts.maxCycles),
|
|
1980
|
-
];
|
|
1981
|
-
if (opts.goal) {
|
|
1982
|
-
daemonArgs.push(opts.goal);
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// Fork using the mneme CLI entry point
|
|
1986
|
-
const mnemeEntry = join(
|
|
1987
|
-
new URL(".", import.meta.url).pathname,
|
|
1988
|
-
"..", "..", "bin", "mneme.mjs",
|
|
1989
|
-
);
|
|
1990
|
-
|
|
1991
|
-
const daemon = fork(mnemeEntry, daemonArgs, {
|
|
1992
|
-
detached: true,
|
|
1993
|
-
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
1994
|
-
});
|
|
1995
|
-
|
|
1996
|
-
daemon.unref();
|
|
1997
|
-
// Disconnect IPC so parent can exit cleanly
|
|
1998
|
-
daemon.disconnect();
|
|
1999
|
-
|
|
2000
|
-
log.ok(`Daemon forked (PID: ${daemon.pid})`);
|
|
2001
|
-
log.info(`Log file: ${LOG_FILE}`);
|
|
2002
|
-
|
|
2003
|
-
// Small delay to let daemon connect before we launch TUI
|
|
2004
|
-
await sleep(1000);
|
|
2005
|
-
|
|
2006
|
-
// Step 4: exec opencode attach (replaces this process)
|
|
2007
|
-
log.info(`Launching TUI: opencode attach ${serverUrl}`);
|
|
2008
|
-
console.log(color.dim(" Type directly in the TUI to intervene. The daemon pauses automatically.\n"));
|
|
2009
|
-
|
|
2010
|
-
try {
|
|
2011
|
-
execSync(`opencode attach ${serverUrl}`, {
|
|
2012
|
-
stdio: "inherit",
|
|
2013
|
-
// This blocks until the TUI exits
|
|
2014
|
-
});
|
|
2015
|
-
} catch {
|
|
2016
|
-
// TUI exited (normal or error)
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// TUI exited — kill daemon
|
|
2020
|
-
log.info("TUI exited.");
|
|
2021
|
-
try {
|
|
2022
|
-
process.kill(daemon.pid, "SIGTERM");
|
|
2023
|
-
log.info(`Sent SIGTERM to daemon (PID: ${daemon.pid})`);
|
|
2024
|
-
} catch {
|
|
2025
|
-
// Daemon may have already exited
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
// If we started the server and --attach wasn't used, optionally stop it
|
|
2029
|
-
// (leave it running — user can `mneme down` manually)
|
|
2030
|
-
log.ok("mneme auto finished.");
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
2034
|
-
|
|
2035
|
-
function sleep(ms) {
|
|
2036
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2037
190
|
}
|