@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -0
  4. package/dist/agent/activity.d.ts +64 -0
  5. package/dist/agent/activity.js +265 -0
  6. package/dist/agent/launcher.d.ts +42 -0
  7. package/dist/agent/launcher.js +243 -0
  8. package/dist/agent/pty-session.d.ts +113 -0
  9. package/dist/agent/pty-session.js +490 -0
  10. package/dist/agent/ready-detector.d.ts +46 -0
  11. package/dist/agent/ready-detector.js +86 -0
  12. package/dist/agent/wrapper.d.ts +18 -0
  13. package/dist/agent/wrapper.js +110 -0
  14. package/dist/bin/lclaude.d.ts +3 -0
  15. package/dist/bin/lclaude.js +7 -0
  16. package/dist/bin/lcodex.d.ts +3 -0
  17. package/dist/bin/lcodex.js +7 -0
  18. package/dist/bin/lgemini.d.ts +3 -0
  19. package/dist/bin/lgemini.js +7 -0
  20. package/dist/bus/daemon.d.ts +56 -0
  21. package/dist/bus/daemon.js +135 -0
  22. package/dist/bus/event-bus.d.ts +105 -0
  23. package/dist/bus/event-bus.js +157 -0
  24. package/dist/bus/message.d.ts +48 -0
  25. package/dist/bus/message.js +129 -0
  26. package/dist/bus/queue.d.ts +50 -0
  27. package/dist/bus/queue.js +100 -0
  28. package/dist/bus/store.d.ts +88 -0
  29. package/dist/bus/store.js +212 -0
  30. package/dist/bus/subscriber.d.ts +76 -0
  31. package/dist/bus/subscriber.js +187 -0
  32. package/dist/config/index.d.ts +8 -0
  33. package/dist/config/index.js +72 -0
  34. package/dist/config/schema.d.ts +18 -0
  35. package/dist/config/schema.js +58 -0
  36. package/dist/core/conversation.d.ts +34 -0
  37. package/dist/core/conversation.js +289 -0
  38. package/dist/core/engine.d.ts +40 -0
  39. package/dist/core/engine.js +288 -0
  40. package/dist/core/loop.d.ts +33 -0
  41. package/dist/core/loop.js +209 -0
  42. package/dist/core/protocol.d.ts +60 -0
  43. package/dist/core/protocol.js +162 -0
  44. package/dist/core/scoring.d.ts +34 -0
  45. package/dist/core/scoring.js +69 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.js +408 -0
  48. package/dist/orchestrator/daemon.d.ts +74 -0
  49. package/dist/orchestrator/daemon.js +294 -0
  50. package/dist/orchestrator/group.d.ts +73 -0
  51. package/dist/orchestrator/group.js +166 -0
  52. package/dist/orchestrator/ipc-server.d.ts +60 -0
  53. package/dist/orchestrator/ipc-server.js +166 -0
  54. package/dist/orchestrator/scheduler.d.ts +32 -0
  55. package/dist/orchestrator/scheduler.js +95 -0
  56. package/dist/plan/context.d.ts +8 -0
  57. package/dist/plan/context.js +42 -0
  58. package/dist/plan/decisions.d.ts +18 -0
  59. package/dist/plan/decisions.js +143 -0
  60. package/dist/plan/shared-plan.d.ts +33 -0
  61. package/dist/plan/shared-plan.js +211 -0
  62. package/dist/skills/executor.d.ts +7 -0
  63. package/dist/skills/executor.js +11 -0
  64. package/dist/skills/loader.d.ts +16 -0
  65. package/dist/skills/loader.js +80 -0
  66. package/dist/skills/registry.d.ts +13 -0
  67. package/dist/skills/registry.js +54 -0
  68. package/dist/terminal/adapter.d.ts +61 -0
  69. package/dist/terminal/adapter.js +42 -0
  70. package/dist/terminal/detect.d.ts +30 -0
  71. package/dist/terminal/detect.js +77 -0
  72. package/dist/terminal/iterm2-adapter.d.ts +19 -0
  73. package/dist/terminal/iterm2-adapter.js +120 -0
  74. package/dist/terminal/pty-adapter.d.ts +18 -0
  75. package/dist/terminal/pty-adapter.js +84 -0
  76. package/dist/terminal/terminal-adapter.d.ts +17 -0
  77. package/dist/terminal/terminal-adapter.js +94 -0
  78. package/dist/terminal/tmux-adapter.d.ts +18 -0
  79. package/dist/terminal/tmux-adapter.js +127 -0
  80. package/dist/ui/banner.d.ts +3 -0
  81. package/dist/ui/banner.js +145 -0
  82. package/dist/ui/colors.d.ts +41 -0
  83. package/dist/ui/colors.js +65 -0
  84. package/dist/ui/dashboard.d.ts +32 -0
  85. package/dist/ui/dashboard.js +138 -0
  86. package/dist/ui/input.d.ts +10 -0
  87. package/dist/ui/input.js +96 -0
  88. package/dist/ui/interactive.d.ts +13 -0
  89. package/dist/ui/interactive.js +230 -0
  90. package/dist/ui/renderer.d.ts +33 -0
  91. package/dist/ui/renderer.js +106 -0
  92. package/dist/utils/ansi.d.ts +11 -0
  93. package/dist/utils/ansi.js +16 -0
  94. package/dist/utils/fs.d.ts +34 -0
  95. package/dist/utils/fs.js +115 -0
  96. package/dist/utils/lock.d.ts +12 -0
  97. package/dist/utils/lock.js +116 -0
  98. package/dist/utils/process.d.ts +31 -0
  99. package/dist/utils/process.js +111 -0
  100. package/dist/utils/pty-filter.d.ts +31 -0
  101. package/dist/utils/pty-filter.js +187 -0
  102. package/package.json +71 -0
  103. package/skills/loop/SKILL.md +19 -0
  104. package/skills/plan/SKILL.md +9 -0
  105. 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
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
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