@lawrence369/loop-cli 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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/agent/activity.d.ts +64 -0
- package/dist/agent/activity.js +265 -0
- package/dist/agent/launcher.d.ts +42 -0
- package/dist/agent/launcher.js +243 -0
- package/dist/agent/pty-session.d.ts +113 -0
- package/dist/agent/pty-session.js +490 -0
- package/dist/agent/ready-detector.d.ts +46 -0
- package/dist/agent/ready-detector.js +86 -0
- package/dist/agent/wrapper.d.ts +18 -0
- package/dist/agent/wrapper.js +110 -0
- package/dist/bin/lclaude.d.ts +3 -0
- package/dist/bin/lclaude.js +7 -0
- package/dist/bin/lcodex.d.ts +3 -0
- package/dist/bin/lcodex.js +7 -0
- package/dist/bin/lgemini.d.ts +3 -0
- package/dist/bin/lgemini.js +7 -0
- package/dist/bus/daemon.d.ts +56 -0
- package/dist/bus/daemon.js +135 -0
- package/dist/bus/event-bus.d.ts +105 -0
- package/dist/bus/event-bus.js +157 -0
- package/dist/bus/message.d.ts +48 -0
- package/dist/bus/message.js +129 -0
- package/dist/bus/queue.d.ts +50 -0
- package/dist/bus/queue.js +100 -0
- package/dist/bus/store.d.ts +88 -0
- package/dist/bus/store.js +212 -0
- package/dist/bus/subscriber.d.ts +76 -0
- package/dist/bus/subscriber.js +187 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +72 -0
- package/dist/config/schema.d.ts +18 -0
- package/dist/config/schema.js +58 -0
- package/dist/core/conversation.d.ts +34 -0
- package/dist/core/conversation.js +289 -0
- package/dist/core/engine.d.ts +40 -0
- package/dist/core/engine.js +288 -0
- package/dist/core/loop.d.ts +33 -0
- package/dist/core/loop.js +209 -0
- package/dist/core/protocol.d.ts +60 -0
- package/dist/core/protocol.js +162 -0
- package/dist/core/scoring.d.ts +34 -0
- package/dist/core/scoring.js +69 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +408 -0
- package/dist/orchestrator/daemon.d.ts +74 -0
- package/dist/orchestrator/daemon.js +294 -0
- package/dist/orchestrator/group.d.ts +73 -0
- package/dist/orchestrator/group.js +166 -0
- package/dist/orchestrator/ipc-server.d.ts +60 -0
- package/dist/orchestrator/ipc-server.js +166 -0
- package/dist/orchestrator/scheduler.d.ts +32 -0
- package/dist/orchestrator/scheduler.js +95 -0
- package/dist/plan/context.d.ts +8 -0
- package/dist/plan/context.js +42 -0
- package/dist/plan/decisions.d.ts +18 -0
- package/dist/plan/decisions.js +143 -0
- package/dist/plan/shared-plan.d.ts +33 -0
- package/dist/plan/shared-plan.js +211 -0
- package/dist/skills/executor.d.ts +7 -0
- package/dist/skills/executor.js +11 -0
- package/dist/skills/loader.d.ts +16 -0
- package/dist/skills/loader.js +80 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +54 -0
- package/dist/terminal/adapter.d.ts +61 -0
- package/dist/terminal/adapter.js +42 -0
- package/dist/terminal/detect.d.ts +30 -0
- package/dist/terminal/detect.js +77 -0
- package/dist/terminal/iterm2-adapter.d.ts +19 -0
- package/dist/terminal/iterm2-adapter.js +120 -0
- package/dist/terminal/pty-adapter.d.ts +18 -0
- package/dist/terminal/pty-adapter.js +84 -0
- package/dist/terminal/terminal-adapter.d.ts +17 -0
- package/dist/terminal/terminal-adapter.js +94 -0
- package/dist/terminal/tmux-adapter.d.ts +18 -0
- package/dist/terminal/tmux-adapter.js +127 -0
- package/dist/ui/banner.d.ts +3 -0
- package/dist/ui/banner.js +145 -0
- package/dist/ui/colors.d.ts +41 -0
- package/dist/ui/colors.js +65 -0
- package/dist/ui/dashboard.d.ts +32 -0
- package/dist/ui/dashboard.js +138 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +96 -0
- package/dist/ui/interactive.d.ts +13 -0
- package/dist/ui/interactive.js +230 -0
- package/dist/ui/renderer.d.ts +33 -0
- package/dist/ui/renderer.js +106 -0
- package/dist/utils/ansi.d.ts +11 -0
- package/dist/utils/ansi.js +16 -0
- package/dist/utils/fs.d.ts +34 -0
- package/dist/utils/fs.js +115 -0
- package/dist/utils/lock.d.ts +12 -0
- package/dist/utils/lock.js +116 -0
- package/dist/utils/process.d.ts +31 -0
- package/dist/utils/process.js +111 -0
- package/dist/utils/pty-filter.d.ts +31 -0
- package/dist/utils/pty-filter.js +187 -0
- package/package.json +71 -0
- package/skills/loop/SKILL.md +19 -0
- package/skills/plan/SKILL.md +9 -0
- package/skills/review/SKILL.md +14 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine abstraction — unified interface for Claude, Gemini, and Codex CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Ported from iterloop's engine.ts with adaptations for loop-cli:
|
|
5
|
+
* - Brand `color` property on each engine
|
|
6
|
+
* - Timeout parameter on RunOptions
|
|
7
|
+
* - ESM imports with .js extensions
|
|
8
|
+
*/
|
|
9
|
+
import { type PtySession, type PtySessionOptions } from "../agent/pty-session.js";
|
|
10
|
+
export type { PtySession, PtySessionOptions };
|
|
11
|
+
export type EngineName = "claude" | "gemini" | "codex";
|
|
12
|
+
export declare const ENGINE_NAMES: EngineName[];
|
|
13
|
+
export interface RunOptions {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
verbose?: boolean;
|
|
16
|
+
onData?: (chunk: string) => void;
|
|
17
|
+
onStatus?: (status: string) => void;
|
|
18
|
+
passthroughArgs?: string[];
|
|
19
|
+
timeout?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface InteractiveOptions {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
args?: string[];
|
|
24
|
+
cols?: number;
|
|
25
|
+
rows?: number;
|
|
26
|
+
env?: Record<string, string>;
|
|
27
|
+
onData?: (data: string) => void;
|
|
28
|
+
onExit?: (exitCode: number) => void;
|
|
29
|
+
passthroughArgs?: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface Engine {
|
|
32
|
+
name: EngineName;
|
|
33
|
+
label: string;
|
|
34
|
+
color: (s: string) => string;
|
|
35
|
+
checkVersion(): string;
|
|
36
|
+
run(prompt: string, opts: RunOptions): Promise<string>;
|
|
37
|
+
interactive(opts: InteractiveOptions): PtySession;
|
|
38
|
+
}
|
|
39
|
+
export declare function createEngine(name: EngineName): Engine;
|
|
40
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine abstraction — unified interface for Claude, Gemini, and Codex CLIs.
|
|
3
|
+
*
|
|
4
|
+
* Ported from iterloop's engine.ts with adaptations for loop-cli:
|
|
5
|
+
* - Brand `color` property on each engine
|
|
6
|
+
* - Timeout parameter on RunOptions
|
|
7
|
+
* - ESM imports with .js extensions
|
|
8
|
+
*/
|
|
9
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
10
|
+
import { stripAnsi } from "../utils/ansi.js";
|
|
11
|
+
import { createPtySession } from "../agent/pty-session.js";
|
|
12
|
+
import { claude as claudeColor, gemini as geminiColor, codex as codexColor } from "../ui/colors.js";
|
|
13
|
+
const DEFAULT_TIMEOUT = 3_600_000; // 1 hour
|
|
14
|
+
export const ENGINE_NAMES = ["claude", "gemini", "codex"];
|
|
15
|
+
// ── Claude ───────────────────────────────────────────
|
|
16
|
+
function createClaude() {
|
|
17
|
+
return {
|
|
18
|
+
name: "claude",
|
|
19
|
+
label: "Claude",
|
|
20
|
+
color: claudeColor,
|
|
21
|
+
checkVersion() {
|
|
22
|
+
return execFileSync("claude", ["--version"], { encoding: "utf-8" }).trim();
|
|
23
|
+
},
|
|
24
|
+
run(prompt, opts) {
|
|
25
|
+
// Use stream-json when streaming callbacks are provided (executor mode)
|
|
26
|
+
if (opts.onData || opts.onStatus) {
|
|
27
|
+
return spawnClaudeStream(prompt, opts);
|
|
28
|
+
}
|
|
29
|
+
return spawnEngine("claude", ["-p", prompt, ...(opts.passthroughArgs ?? [])], opts);
|
|
30
|
+
},
|
|
31
|
+
interactive(opts) {
|
|
32
|
+
return createPtySession({
|
|
33
|
+
cmd: "claude",
|
|
34
|
+
args: [...(opts.passthroughArgs ?? [])],
|
|
35
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
36
|
+
env: opts.env,
|
|
37
|
+
engineName: "claude",
|
|
38
|
+
onData: opts.onData,
|
|
39
|
+
onExit: opts.onExit,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ── Gemini ───────────────────────────────────────────
|
|
45
|
+
function createGemini() {
|
|
46
|
+
return {
|
|
47
|
+
name: "gemini",
|
|
48
|
+
label: "Gemini",
|
|
49
|
+
color: geminiColor,
|
|
50
|
+
checkVersion() {
|
|
51
|
+
return execFileSync("gemini", ["--version"], { encoding: "utf-8" }).trim();
|
|
52
|
+
},
|
|
53
|
+
run(prompt, opts) {
|
|
54
|
+
return spawnEngine("gemini", ["-p", prompt, ...(opts.passthroughArgs ?? [])], opts);
|
|
55
|
+
},
|
|
56
|
+
interactive(opts) {
|
|
57
|
+
return createPtySession({
|
|
58
|
+
cmd: "gemini",
|
|
59
|
+
args: [...(opts.passthroughArgs ?? [])],
|
|
60
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
61
|
+
env: opts.env,
|
|
62
|
+
engineName: "gemini",
|
|
63
|
+
onData: opts.onData,
|
|
64
|
+
onExit: opts.onExit,
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ── Codex ────────────────────────────────────────────
|
|
70
|
+
function createCodex() {
|
|
71
|
+
return {
|
|
72
|
+
name: "codex",
|
|
73
|
+
label: "Codex",
|
|
74
|
+
color: codexColor,
|
|
75
|
+
checkVersion() {
|
|
76
|
+
return execFileSync("codex", ["--version"], { encoding: "utf-8" }).trim();
|
|
77
|
+
},
|
|
78
|
+
run(prompt, opts) {
|
|
79
|
+
const args = ["exec", "--full-auto", "--skip-git-repo-check"];
|
|
80
|
+
if (opts.cwd) {
|
|
81
|
+
args.push("-C", opts.cwd);
|
|
82
|
+
}
|
|
83
|
+
args.push(...(opts.passthroughArgs ?? []), prompt);
|
|
84
|
+
return spawnEngine("codex", args, opts);
|
|
85
|
+
},
|
|
86
|
+
interactive(opts) {
|
|
87
|
+
return createPtySession({
|
|
88
|
+
cmd: "codex",
|
|
89
|
+
args: [...(opts.passthroughArgs ?? [])],
|
|
90
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
91
|
+
env: opts.env,
|
|
92
|
+
engineName: "codex",
|
|
93
|
+
onData: opts.onData,
|
|
94
|
+
onExit: opts.onExit,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── Factory ──────────────────────────────────────────
|
|
100
|
+
export function createEngine(name) {
|
|
101
|
+
switch (name) {
|
|
102
|
+
case "claude":
|
|
103
|
+
return createClaude();
|
|
104
|
+
case "gemini":
|
|
105
|
+
return createGemini();
|
|
106
|
+
case "codex":
|
|
107
|
+
return createCodex();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ── Shared spawn helper ──────────────────────────────
|
|
111
|
+
function spawnEngine(cmd, args, opts) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const proc = spawn(cmd, args, {
|
|
114
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
115
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
116
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
117
|
+
});
|
|
118
|
+
let stdout = "";
|
|
119
|
+
let stderr = "";
|
|
120
|
+
const timeoutMs = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
proc.kill("SIGTERM");
|
|
123
|
+
reject(new Error(`${cmd} timed out (${Math.round(timeoutMs / 60_000)} minute limit)`));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
proc.stdout.on("data", (chunk) => {
|
|
126
|
+
const text = chunk.toString();
|
|
127
|
+
stdout += text;
|
|
128
|
+
if (opts.onData) {
|
|
129
|
+
opts.onData(text);
|
|
130
|
+
}
|
|
131
|
+
else if (opts.verbose) {
|
|
132
|
+
process.stdout.write(text);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
proc.stderr.on("data", (chunk) => {
|
|
136
|
+
const text = chunk.toString();
|
|
137
|
+
stderr += text;
|
|
138
|
+
if (opts.verbose) {
|
|
139
|
+
process.stderr.write(text);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
proc.on("close", (code) => {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
if (code !== 0) {
|
|
145
|
+
reject(new Error(`${cmd} exited with code ${code}\n${stderr}`));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
resolve(stripAnsi(stdout).trim());
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
proc.on("error", (err) => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
reject(err);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// ── Claude stream-json helpers ───────────────────────
|
|
158
|
+
function summarizeToolInput(name, input) {
|
|
159
|
+
if (input.file_path)
|
|
160
|
+
return `${name} ${input.file_path}`;
|
|
161
|
+
if (input.pattern)
|
|
162
|
+
return `${name} ${input.pattern}`;
|
|
163
|
+
if (input.command) {
|
|
164
|
+
const cmd = String(input.command);
|
|
165
|
+
return `${name} ${cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd}`;
|
|
166
|
+
}
|
|
167
|
+
if (input.query)
|
|
168
|
+
return `${name} ${input.query}`;
|
|
169
|
+
return name;
|
|
170
|
+
}
|
|
171
|
+
function spawnClaudeStream(prompt, opts) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const args = [
|
|
174
|
+
"-p",
|
|
175
|
+
prompt,
|
|
176
|
+
"--output-format",
|
|
177
|
+
"stream-json",
|
|
178
|
+
"--include-partial-messages",
|
|
179
|
+
"--verbose",
|
|
180
|
+
...(opts.passthroughArgs ?? []),
|
|
181
|
+
];
|
|
182
|
+
const proc = spawn("claude", args, {
|
|
183
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
184
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
185
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
186
|
+
});
|
|
187
|
+
let resultText = "";
|
|
188
|
+
let rawStdout = "";
|
|
189
|
+
let stderr = "";
|
|
190
|
+
let lineBuffer = "";
|
|
191
|
+
const timeoutMs = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
proc.kill("SIGTERM");
|
|
194
|
+
reject(new Error(`Claude CLI timed out (${Math.round(timeoutMs / 60_000)} minute limit)`));
|
|
195
|
+
}, timeoutMs);
|
|
196
|
+
proc.stdout.on("data", (chunk) => {
|
|
197
|
+
const text = chunk.toString();
|
|
198
|
+
rawStdout += text;
|
|
199
|
+
lineBuffer += text;
|
|
200
|
+
// Process complete JSON lines
|
|
201
|
+
let nlIdx;
|
|
202
|
+
while ((nlIdx = lineBuffer.indexOf("\n")) !== -1) {
|
|
203
|
+
const line = lineBuffer.slice(0, nlIdx).trim();
|
|
204
|
+
lineBuffer = lineBuffer.slice(nlIdx + 1);
|
|
205
|
+
if (!line)
|
|
206
|
+
continue;
|
|
207
|
+
try {
|
|
208
|
+
const event = JSON.parse(line);
|
|
209
|
+
handleStreamEvent(event, opts);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Non-JSON line — forward as plain text
|
|
213
|
+
opts.onData?.(line + "\n");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
proc.stderr.on("data", (chunk) => {
|
|
218
|
+
const text = chunk.toString();
|
|
219
|
+
stderr += text;
|
|
220
|
+
if (opts.verbose) {
|
|
221
|
+
process.stderr.write(text);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
proc.on("close", (code) => {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
if (code !== 0) {
|
|
227
|
+
reject(new Error(`Claude exited with code ${code}\n${stderr}`));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Use parsed result if available, fall back to raw stdout
|
|
231
|
+
resolve(resultText || stripAnsi(rawStdout).trim());
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
proc.on("error", (err) => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
reject(err);
|
|
237
|
+
});
|
|
238
|
+
function handleStreamEvent(event, runOpts) {
|
|
239
|
+
switch (event.type) {
|
|
240
|
+
case "system":
|
|
241
|
+
runOpts.onStatus?.("session started");
|
|
242
|
+
break;
|
|
243
|
+
// Claude CLI wraps API streaming events inside {"type":"stream_event","event":{...}}
|
|
244
|
+
case "stream_event": {
|
|
245
|
+
const inner = event.event;
|
|
246
|
+
if (!inner)
|
|
247
|
+
break;
|
|
248
|
+
if (inner.type === "content_block_start") {
|
|
249
|
+
const block = inner.content_block;
|
|
250
|
+
if (block?.type === "tool_use" && block.name) {
|
|
251
|
+
runOpts.onStatus?.(block.name);
|
|
252
|
+
}
|
|
253
|
+
else if (block?.type === "thinking") {
|
|
254
|
+
runOpts.onStatus?.("Thinking...");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (inner.type === "content_block_delta") {
|
|
258
|
+
const delta = inner.delta;
|
|
259
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
260
|
+
runOpts.onData?.(delta.text);
|
|
261
|
+
}
|
|
262
|
+
// thinking_delta — keep status as "Thinking..."
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "assistant": {
|
|
267
|
+
// Complete assistant message — extract tool use info for status
|
|
268
|
+
const content = event.message?.content;
|
|
269
|
+
if (Array.isArray(content)) {
|
|
270
|
+
for (const block of content) {
|
|
271
|
+
if (block.type === "tool_use" && block.name) {
|
|
272
|
+
runOpts.onStatus?.(summarizeToolInput(block.name, block.input ?? {}));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "result": {
|
|
279
|
+
if (typeof event.result === "string") {
|
|
280
|
+
resultText = event.result;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main iteration loop — orchestrates executor/reviewer cycles.
|
|
3
|
+
*
|
|
4
|
+
* Ported from iterloop's loop.ts with:
|
|
5
|
+
* - scoring.ts for approval logic (not inline check)
|
|
6
|
+
* - protocol.ts for message creation/parsing
|
|
7
|
+
* - Shared-plan integration (import from plan/shared-plan.ts)
|
|
8
|
+
* - Configurable threshold
|
|
9
|
+
*/
|
|
10
|
+
import type { Engine } from "./engine.js";
|
|
11
|
+
import type { ExecutionMode } from "./conversation.js";
|
|
12
|
+
import { type LoopMessage } from "./protocol.js";
|
|
13
|
+
export interface LoopOptions {
|
|
14
|
+
task: string;
|
|
15
|
+
maxIterations: number;
|
|
16
|
+
executor: Engine;
|
|
17
|
+
reviewer: Engine;
|
|
18
|
+
cwd: string;
|
|
19
|
+
verbose: boolean;
|
|
20
|
+
mode: {
|
|
21
|
+
current: ExecutionMode;
|
|
22
|
+
};
|
|
23
|
+
passthroughArgs?: string[];
|
|
24
|
+
threshold: number;
|
|
25
|
+
}
|
|
26
|
+
export interface LoopResult {
|
|
27
|
+
iterations: number;
|
|
28
|
+
approved: boolean;
|
|
29
|
+
finalOutput: string;
|
|
30
|
+
history: LoopMessage[];
|
|
31
|
+
}
|
|
32
|
+
export declare function runLoop(options: LoopOptions): Promise<LoopResult>;
|
|
33
|
+
//# sourceMappingURL=loop.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main iteration loop — orchestrates executor/reviewer cycles.
|
|
3
|
+
*
|
|
4
|
+
* Ported from iterloop's loop.ts with:
|
|
5
|
+
* - scoring.ts for approval logic (not inline check)
|
|
6
|
+
* - protocol.ts for message creation/parsing
|
|
7
|
+
* - Shared-plan integration (import from plan/shared-plan.ts)
|
|
8
|
+
* - Configurable threshold
|
|
9
|
+
*/
|
|
10
|
+
import { runConversation } from "./conversation.js";
|
|
11
|
+
import { createExecutorMessage, parseReviewerOutput, formatForReviewer, } from "./protocol.js";
|
|
12
|
+
import { evaluateReview } from "./scoring.js";
|
|
13
|
+
import { bold, dim, success, warn, brandColor } from "../ui/colors.js";
|
|
14
|
+
const NO_OP_PLAN = {
|
|
15
|
+
initSharedPlan: () => { },
|
|
16
|
+
updateSharedPlan: () => { },
|
|
17
|
+
getExecutorContext: () => "",
|
|
18
|
+
getReviewerContext: () => "",
|
|
19
|
+
};
|
|
20
|
+
// Lazy-loaded promise — awaited before first use in runLoop()
|
|
21
|
+
let _planModulePromise = null;
|
|
22
|
+
async function loadPlanModule() {
|
|
23
|
+
if (!_planModulePromise) {
|
|
24
|
+
_planModulePromise = (async () => {
|
|
25
|
+
try {
|
|
26
|
+
const planPath = ["../plan", "shared-plan.js"].join("/");
|
|
27
|
+
const mod = (await import(planPath));
|
|
28
|
+
return {
|
|
29
|
+
initSharedPlan: typeof mod.initSharedPlan === "function"
|
|
30
|
+
? mod.initSharedPlan : NO_OP_PLAN.initSharedPlan,
|
|
31
|
+
updateSharedPlan: typeof mod.updateSharedPlan === "function"
|
|
32
|
+
? mod.updateSharedPlan : NO_OP_PLAN.updateSharedPlan,
|
|
33
|
+
getExecutorContext: typeof mod.getExecutorContext === "function"
|
|
34
|
+
? mod.getExecutorContext : NO_OP_PLAN.getExecutorContext,
|
|
35
|
+
getReviewerContext: typeof mod.getReviewerContext === "function"
|
|
36
|
+
? mod.getReviewerContext : NO_OP_PLAN.getReviewerContext,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return NO_OP_PLAN;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
}
|
|
44
|
+
return _planModulePromise;
|
|
45
|
+
}
|
|
46
|
+
// ── Helpers ──────────────────────────────────────────
|
|
47
|
+
function elapsed(startMs) {
|
|
48
|
+
const sec = ((Date.now() - startMs) / 1000).toFixed(1);
|
|
49
|
+
return dim(`(${sec}s)`);
|
|
50
|
+
}
|
|
51
|
+
function reviewerHeader(engine) {
|
|
52
|
+
const color = engine.color;
|
|
53
|
+
return color(` \u250C\u2500 \u25A0 ${engine.label} (reviewer) ${"\u2500".repeat(Math.max(0, 44 - engine.label.length))}\u2510`);
|
|
54
|
+
}
|
|
55
|
+
function reviewerFooter(engine, timeStr) {
|
|
56
|
+
const color = engine.color;
|
|
57
|
+
return color(` \u2514${"\u2500".repeat(42)} \u2713 done ${timeStr} \u2500\u2518`);
|
|
58
|
+
}
|
|
59
|
+
// ── Main loop ────────────────────────────────────────
|
|
60
|
+
export async function runLoop(options) {
|
|
61
|
+
const { task, maxIterations, executor, reviewer, cwd, verbose, mode, passthroughArgs, threshold, } = options;
|
|
62
|
+
const revColor = brandColor(reviewer.name);
|
|
63
|
+
const history = [];
|
|
64
|
+
const scoringConfig = {
|
|
65
|
+
threshold,
|
|
66
|
+
requireExplicitApproval: false,
|
|
67
|
+
};
|
|
68
|
+
// Load shared plan module (awaited — no race condition)
|
|
69
|
+
const plan = await loadPlanModule();
|
|
70
|
+
plan.initSharedPlan(cwd, task);
|
|
71
|
+
let executorOutput = "";
|
|
72
|
+
let reviewerFeedback = "";
|
|
73
|
+
for (let i = 1; i <= maxIterations; i++) {
|
|
74
|
+
console.log(bold(`\n ${"\u2550".repeat(12)} Iteration ${i} / ${maxIterations} ${"\u2550".repeat(12)}\n`));
|
|
75
|
+
// ── Build executor prompt ──
|
|
76
|
+
let initialPrompt;
|
|
77
|
+
if (i === 1) {
|
|
78
|
+
initialPrompt = task;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const executorContext = plan.getExecutorContext(cwd);
|
|
82
|
+
initialPrompt = [
|
|
83
|
+
"Please revise your previous work based on the following review feedback.",
|
|
84
|
+
"",
|
|
85
|
+
...(executorContext ? [executorContext, ""] : []),
|
|
86
|
+
"## Original Task",
|
|
87
|
+
task,
|
|
88
|
+
"",
|
|
89
|
+
"## Your Previous Output",
|
|
90
|
+
executorOutput,
|
|
91
|
+
"",
|
|
92
|
+
`## Review Feedback from ${reviewer.label}`,
|
|
93
|
+
reviewerFeedback,
|
|
94
|
+
"",
|
|
95
|
+
"Please make corrections based on the feedback and output the complete revised result.",
|
|
96
|
+
].join("\n");
|
|
97
|
+
}
|
|
98
|
+
// ── Multi-turn conversation with executor ──
|
|
99
|
+
const executorStartMs = Date.now();
|
|
100
|
+
const conversation = await runConversation({
|
|
101
|
+
engine: executor,
|
|
102
|
+
initialPrompt,
|
|
103
|
+
cwd,
|
|
104
|
+
verbose,
|
|
105
|
+
mode,
|
|
106
|
+
passthroughArgs,
|
|
107
|
+
});
|
|
108
|
+
executorOutput = conversation.finalOutput;
|
|
109
|
+
const executorDurationMs = Date.now() - executorStartMs;
|
|
110
|
+
// Create structured executor message
|
|
111
|
+
const executorMsg = createExecutorMessage({
|
|
112
|
+
iteration: i,
|
|
113
|
+
engine: executor.name,
|
|
114
|
+
originalTask: task,
|
|
115
|
+
context: reviewerFeedback,
|
|
116
|
+
outputText: executorOutput,
|
|
117
|
+
durationMs: executorDurationMs,
|
|
118
|
+
bytesReceived: conversation.bytes_received,
|
|
119
|
+
});
|
|
120
|
+
history.push(executorMsg);
|
|
121
|
+
// ── Reviewer ──
|
|
122
|
+
const reviewerContext = plan.getReviewerContext(cwd);
|
|
123
|
+
const reviewPrompt = [
|
|
124
|
+
"You are a code review expert. Please review the following task completion.",
|
|
125
|
+
"",
|
|
126
|
+
...(reviewerContext ? [reviewerContext, ""] : []),
|
|
127
|
+
"## Original Task",
|
|
128
|
+
task,
|
|
129
|
+
"",
|
|
130
|
+
formatForReviewer(executorMsg),
|
|
131
|
+
"",
|
|
132
|
+
"Please provide:",
|
|
133
|
+
"1. Score (1-10, 10 being perfect)",
|
|
134
|
+
"2. Issues found",
|
|
135
|
+
"3. Specific correction suggestions",
|
|
136
|
+
`4. If score >= ${threshold}, output "APPROVED" on the last line`,
|
|
137
|
+
].join("\n");
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(reviewerHeader(reviewer));
|
|
140
|
+
console.log(revColor(" \u2502"));
|
|
141
|
+
const revStart = Date.now();
|
|
142
|
+
reviewerFeedback = await reviewer.run(reviewPrompt, {
|
|
143
|
+
cwd,
|
|
144
|
+
verbose,
|
|
145
|
+
onData(chunk) {
|
|
146
|
+
const lines = chunk.split("\n");
|
|
147
|
+
for (let j = 0; j < lines.length; j++) {
|
|
148
|
+
const line = lines[j];
|
|
149
|
+
if (j < lines.length - 1) {
|
|
150
|
+
process.stdout.write(`${revColor(" \u2502")} ${line}\n`);
|
|
151
|
+
}
|
|
152
|
+
else if (line.length > 0) {
|
|
153
|
+
process.stdout.write(`${revColor(" \u2502")} ${line}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
console.log(revColor(" \u2502"));
|
|
159
|
+
console.log(reviewerFooter(reviewer, elapsed(revStart)));
|
|
160
|
+
// Parse structured reviewer output
|
|
161
|
+
const reviewerMsg = parseReviewerOutput(reviewerFeedback, {
|
|
162
|
+
iteration: i,
|
|
163
|
+
engine: reviewer.name,
|
|
164
|
+
originalTask: task,
|
|
165
|
+
durationMs: Date.now() - revStart,
|
|
166
|
+
bytesReceived: Buffer.byteLength(reviewerFeedback),
|
|
167
|
+
});
|
|
168
|
+
history.push(reviewerMsg);
|
|
169
|
+
// Evaluate review using scoring module
|
|
170
|
+
const scoringResult = evaluateReview(reviewerMsg.review, scoringConfig);
|
|
171
|
+
// Update shared plan with iteration data
|
|
172
|
+
plan.updateSharedPlan(cwd, {
|
|
173
|
+
iteration: i,
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
executor: executor.name,
|
|
176
|
+
reviewer: reviewer.name,
|
|
177
|
+
executorSummary: executorOutput.slice(0, 500),
|
|
178
|
+
reviewerScore: reviewerMsg.review?.score ?? 0,
|
|
179
|
+
reviewerApproved: scoringResult.approved,
|
|
180
|
+
reviewerFeedback: reviewerFeedback.slice(0, 500),
|
|
181
|
+
}, executorMsg.output.files_changed);
|
|
182
|
+
// ── Check approval ──
|
|
183
|
+
if (scoringResult.approved) {
|
|
184
|
+
console.log(success(bold(`\n \u2713 ${reviewer.label} approved! ${scoringResult.reason}. Completed after iteration ${i}.\n`)));
|
|
185
|
+
return {
|
|
186
|
+
iterations: i,
|
|
187
|
+
approved: true,
|
|
188
|
+
finalOutput: executorOutput,
|
|
189
|
+
history,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (i === maxIterations) {
|
|
193
|
+
console.log(warn(`\n \u26A0 Reached max iterations (${maxIterations}). ${scoringResult.reason}\n`));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(dim(`\n \u2192 ${scoringResult.reason}. Proceeding to iteration ${i + 1}...\n`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log(bold(`\n ${"\u2550".repeat(12)} Final Result ${"\u2550".repeat(12)}\n`));
|
|
200
|
+
console.log(executorOutput);
|
|
201
|
+
console.log("");
|
|
202
|
+
return {
|
|
203
|
+
iterations: maxIterations,
|
|
204
|
+
approved: false,
|
|
205
|
+
finalOutput: executorOutput,
|
|
206
|
+
history,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=loop.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured communication protocol for loop iterations.
|
|
3
|
+
*
|
|
4
|
+
* Ported from iterloop's protocol.ts with the protocol name changed
|
|
5
|
+
* from "iterloop-v1" to "loop-v1".
|
|
6
|
+
*/
|
|
7
|
+
import type { EngineName } from "./engine.js";
|
|
8
|
+
export interface LoopMessage {
|
|
9
|
+
protocol: "loop-v1";
|
|
10
|
+
timestamp: string;
|
|
11
|
+
iteration: number;
|
|
12
|
+
role: "executor" | "reviewer";
|
|
13
|
+
engine: EngineName;
|
|
14
|
+
task: {
|
|
15
|
+
original: string;
|
|
16
|
+
context: string;
|
|
17
|
+
};
|
|
18
|
+
output: {
|
|
19
|
+
text: string;
|
|
20
|
+
files_changed: string[];
|
|
21
|
+
commands_executed: string[];
|
|
22
|
+
status: "completed" | "needs_revision" | "error";
|
|
23
|
+
};
|
|
24
|
+
review?: {
|
|
25
|
+
score: number;
|
|
26
|
+
issues: string[];
|
|
27
|
+
suggestions: string[];
|
|
28
|
+
approved: boolean;
|
|
29
|
+
};
|
|
30
|
+
metadata: {
|
|
31
|
+
duration_ms: number;
|
|
32
|
+
bytes_received: number;
|
|
33
|
+
model?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Create an executor message from session output. */
|
|
37
|
+
export declare function createExecutorMessage(params: {
|
|
38
|
+
iteration: number;
|
|
39
|
+
engine: EngineName;
|
|
40
|
+
originalTask: string;
|
|
41
|
+
context: string;
|
|
42
|
+
outputText: string;
|
|
43
|
+
durationMs: number;
|
|
44
|
+
bytesReceived: number;
|
|
45
|
+
}): LoopMessage;
|
|
46
|
+
/** Parse reviewer output into a structured review. */
|
|
47
|
+
export declare function parseReviewerOutput(reviewText: string, params: {
|
|
48
|
+
iteration: number;
|
|
49
|
+
engine: EngineName;
|
|
50
|
+
originalTask: string;
|
|
51
|
+
durationMs: number;
|
|
52
|
+
bytesReceived: number;
|
|
53
|
+
}): LoopMessage;
|
|
54
|
+
/** Serialize a message to JSON string. */
|
|
55
|
+
export declare function serializeMessage(msg: LoopMessage): string;
|
|
56
|
+
/** Deserialize a JSON string to a message. */
|
|
57
|
+
export declare function deserializeMessage(json: string): LoopMessage;
|
|
58
|
+
/** Format an executor message for inclusion in the reviewer prompt. */
|
|
59
|
+
export declare function formatForReviewer(msg: LoopMessage): string;
|
|
60
|
+
//# sourceMappingURL=protocol.d.ts.map
|