@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,162 @@
|
|
|
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
|
+
// ── Message constructors ─────────────────────────────
|
|
8
|
+
/** Create an executor message from session output. */
|
|
9
|
+
export function createExecutorMessage(params) {
|
|
10
|
+
return {
|
|
11
|
+
protocol: "loop-v1",
|
|
12
|
+
timestamp: new Date().toISOString(),
|
|
13
|
+
iteration: params.iteration,
|
|
14
|
+
role: "executor",
|
|
15
|
+
engine: params.engine,
|
|
16
|
+
task: {
|
|
17
|
+
original: params.originalTask,
|
|
18
|
+
context: params.context,
|
|
19
|
+
},
|
|
20
|
+
output: {
|
|
21
|
+
text: params.outputText,
|
|
22
|
+
files_changed: extractFilesChanged(params.outputText),
|
|
23
|
+
commands_executed: extractCommandsExecuted(params.outputText),
|
|
24
|
+
status: "completed",
|
|
25
|
+
},
|
|
26
|
+
metadata: {
|
|
27
|
+
duration_ms: params.durationMs,
|
|
28
|
+
bytes_received: params.bytesReceived,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Parse reviewer output into a structured review. */
|
|
33
|
+
export function parseReviewerOutput(reviewText, params) {
|
|
34
|
+
const score = extractScore(reviewText);
|
|
35
|
+
const issues = extractListSection(reviewText, "issues");
|
|
36
|
+
const suggestions = extractListSection(reviewText, "suggestions");
|
|
37
|
+
const approved = reviewText
|
|
38
|
+
.split("\n")
|
|
39
|
+
.some((line) => /^\s*APPROVED\s*$/.test(line));
|
|
40
|
+
return {
|
|
41
|
+
protocol: "loop-v1",
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
iteration: params.iteration,
|
|
44
|
+
role: "reviewer",
|
|
45
|
+
engine: params.engine,
|
|
46
|
+
task: {
|
|
47
|
+
original: params.originalTask,
|
|
48
|
+
context: "",
|
|
49
|
+
},
|
|
50
|
+
output: {
|
|
51
|
+
text: reviewText,
|
|
52
|
+
files_changed: [],
|
|
53
|
+
commands_executed: [],
|
|
54
|
+
status: approved ? "completed" : "needs_revision",
|
|
55
|
+
},
|
|
56
|
+
review: {
|
|
57
|
+
score,
|
|
58
|
+
issues,
|
|
59
|
+
suggestions,
|
|
60
|
+
approved,
|
|
61
|
+
},
|
|
62
|
+
metadata: {
|
|
63
|
+
duration_ms: params.durationMs,
|
|
64
|
+
bytes_received: params.bytesReceived,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Serialize a message to JSON string. */
|
|
69
|
+
export function serializeMessage(msg) {
|
|
70
|
+
return JSON.stringify(msg, null, 2);
|
|
71
|
+
}
|
|
72
|
+
/** Deserialize a JSON string to a message. */
|
|
73
|
+
export function deserializeMessage(json) {
|
|
74
|
+
const parsed = JSON.parse(json);
|
|
75
|
+
if (typeof parsed !== "object" ||
|
|
76
|
+
parsed === null ||
|
|
77
|
+
!("protocol" in parsed) ||
|
|
78
|
+
parsed.protocol !== "loop-v1") {
|
|
79
|
+
throw new Error(`Unknown protocol: ${typeof parsed === "object" && parsed !== null && "protocol" in parsed ? parsed.protocol : "undefined"}`);
|
|
80
|
+
}
|
|
81
|
+
return parsed;
|
|
82
|
+
}
|
|
83
|
+
/** Format an executor message for inclusion in the reviewer prompt. */
|
|
84
|
+
export function formatForReviewer(msg) {
|
|
85
|
+
const sections = [];
|
|
86
|
+
sections.push(`## Executor Output (${msg.engine}, iteration ${msg.iteration})`);
|
|
87
|
+
sections.push("");
|
|
88
|
+
sections.push(msg.output.text);
|
|
89
|
+
if (msg.output.files_changed.length > 0) {
|
|
90
|
+
sections.push("");
|
|
91
|
+
sections.push("## Files Changed");
|
|
92
|
+
for (const f of msg.output.files_changed) {
|
|
93
|
+
sections.push(`- ${f}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (msg.output.commands_executed.length > 0) {
|
|
97
|
+
sections.push("");
|
|
98
|
+
sections.push("## Commands Executed");
|
|
99
|
+
for (const c of msg.output.commands_executed) {
|
|
100
|
+
sections.push(`- ${c}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
sections.push("");
|
|
104
|
+
sections.push("## Metadata");
|
|
105
|
+
sections.push(`- Duration: ${(msg.metadata.duration_ms / 1000).toFixed(1)}s`);
|
|
106
|
+
sections.push(`- Data received: ${msg.metadata.bytes_received} bytes`);
|
|
107
|
+
return sections.join("\n");
|
|
108
|
+
}
|
|
109
|
+
// ── Extraction helpers ───────────────────────────────
|
|
110
|
+
function extractFilesChanged(text) {
|
|
111
|
+
const files = [];
|
|
112
|
+
const patterns = [
|
|
113
|
+
/(?:created|modified|wrote to|editing|writing)\s+(?:file:?\s*)?([^\s,]+\.\w+)/gi,
|
|
114
|
+
/(?:Read|Edit|Write)\s+([^\s]+\.\w+)/g,
|
|
115
|
+
];
|
|
116
|
+
for (const pattern of patterns) {
|
|
117
|
+
let match;
|
|
118
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
119
|
+
const file = match[1];
|
|
120
|
+
if (!files.includes(file))
|
|
121
|
+
files.push(file);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return files;
|
|
125
|
+
}
|
|
126
|
+
function extractCommandsExecuted(text) {
|
|
127
|
+
const commands = [];
|
|
128
|
+
const patterns = [
|
|
129
|
+
/^\$\s+(.+)$/gm,
|
|
130
|
+
/(?:running|executing|ran):\s*(.+)$/gim,
|
|
131
|
+
/Bash\s+(.+)$/gm,
|
|
132
|
+
];
|
|
133
|
+
for (const pattern of patterns) {
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
136
|
+
const cmd = match[1].trim();
|
|
137
|
+
if (cmd && !commands.includes(cmd))
|
|
138
|
+
commands.push(cmd);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return commands;
|
|
142
|
+
}
|
|
143
|
+
function extractScore(text) {
|
|
144
|
+
const match = text.match(/(?:score|rating)\s*:?\s*(\d+)\s*(?:\/\s*10)?/i) ??
|
|
145
|
+
text.match(/(\d+)\s*\/\s*10/);
|
|
146
|
+
return match ? Math.min(10, Math.max(1, parseInt(match[1], 10))) : 0;
|
|
147
|
+
}
|
|
148
|
+
function extractListSection(text, sectionName) {
|
|
149
|
+
const items = [];
|
|
150
|
+
const sectionPattern = new RegExp(`(?:#{1,3}\\s*)?${sectionName}[:\\s]*\\n([\\s\\S]*?)(?=\\n#{1,3}\\s|\\n\\n|$)`, "i");
|
|
151
|
+
const match = text.match(sectionPattern);
|
|
152
|
+
if (match) {
|
|
153
|
+
const lines = match[1].split("\n");
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
const itemMatch = line.match(/^\s*[-*\d.]+\s+(.+)/);
|
|
156
|
+
if (itemMatch)
|
|
157
|
+
items.push(itemMatch[1].trim());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return items;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring and approval logic — extracted from the inline check in iterloop's
|
|
3
|
+
* loop.ts into a reusable module.
|
|
4
|
+
*/
|
|
5
|
+
export interface ScoringConfig {
|
|
6
|
+
/** Minimum score (1-10) required for automatic approval. Default: 9 */
|
|
7
|
+
threshold: number;
|
|
8
|
+
/** If true, only approve when the reviewer explicitly outputs APPROVED. Default: false */
|
|
9
|
+
requireExplicitApproval: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ScoringResult {
|
|
12
|
+
/** The numeric score extracted from the review (0 if not found). */
|
|
13
|
+
score: number;
|
|
14
|
+
/** Whether the review meets approval criteria. */
|
|
15
|
+
approved: boolean;
|
|
16
|
+
/** Human-readable reason for the decision. */
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_SCORING_CONFIG: ScoringConfig;
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a parsed review against the scoring configuration.
|
|
22
|
+
*
|
|
23
|
+
* Rules:
|
|
24
|
+
* 1. If `review` is undefined → not approved, reason "No review data".
|
|
25
|
+
* 2. If `requireExplicitApproval` is true → only approve when `review.approved === true`.
|
|
26
|
+
* 3. Otherwise → approve when `review.score >= threshold` OR `review.approved === true`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function evaluateReview(review: {
|
|
29
|
+
score: number;
|
|
30
|
+
issues: string[];
|
|
31
|
+
suggestions: string[];
|
|
32
|
+
approved: boolean;
|
|
33
|
+
} | undefined, config: ScoringConfig): ScoringResult;
|
|
34
|
+
//# sourceMappingURL=scoring.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scoring and approval logic — extracted from the inline check in iterloop's
|
|
3
|
+
* loop.ts into a reusable module.
|
|
4
|
+
*/
|
|
5
|
+
// ── Default config ───────────────────────────────────
|
|
6
|
+
export const DEFAULT_SCORING_CONFIG = {
|
|
7
|
+
threshold: 9,
|
|
8
|
+
requireExplicitApproval: false,
|
|
9
|
+
};
|
|
10
|
+
// ── Evaluation ───────────────────────────────────────
|
|
11
|
+
/**
|
|
12
|
+
* Evaluate a parsed review against the scoring configuration.
|
|
13
|
+
*
|
|
14
|
+
* Rules:
|
|
15
|
+
* 1. If `review` is undefined → not approved, reason "No review data".
|
|
16
|
+
* 2. If `requireExplicitApproval` is true → only approve when `review.approved === true`.
|
|
17
|
+
* 3. Otherwise → approve when `review.score >= threshold` OR `review.approved === true`.
|
|
18
|
+
*/
|
|
19
|
+
export function evaluateReview(review, config) {
|
|
20
|
+
if (!review) {
|
|
21
|
+
return {
|
|
22
|
+
score: 0,
|
|
23
|
+
approved: false,
|
|
24
|
+
reason: "No review data",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const { score, approved: explicitlyApproved } = review;
|
|
28
|
+
if (config.requireExplicitApproval) {
|
|
29
|
+
return {
|
|
30
|
+
score,
|
|
31
|
+
approved: explicitlyApproved,
|
|
32
|
+
reason: explicitlyApproved
|
|
33
|
+
? "Reviewer explicitly approved"
|
|
34
|
+
: `Reviewer did not explicitly approve (score: ${score}/10)`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Standard mode: approve if score meets threshold OR explicit approval
|
|
38
|
+
const meetsThreshold = score >= config.threshold;
|
|
39
|
+
const approved = meetsThreshold || explicitlyApproved;
|
|
40
|
+
if (approved) {
|
|
41
|
+
if (explicitlyApproved && meetsThreshold) {
|
|
42
|
+
return {
|
|
43
|
+
score,
|
|
44
|
+
approved: true,
|
|
45
|
+
reason: `Approved (score: ${score}/10, explicitly approved)`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (explicitlyApproved) {
|
|
49
|
+
return {
|
|
50
|
+
score,
|
|
51
|
+
approved: true,
|
|
52
|
+
reason: `Approved (explicitly approved, score: ${score}/10)`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
score,
|
|
57
|
+
approved: true,
|
|
58
|
+
reason: `Approved (score: ${score}/10 meets threshold of ${config.threshold})`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const issueCount = review.issues.length;
|
|
62
|
+
const issueNote = issueCount > 0 ? `, ${issueCount} issue${issueCount === 1 ? "" : "s"}` : "";
|
|
63
|
+
return {
|
|
64
|
+
score,
|
|
65
|
+
approved: false,
|
|
66
|
+
reason: `Not approved (score: ${score}/10, threshold: ${config.threshold}${issueNote})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=scoring.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { bold, red, yellow, green } from "./ui/colors.js";
|
|
6
|
+
import { ENGINE_NAMES } from "./config/schema.js";
|
|
7
|
+
import { loadConfig } from "./config/index.js";
|
|
8
|
+
import { interactive } from "./ui/interactive.js";
|
|
9
|
+
import { showPlan, clearPlan } from "./plan/shared-plan.js";
|
|
10
|
+
import { addDecision, listDecisions, resolveDecision } from "./plan/decisions.js";
|
|
11
|
+
import { SkillRegistry } from "./skills/registry.js";
|
|
12
|
+
import { runLoop } from "./core/loop.js";
|
|
13
|
+
import { createEngine } from "./core/engine.js";
|
|
14
|
+
import { EventBus } from "./bus/event-bus.js";
|
|
15
|
+
import { OrchestratorDaemon } from "./orchestrator/daemon.js";
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name("loop")
|
|
19
|
+
.description("Iterative multi-engine AI orchestration CLI — Claude, Gemini, Codex")
|
|
20
|
+
.version("0.1.0")
|
|
21
|
+
.argument("[task]", "Task description (omit to enter interactive mode)")
|
|
22
|
+
.option("-e, --executor <engine>", "Executor engine: claude | gemini | codex")
|
|
23
|
+
.option("-r, --reviewer <engine>", "Reviewer engine: claude | gemini | codex")
|
|
24
|
+
.option("-n, --iterations <number>", "Max number of iterations")
|
|
25
|
+
.option("-d, --dir <path>", "Working directory")
|
|
26
|
+
.option("-v, --verbose", "Stream real-time output from CLI tools")
|
|
27
|
+
.option("--auto", "Auto mode: skip manual conversation, auto-submit to reviewer")
|
|
28
|
+
.option("--pass <args...>", "Pass native flags to executor CLI")
|
|
29
|
+
.option("--threshold <number>", "Approval score threshold (1-10)")
|
|
30
|
+
.action(async (task, options) => {
|
|
31
|
+
try {
|
|
32
|
+
const config = await loadConfig(options.dir ?? process.cwd());
|
|
33
|
+
if (!task) {
|
|
34
|
+
// Interactive mode
|
|
35
|
+
const interactiveConfig = await interactive();
|
|
36
|
+
if (!interactiveConfig)
|
|
37
|
+
process.exit(0);
|
|
38
|
+
const execName = interactiveConfig.executor;
|
|
39
|
+
const revName = interactiveConfig.reviewer;
|
|
40
|
+
console.log(bold("\n Preflight check...\n"));
|
|
41
|
+
await preflight(execName, revName);
|
|
42
|
+
await runLoop({
|
|
43
|
+
task: interactiveConfig.task,
|
|
44
|
+
maxIterations: interactiveConfig.iterations,
|
|
45
|
+
threshold: interactiveConfig.threshold,
|
|
46
|
+
executor: createEngine(execName),
|
|
47
|
+
reviewer: createEngine(revName),
|
|
48
|
+
cwd: interactiveConfig.dir,
|
|
49
|
+
verbose: interactiveConfig.verbose,
|
|
50
|
+
mode: { current: interactiveConfig.mode },
|
|
51
|
+
passthroughArgs: interactiveConfig.passthroughArgs,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Command-line mode
|
|
56
|
+
const executorName = (options.executor ?? config.defaultExecutor);
|
|
57
|
+
const reviewerName = (options.reviewer ?? config.defaultReviewer);
|
|
58
|
+
if (!ENGINE_NAMES.includes(executorName)) {
|
|
59
|
+
console.error(red(`Invalid executor: ${options.executor}. Choose: claude | gemini | codex`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (!ENGINE_NAMES.includes(reviewerName)) {
|
|
63
|
+
console.error(red(`Invalid reviewer: ${options.reviewer}. Choose: claude | gemini | codex`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const iterations = options.iterations
|
|
67
|
+
? parseInt(options.iterations, 10)
|
|
68
|
+
: config.maxIterations;
|
|
69
|
+
if (isNaN(iterations) || iterations < 1 || iterations > 20) {
|
|
70
|
+
console.error(red("Invalid iterations: must be between 1 and 20"));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const threshold = options.threshold
|
|
74
|
+
? parseInt(options.threshold, 10)
|
|
75
|
+
: config.threshold;
|
|
76
|
+
if (isNaN(threshold) || threshold < 1 || threshold > 10) {
|
|
77
|
+
console.error(red("Invalid threshold: must be between 1 and 10"));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const modeValue = options.auto ? "auto" : config.mode;
|
|
81
|
+
if (options.dir && !existsSync(options.dir)) {
|
|
82
|
+
console.error(red(`Invalid directory: ${options.dir} does not exist`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (executorName === reviewerName) {
|
|
86
|
+
console.log(yellow(`\n Warning: executor and reviewer are both "${executorName}". Using different engines is recommended.\n`));
|
|
87
|
+
}
|
|
88
|
+
console.log(bold("\n Preflight check...\n"));
|
|
89
|
+
await preflight(executorName, reviewerName);
|
|
90
|
+
const cwd = options.dir ? resolve(options.dir) : process.cwd();
|
|
91
|
+
console.log(bold("\n Starting loop\n"));
|
|
92
|
+
console.log(` Executor: ${executorName}`);
|
|
93
|
+
console.log(` Reviewer: ${reviewerName}`);
|
|
94
|
+
console.log(` Task: ${task}`);
|
|
95
|
+
console.log(` Iterations: ${iterations}`);
|
|
96
|
+
console.log(` Threshold: ${threshold}`);
|
|
97
|
+
console.log(` Directory: ${cwd}`);
|
|
98
|
+
console.log(` Verbose: ${options.verbose ? "on" : "off"}`);
|
|
99
|
+
console.log(` Mode: ${modeValue === "auto" ? "\u23F5\u23F5 Auto" : "\u23F5\u23F5 Manual"}`);
|
|
100
|
+
await runLoop({
|
|
101
|
+
task,
|
|
102
|
+
maxIterations: iterations,
|
|
103
|
+
threshold,
|
|
104
|
+
executor: createEngine(executorName),
|
|
105
|
+
reviewer: createEngine(reviewerName),
|
|
106
|
+
cwd,
|
|
107
|
+
verbose: options.verbose ?? false,
|
|
108
|
+
mode: { current: modeValue },
|
|
109
|
+
passthroughArgs: options.pass,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
console.error(red(`\n Error: ${msg}`));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// ── Subcommand: daemon ──────────────────────────────
|
|
120
|
+
const daemon = program.command("daemon").description("Daemon management");
|
|
121
|
+
daemon
|
|
122
|
+
.command("start")
|
|
123
|
+
.description("Start the loop daemon")
|
|
124
|
+
.action(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const cwd = process.cwd();
|
|
127
|
+
const mgr = new OrchestratorDaemon(cwd);
|
|
128
|
+
console.log(bold(" Starting daemon..."));
|
|
129
|
+
await mgr.start();
|
|
130
|
+
console.log(green(" Daemon started (pid=" + process.pid + ")"));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
134
|
+
console.error(red(` Error: ${msg}`));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
daemon
|
|
139
|
+
.command("stop")
|
|
140
|
+
.description("Stop the loop daemon")
|
|
141
|
+
.action(async () => {
|
|
142
|
+
try {
|
|
143
|
+
const cwd = process.cwd();
|
|
144
|
+
const mgr = new OrchestratorDaemon(cwd);
|
|
145
|
+
if (!mgr.isRunning()) {
|
|
146
|
+
console.log(yellow(" Daemon is not running."));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await mgr.stop();
|
|
150
|
+
console.log(green(" Daemon stopped."));
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
154
|
+
console.error(red(` Error: ${msg}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
daemon
|
|
159
|
+
.command("status")
|
|
160
|
+
.description("Show daemon status")
|
|
161
|
+
.action(async () => {
|
|
162
|
+
try {
|
|
163
|
+
const cwd = process.cwd();
|
|
164
|
+
const mgr = new OrchestratorDaemon(cwd);
|
|
165
|
+
if (!mgr.isRunning()) {
|
|
166
|
+
console.log(yellow(" Daemon is not running."));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const status = await mgr.getStatus();
|
|
170
|
+
console.log(bold(" Daemon status:"));
|
|
171
|
+
console.log(` PID: ${status.pid}`);
|
|
172
|
+
console.log(` Uptime: ${status.uptime}s`);
|
|
173
|
+
console.log(` Agents: ${status.agents}`);
|
|
174
|
+
console.log(` Events: ${status.busEvents}`);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
console.error(red(` Error: ${msg}`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// ── Subcommand: bus ─────────────────────────────────
|
|
183
|
+
const bus = program.command("bus").description("Event bus operations");
|
|
184
|
+
bus
|
|
185
|
+
.command("send <message>")
|
|
186
|
+
.description("Send a message on the event bus")
|
|
187
|
+
.option("-t, --target <id>", "Target subscriber ID", "*")
|
|
188
|
+
.action(async (message, opts) => {
|
|
189
|
+
try {
|
|
190
|
+
const cwd = process.cwd();
|
|
191
|
+
const eventBus = new EventBus(cwd);
|
|
192
|
+
await eventBus.init();
|
|
193
|
+
const target = opts.target ?? "*";
|
|
194
|
+
const event = target === "*"
|
|
195
|
+
? await eventBus.broadcast("cli", message)
|
|
196
|
+
: await eventBus.send("cli", target, message);
|
|
197
|
+
console.log(green(` Sent (seq=${event.seq}, target=${event.target})`));
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
201
|
+
console.error(red(` Error: ${msg}`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
bus
|
|
206
|
+
.command("check <subscriberId>")
|
|
207
|
+
.description("Check for pending bus messages")
|
|
208
|
+
.action(async (subscriberId) => {
|
|
209
|
+
try {
|
|
210
|
+
const cwd = process.cwd();
|
|
211
|
+
const eventBus = new EventBus(cwd);
|
|
212
|
+
await eventBus.init();
|
|
213
|
+
const events = await eventBus.check(subscriberId);
|
|
214
|
+
if (events.length === 0) {
|
|
215
|
+
console.log(" No pending messages.");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
console.log(bold(` Pending messages (${events.length}):\n`));
|
|
219
|
+
for (const e of events) {
|
|
220
|
+
console.log(` [${e.seq}] ${e.publisher} → ${e.target}: ${JSON.stringify(e.data)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
225
|
+
console.error(red(` Error: ${msg}`));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
bus
|
|
230
|
+
.command("status")
|
|
231
|
+
.description("Show event bus status")
|
|
232
|
+
.action(async () => {
|
|
233
|
+
try {
|
|
234
|
+
const cwd = process.cwd();
|
|
235
|
+
const eventBus = new EventBus(cwd);
|
|
236
|
+
await eventBus.init();
|
|
237
|
+
const status = await eventBus.status();
|
|
238
|
+
console.log(bold(" Bus status:"));
|
|
239
|
+
console.log(` Workspace: ${status.id}`);
|
|
240
|
+
console.log(` Agents: ${status.agents}`);
|
|
241
|
+
console.log(` Events: ${status.events}`);
|
|
242
|
+
if (status.agentList.length > 0) {
|
|
243
|
+
console.log(bold("\n Agents:"));
|
|
244
|
+
for (const a of status.agentList) {
|
|
245
|
+
console.log(` ${a.id} [${a.type}] ${a.nickname || ""} — ${a.status}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
251
|
+
console.error(red(` Error: ${msg}`));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
// ── Subcommand: chat ────────────────────────────────
|
|
256
|
+
program
|
|
257
|
+
.command("chat")
|
|
258
|
+
.description("Interactive dashboard")
|
|
259
|
+
.action(async () => {
|
|
260
|
+
const { Dashboard } = await import("./ui/dashboard.js");
|
|
261
|
+
const dashboard = new Dashboard();
|
|
262
|
+
dashboard.start();
|
|
263
|
+
});
|
|
264
|
+
// ── Subcommand: plan ────────────────────────────────
|
|
265
|
+
const plan = program.command("plan").description("Plan management");
|
|
266
|
+
plan
|
|
267
|
+
.command("show")
|
|
268
|
+
.description("Show the current shared plan")
|
|
269
|
+
.action(async () => {
|
|
270
|
+
const cwd = process.cwd();
|
|
271
|
+
const content = await showPlan(cwd);
|
|
272
|
+
console.log(content);
|
|
273
|
+
});
|
|
274
|
+
plan
|
|
275
|
+
.command("clear")
|
|
276
|
+
.description("Clear the shared plan")
|
|
277
|
+
.action(async () => {
|
|
278
|
+
const cwd = process.cwd();
|
|
279
|
+
await clearPlan(cwd);
|
|
280
|
+
console.log(green(" Plan cleared."));
|
|
281
|
+
});
|
|
282
|
+
// ── Subcommand: ctx (decisions) ─────────────────────
|
|
283
|
+
const ctx = program.command("ctx").description("Decision tracking");
|
|
284
|
+
ctx
|
|
285
|
+
.command("add <title>")
|
|
286
|
+
.description("Add a new decision")
|
|
287
|
+
.option("-s, --status <status>", "Initial status: proposed | accepted", "proposed")
|
|
288
|
+
.option("--context <text>", "Decision context")
|
|
289
|
+
.option("--decision <text>", "The decision itself")
|
|
290
|
+
.option("--consequences <text>", "Consequences of the decision")
|
|
291
|
+
.action(async (title, opts) => {
|
|
292
|
+
const cwd = process.cwd();
|
|
293
|
+
const status = opts.status === "accepted" ||
|
|
294
|
+
opts.status === "proposed" ||
|
|
295
|
+
opts.status === "rejected" ||
|
|
296
|
+
opts.status === "superseded"
|
|
297
|
+
? opts.status
|
|
298
|
+
: "proposed";
|
|
299
|
+
const decision = await addDecision(cwd, {
|
|
300
|
+
title,
|
|
301
|
+
status: status,
|
|
302
|
+
context: opts.context ?? "",
|
|
303
|
+
decision: opts.decision ?? "",
|
|
304
|
+
consequences: opts.consequences ?? "",
|
|
305
|
+
});
|
|
306
|
+
console.log(green(` Created decision #${decision.id}: ${decision.title}`));
|
|
307
|
+
});
|
|
308
|
+
ctx
|
|
309
|
+
.command("list")
|
|
310
|
+
.description("List all decisions")
|
|
311
|
+
.option("-s, --status <status>", "Filter by status")
|
|
312
|
+
.action(async (opts) => {
|
|
313
|
+
const cwd = process.cwd();
|
|
314
|
+
const decisions = await listDecisions(cwd);
|
|
315
|
+
const filtered = opts.status
|
|
316
|
+
? decisions.filter((d) => d.status === opts.status)
|
|
317
|
+
: decisions;
|
|
318
|
+
if (filtered.length === 0) {
|
|
319
|
+
console.log(" No decisions found.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
console.log(bold(` Decisions (${filtered.length}):\n`));
|
|
323
|
+
for (const d of filtered) {
|
|
324
|
+
const statusColor = d.status === "accepted"
|
|
325
|
+
? green
|
|
326
|
+
: d.status === "rejected"
|
|
327
|
+
? red
|
|
328
|
+
: yellow;
|
|
329
|
+
console.log(` #${d.id} [${statusColor(d.status.toUpperCase())}] ${d.title} ${d.date}`);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
ctx
|
|
333
|
+
.command("resolve <id>")
|
|
334
|
+
.description("Resolve a decision")
|
|
335
|
+
.option("-s, --status <status>", "New status: accepted | rejected | superseded", "accepted")
|
|
336
|
+
.action(async (id, opts) => {
|
|
337
|
+
const cwd = process.cwd();
|
|
338
|
+
const numId = parseInt(id, 10);
|
|
339
|
+
if (isNaN(numId)) {
|
|
340
|
+
console.error(red(" Invalid decision ID"));
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
const status = opts.status === "accepted" ||
|
|
344
|
+
opts.status === "rejected" ||
|
|
345
|
+
opts.status === "superseded"
|
|
346
|
+
? opts.status
|
|
347
|
+
: "accepted";
|
|
348
|
+
await resolveDecision(cwd, numId, status);
|
|
349
|
+
console.log(green(` Decision #${numId} resolved as ${status}`));
|
|
350
|
+
});
|
|
351
|
+
// ── Subcommand: skills ──────────────────────────────
|
|
352
|
+
const skills = program.command("skills").description("Skills management");
|
|
353
|
+
skills
|
|
354
|
+
.command("list")
|
|
355
|
+
.description("List available skills")
|
|
356
|
+
.action(async () => {
|
|
357
|
+
const cwd = process.cwd();
|
|
358
|
+
const registry = new SkillRegistry();
|
|
359
|
+
await registry.load(cwd);
|
|
360
|
+
const all = registry.list();
|
|
361
|
+
if (all.length === 0) {
|
|
362
|
+
console.log(" No skills found.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
console.log(bold(` Skills (${all.length}):\n`));
|
|
366
|
+
for (const s of all) {
|
|
367
|
+
const scope = `[${s.scope}]`.padEnd(10);
|
|
368
|
+
console.log(` ${scope} ${bold(s.name)} ${s.description}`);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
skills
|
|
372
|
+
.command("add <name>")
|
|
373
|
+
.description("Add a new skill")
|
|
374
|
+
.option("--global", "Add as global skill")
|
|
375
|
+
.option("--content <text>", "Skill content (markdown)")
|
|
376
|
+
.action(async (name, opts) => {
|
|
377
|
+
const cwd = process.cwd();
|
|
378
|
+
const scope = opts.global ? "global" : "project";
|
|
379
|
+
const content = opts.content ?? `# ${name}\n\nSkill content here.\n`;
|
|
380
|
+
const registry = new SkillRegistry();
|
|
381
|
+
await registry.add(name, content, scope, cwd);
|
|
382
|
+
console.log(green(` Added skill: ${name} (${scope})`));
|
|
383
|
+
});
|
|
384
|
+
// ── Preflight check ─────────────────────────────────
|
|
385
|
+
async function preflight(executor, reviewer) {
|
|
386
|
+
const { execFileSync } = await import("node:child_process");
|
|
387
|
+
const names = [...new Set([executor, reviewer])];
|
|
388
|
+
let ok = true;
|
|
389
|
+
for (const name of names) {
|
|
390
|
+
try {
|
|
391
|
+
const version = execFileSync(name, ["--version"], {
|
|
392
|
+
encoding: "utf-8",
|
|
393
|
+
timeout: 5000,
|
|
394
|
+
}).trim();
|
|
395
|
+
console.log(green(` \u2713 ${name}`) + ` ${version}`);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
console.error(red(` \u2717 ${name} CLI not found`));
|
|
399
|
+
ok = false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!ok) {
|
|
403
|
+
throw new Error("Required engine CLI(s) not found. Please install them and try again.");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── Parse and run ───────────────────────────────────
|
|
407
|
+
program.parse();
|
|
408
|
+
//# sourceMappingURL=index.js.map
|