@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-03-10
9
+
10
+ ### Added
11
+
12
+ - Iterative execution loop: executor produces output, reviewer scores (1-10), feedback fed back until approved
13
+ - Multi-engine support: Claude CLI, Gemini CLI, Codex CLI via unified `Engine` interface
14
+ - File-based event bus: append-only JSONL event streaming, crash-safe, zero external dependencies
15
+ - Background daemon: agent lifecycle management with Unix domain socket IPC
16
+ - Agent wrappers: `lclaude`, `lgemini`, `lcodex` — transforms CLI agents into loop participants
17
+ - Skills system: executable markdown (SKILL.md) auto-injected into agent prompts
18
+ - Interactive TUI: `@clack/prompts` guided setup + `blessed` real-time monitoring dashboard
19
+ - Terminal adapters: pluggable backends for Terminal.app, iTerm2, tmux, PTY emulation
20
+ - Shared plan management: cross-session iteration context with architectural decision tracking
21
+ - Configuration cascade: project-level `.loop/config.json` with sensible defaults
22
+ - 282 tests across 23 test files with full pass rate
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 loop-cli contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # loop
2
+
3
+ > Iterative Multi-Engine AI Orchestration CLI
4
+
5
+ Combine **Claude**, **Gemini**, and **Codex** in a quality-scored iteration loop. One engine executes, another reviews. Repeat until perfect.
6
+
7
+ ```
8
+ npm install -g @lawrence369/loop-cli
9
+ ```
10
+
11
+ ## What It Does
12
+
13
+ ```
14
+ ┌─────────────┐ ┌─────────────┐
15
+ │ Executor │────▶│ Reviewer │
16
+ │ (Claude) │◀────│ (Gemini) │
17
+ └─────────────┘ └─────────────┘
18
+ │ │
19
+ │ Score < 8? │
20
+ │◀───Feedback────────│
21
+ │ │
22
+ │ Score ≥ 8? │
23
+ │────APPROVED───────▶│
24
+ ```
25
+
26
+ You give it a task. The **executor** engine produces output. The **reviewer** engine scores it (1-10) and provides feedback. If the score doesn't meet the threshold, the feedback is fed back to the executor. This continues until the output is approved or max iterations are reached.
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Interactive guided setup
32
+ loop
33
+
34
+ # Direct execution
35
+ loop "Refactor the auth module to use JWT" -e claude -r gemini
36
+
37
+ # Auto mode with custom threshold
38
+ loop "Write comprehensive tests for utils/" -e gemini -r claude --auto --threshold 9
39
+
40
+ # Pass flags to the executor CLI
41
+ loop "Build the API endpoints" -e claude --pass --model opus
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **Multi-engine**: Claude CLI, Gemini CLI, Codex CLI — mix and match
47
+ - **Quality-scored iteration**: Automatic review loop with configurable threshold (1-10)
48
+ - **File-based event bus**: Append-only JSONL, crash-safe, zero external dependencies
49
+ - **Multi-agent orchestration**: Background daemon with IPC for coordinating agent teams
50
+ - **Skills system**: Executable markdown (`SKILL.md`) auto-injected into agent prompts
51
+ - **Interactive TUI**: Beautiful guided setup + real-time monitoring dashboard
52
+ - **Terminal adapters**: Terminal.app, iTerm2, tmux, built-in PTY
53
+ - **Decision tracking**: Architectural decisions persisted across sessions
54
+ - **Local-first**: Everything runs on your machine. No cloud services, no databases.
55
+
56
+ ## Commands
57
+
58
+ ```bash
59
+ loop [task] # Run iteration loop (interactive if no task)
60
+ loop daemon start # Start background daemon
61
+ loop daemon stop # Stop daemon
62
+ loop daemon status # Check daemon status
63
+ loop bus tail # Stream event bus
64
+ loop bus stats # Show bus statistics
65
+ loop chat # Open real-time dashboard
66
+ loop plan show # Show current iteration plan
67
+ loop plan clear # Clear plan
68
+ loop ctx add # Add architectural decision
69
+ loop ctx list # List decisions
70
+ loop skills list # List available skills
71
+ loop skills show <name> # Show skill content
72
+ ```
73
+
74
+ ## Options
75
+
76
+ | Flag | Description |
77
+ |------|-------------|
78
+ | `-e, --executor <engine>` | Executor engine: `claude` \| `gemini` \| `codex` |
79
+ | `-r, --reviewer <engine>` | Reviewer engine: `claude` \| `gemini` \| `codex` |
80
+ | `-n, --iterations <num>` | Max iterations (default: 5) |
81
+ | `-d, --dir <path>` | Working directory |
82
+ | `-v, --verbose` | Stream real-time output |
83
+ | `--auto` | Auto mode — skip manual conversation |
84
+ | `--pass <args...>` | Pass native flags to executor CLI |
85
+ | `--threshold <num>` | Approval score threshold, 1-10 (default: 8) |
86
+
87
+ ## How It Works
88
+
89
+ 1. **Configuration**: Reads `.loop/config.json` from your project (or uses defaults)
90
+ 2. **Engine selection**: Picks executor + reviewer from config or CLI flags
91
+ 3. **Iteration loop**:
92
+ - Executor receives the task (with prior feedback, if any)
93
+ - Executor produces output via PTY-based CLI session
94
+ - Output is sent to the reviewer with the LoopMessage v1 protocol
95
+ - Reviewer scores (1-10) and provides structured feedback
96
+ - If score meets threshold or "APPROVED" keyword: done
97
+ - Otherwise: feedback → executor → next iteration
98
+ 4. **Event bus**: All messages logged to append-only JSONL for crash recovery
99
+ 5. **Skills**: Relevant `SKILL.md` files injected into agent prompts for context
100
+
101
+ ## Configuration
102
+
103
+ Create `.loop/config.json` in your project:
104
+
105
+ ```json
106
+ {
107
+ "executor": "claude",
108
+ "reviewer": "gemini",
109
+ "maxIterations": 5,
110
+ "threshold": 8,
111
+ "verbose": false,
112
+ "auto": false
113
+ }
114
+ ```
115
+
116
+ ## Requirements
117
+
118
+ - Node.js 18+
119
+ - At least one AI CLI tool installed:
120
+ - [Claude CLI](https://docs.anthropic.com/en/docs/claude-cli)
121
+ - [Gemini CLI](https://github.com/google-gemini/gemini-cli)
122
+ - [Codex CLI](https://github.com/openai/codex)
123
+
124
+ ## Install from Source
125
+
126
+ ```bash
127
+ git clone https://github.com/lawrence3699/loop.git
128
+ cd loop
129
+ npm install
130
+ npm run build
131
+ npm link
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ActivityDetector — monitors a PtySession to determine the agent's
3
+ * current activity state.
4
+ *
5
+ * State machine:
6
+ *
7
+ * starting ──▶ working ◀──▶ idle
8
+ * │ ▲
9
+ * ▼ │
10
+ * waiting_input ──────┘
11
+ * │
12
+ * ▼
13
+ * blocked
14
+ *
15
+ * Ported from ufoo's activityDetector.js, adapted to listen on
16
+ * PtySession events rather than receiving raw processOutput calls.
17
+ */
18
+ import type { PtySession } from "./pty-session.js";
19
+ export type ActivityState = "idle" | "working" | "starting" | "waiting_input" | "blocked";
20
+ type StateChangeListener = (newState: ActivityState, oldState: ActivityState) => void;
21
+ export declare class ActivityDetector {
22
+ private _state;
23
+ private _since;
24
+ private _detail;
25
+ private _buffer;
26
+ private _listeners;
27
+ private _blockedTimer;
28
+ private _quietTimer;
29
+ private _quietToken;
30
+ private readonly _quietWindowMs;
31
+ private readonly _blockedTimeoutMs;
32
+ private readonly _agentType;
33
+ private readonly _onPtyData;
34
+ private readonly _onIdle;
35
+ private readonly _session;
36
+ constructor(ptySession: PtySession, agentType?: string, options?: {
37
+ quietWindowMs?: number;
38
+ blockedTimeoutMs?: number;
39
+ });
40
+ getState(): ActivityState;
41
+ /** Timestamp of the last state transition. */
42
+ get since(): number;
43
+ /** Detail string associated with the current state. */
44
+ get detail(): string;
45
+ onStateChange(listener: StateChangeListener): void;
46
+ destroy(): void;
47
+ private _setState;
48
+ /**
49
+ * Process raw PTY output: normalize, buffer, transition to WORKING,
50
+ * and schedule quiet-window classification.
51
+ */
52
+ private _processOutput;
53
+ private _markIdle;
54
+ private _scheduleQuietClassification;
55
+ private _classifyAfterQuiet;
56
+ private _tailWindow;
57
+ private _hasDeniedContext;
58
+ private _startBlockedTimer;
59
+ private _clearBlockedTimer;
60
+ private _clearQuietTimer;
61
+ private _normalize;
62
+ }
63
+ export {};
64
+ //# sourceMappingURL=activity.d.ts.map
@@ -0,0 +1,265 @@
1
+ /**
2
+ * ActivityDetector — monitors a PtySession to determine the agent's
3
+ * current activity state.
4
+ *
5
+ * State machine:
6
+ *
7
+ * starting ──▶ working ◀──▶ idle
8
+ * │ ▲
9
+ * ▼ │
10
+ * waiting_input ──────┘
11
+ * │
12
+ * ▼
13
+ * blocked
14
+ *
15
+ * Ported from ufoo's activityDetector.js, adapted to listen on
16
+ * PtySession events rather than receiving raw processOutput calls.
17
+ */
18
+ // ---------------------------------------------------------------------------
19
+ // Constants
20
+ // ---------------------------------------------------------------------------
21
+ /** Time to wait after last output before classifying as idle / waiting_input. */
22
+ const DEFAULT_QUIET_WINDOW_MS = 5_000;
23
+ /** Time in waiting_input before escalating to blocked. */
24
+ const DEFAULT_BLOCKED_TIMEOUT_MS = 300_000; // 5 min
25
+ /** How many tail characters to check for input prompts. */
26
+ const TAIL_BUFFER_SIZE = 4_000;
27
+ const TAIL_LINES = 10;
28
+ /** ANSI / OSC stripping patterns (for the rolling buffer). */
29
+ const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
30
+ const OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
31
+ // Agent-specific patterns that indicate the CLI is waiting for user input.
32
+ const INPUT_PATTERNS = {
33
+ "claude-code": [
34
+ /\bAllow\b.*\bDeny\b/,
35
+ /\ballow mcp\b/i,
36
+ /Enter to select.*\u2191\/\u2193 to navigate/,
37
+ ],
38
+ codex: [
39
+ /\[Y\/n\]/,
40
+ /\by\/n\b/i,
41
+ ],
42
+ };
43
+ const COMMON_INPUT_PATTERNS = [
44
+ /Continue\?\s*$/m,
45
+ /Proceed\?\s*$/m,
46
+ /Press enter/i,
47
+ /\(y\/n\)\s*:?\s*$/m,
48
+ ];
49
+ // Deny-list: per-line context that suppresses false positive prompt matches.
50
+ const LINE_DENY_PATTERNS = [
51
+ /function\s+\w+/,
52
+ /\/\//,
53
+ /import\s+/,
54
+ /require\s*\(/,
55
+ ];
56
+ // ---------------------------------------------------------------------------
57
+ // Implementation
58
+ // ---------------------------------------------------------------------------
59
+ export class ActivityDetector {
60
+ _state = "starting";
61
+ _since = Date.now();
62
+ _detail = "";
63
+ _buffer = "";
64
+ _listeners = [];
65
+ _blockedTimer = null;
66
+ _quietTimer = null;
67
+ _quietToken = 0;
68
+ _quietWindowMs;
69
+ _blockedTimeoutMs;
70
+ _agentType;
71
+ _onPtyData;
72
+ _onIdle;
73
+ _session;
74
+ constructor(ptySession, agentType, options) {
75
+ this._session = ptySession;
76
+ this._agentType = agentType ?? "";
77
+ this._quietWindowMs = options?.quietWindowMs ?? DEFAULT_QUIET_WINDOW_MS;
78
+ this._blockedTimeoutMs = options?.blockedTimeoutMs ?? DEFAULT_BLOCKED_TIMEOUT_MS;
79
+ // PTY data → mark working, buffer output, schedule classification
80
+ this._onPtyData = (data) => {
81
+ this._processOutput(data);
82
+ };
83
+ // Idle event → fast-path to idle state
84
+ this._onIdle = () => {
85
+ this._markIdle();
86
+ };
87
+ this._session.on("pty-data", this._onPtyData);
88
+ this._session.on("idle", this._onIdle);
89
+ }
90
+ // ── Public API ──────────────────────────────────────────────────────────
91
+ getState() {
92
+ return this._state;
93
+ }
94
+ /** Timestamp of the last state transition. */
95
+ get since() {
96
+ return this._since;
97
+ }
98
+ /** Detail string associated with the current state. */
99
+ get detail() {
100
+ return this._detail;
101
+ }
102
+ onStateChange(listener) {
103
+ this._listeners.push(listener);
104
+ }
105
+ destroy() {
106
+ this._clearBlockedTimer();
107
+ this._clearQuietTimer();
108
+ this._listeners = [];
109
+ this._session.removeListener("pty-data", this._onPtyData);
110
+ this._session.removeListener("idle", this._onIdle);
111
+ }
112
+ // ── Internal transitions ────────────────────────────────────────────────
113
+ _setState(newState, detail = "") {
114
+ if (newState === this._state)
115
+ return;
116
+ const oldState = this._state;
117
+ this._state = newState;
118
+ this._since = Date.now();
119
+ this._detail = detail;
120
+ for (const cb of this._listeners) {
121
+ try {
122
+ cb(newState, oldState);
123
+ }
124
+ catch {
125
+ // Ignore listener errors
126
+ }
127
+ }
128
+ }
129
+ /**
130
+ * Process raw PTY output: normalize, buffer, transition to WORKING,
131
+ * and schedule quiet-window classification.
132
+ */
133
+ _processOutput(raw) {
134
+ const normalized = this._normalize(raw);
135
+ if (!normalized)
136
+ return;
137
+ // Check for meaningful visible content
138
+ const visible = normalized.replace(/[\s\u0000-\u001F\u007F]+/g, "");
139
+ if (visible.length === 0)
140
+ return;
141
+ // STARTING → WORKING on first meaningful output
142
+ if (this._state === "starting") {
143
+ this._setState("working", "output");
144
+ }
145
+ // Append to rolling buffer
146
+ this._buffer += normalized;
147
+ if (this._buffer.length > TAIL_BUFFER_SIZE) {
148
+ this._buffer = this._buffer.slice(-TAIL_BUFFER_SIZE);
149
+ }
150
+ // Any output means WORKING (cancels prior waiting/blocked)
151
+ if (this._state !== "working") {
152
+ this._clearBlockedTimer();
153
+ this._setState("working");
154
+ }
155
+ this._scheduleQuietClassification();
156
+ }
157
+ _markIdle() {
158
+ if (this._state !== "working" &&
159
+ this._state !== "waiting_input" &&
160
+ this._state !== "blocked") {
161
+ return;
162
+ }
163
+ this._clearBlockedTimer();
164
+ this._clearQuietTimer();
165
+ this._buffer = "";
166
+ this._setState("idle");
167
+ }
168
+ // ── Quiet-window classification ─────────────────────────────────────────
169
+ _scheduleQuietClassification() {
170
+ this._quietToken += 1;
171
+ const token = this._quietToken;
172
+ this._clearQuietTimer();
173
+ this._quietTimer = setTimeout(() => {
174
+ if (token !== this._quietToken)
175
+ return;
176
+ this._quietTimer = null;
177
+ this._classifyAfterQuiet();
178
+ }, this._quietWindowMs);
179
+ if (this._quietTimer && typeof this._quietTimer.unref === "function") {
180
+ this._quietTimer.unref();
181
+ }
182
+ }
183
+ _classifyAfterQuiet() {
184
+ if (this._state !== "working")
185
+ return;
186
+ const tail = this._tailWindow();
187
+ // Check agent-specific and common prompt patterns
188
+ const agentPatterns = INPUT_PATTERNS[this._agentType] ?? [];
189
+ const allPatterns = [...agentPatterns, ...COMMON_INPUT_PATTERNS];
190
+ for (const pattern of allPatterns) {
191
+ const match = pattern.exec(tail);
192
+ if (!match)
193
+ continue;
194
+ const matchedText = match[0] ?? "";
195
+ const matchIndex = Number.isFinite(match.index)
196
+ ? match.index
197
+ : Math.max(0, tail.length - matchedText.length);
198
+ if (this._hasDeniedContext(tail, matchIndex, matchedText.length))
199
+ continue;
200
+ this._setState("waiting_input", pattern.source);
201
+ this._startBlockedTimer();
202
+ return;
203
+ }
204
+ // No input patterns matched → idle
205
+ this._setState("idle");
206
+ }
207
+ _tailWindow() {
208
+ if (!this._buffer)
209
+ return "";
210
+ const lines = this._buffer.split("\n");
211
+ if (lines.length <= TAIL_LINES)
212
+ return this._buffer;
213
+ return lines.slice(-TAIL_LINES).join("\n");
214
+ }
215
+ _hasDeniedContext(haystack, matchIndex, matchLength) {
216
+ // Inside a code fence?
217
+ const before = haystack.slice(0, Math.max(0, matchIndex));
218
+ const fences = before.match(/```/g);
219
+ if ((fences ? fences.length : 0) % 2 === 1)
220
+ return true;
221
+ // Check surrounding line against deny patterns
222
+ const center = Math.max(0, matchIndex + Math.max(0, Math.trunc(matchLength / 2)));
223
+ const lineStart = haystack.lastIndexOf("\n", center - 1) + 1;
224
+ const lineEndCandidate = haystack.indexOf("\n", center);
225
+ const lineEnd = lineEndCandidate >= 0 ? lineEndCandidate : haystack.length;
226
+ const line = haystack.slice(lineStart, lineEnd);
227
+ return LINE_DENY_PATTERNS.some((deny) => deny.test(line));
228
+ }
229
+ // ── Timers ──────────────────────────────────────────────────────────────
230
+ _startBlockedTimer() {
231
+ this._clearBlockedTimer();
232
+ this._blockedTimer = setTimeout(() => {
233
+ this._blockedTimer = null;
234
+ if (this._state === "waiting_input") {
235
+ this._setState("blocked", `waiting_input for ${this._blockedTimeoutMs}ms`);
236
+ }
237
+ }, this._blockedTimeoutMs);
238
+ if (this._blockedTimer && typeof this._blockedTimer.unref === "function") {
239
+ this._blockedTimer.unref();
240
+ }
241
+ }
242
+ _clearBlockedTimer() {
243
+ if (this._blockedTimer) {
244
+ clearTimeout(this._blockedTimer);
245
+ this._blockedTimer = null;
246
+ }
247
+ }
248
+ _clearQuietTimer() {
249
+ if (this._quietTimer) {
250
+ clearTimeout(this._quietTimer);
251
+ this._quietTimer = null;
252
+ }
253
+ }
254
+ // ── Helpers ─────────────────────────────────────────────────────────────
255
+ _normalize(text) {
256
+ if (!text)
257
+ return "";
258
+ return String(text)
259
+ .replace(OSC_RE, "")
260
+ .replace(/\r\n/g, "\n")
261
+ .replace(/\r/g, "\n")
262
+ .replace(ANSI_RE, "");
263
+ }
264
+ }
265
+ //# sourceMappingURL=activity.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AgentLauncher — unified agent lifecycle manager.
3
+ *
4
+ * Handles:
5
+ * 1. Ensuring the .loop/ project directory exists
6
+ * 2. Detecting (or using specified) launch mode
7
+ * 3. Spawning a PtySession via the appropriate terminal adapter
8
+ * 4. Setting up ReadyDetector + ActivityDetector
9
+ * 5. Registering with the daemon via IPC (fail-silently if no daemon)
10
+ * 6. SIGTERM / SIGINT cleanup
11
+ *
12
+ * Simplified port of ufoo's launcher.js.
13
+ */
14
+ import { PtySession } from "./pty-session.js";
15
+ import { ActivityDetector } from "./activity.js";
16
+ import { ReadyDetector } from "./ready-detector.js";
17
+ import { type LaunchMode } from "../terminal/detect.js";
18
+ export interface LaunchOptions {
19
+ agentType: string;
20
+ command: string;
21
+ args: string[];
22
+ cwd: string;
23
+ launchMode?: LaunchMode;
24
+ nickname?: string;
25
+ env?: Record<string, string>;
26
+ }
27
+ export interface LaunchedAgent {
28
+ subscriberId: string;
29
+ ptySession: PtySession;
30
+ activityDetector: ActivityDetector;
31
+ readyDetector: ReadyDetector;
32
+ cleanup: () => Promise<void>;
33
+ }
34
+ export declare class AgentLauncher {
35
+ private readonly _projectRoot;
36
+ constructor(projectRoot: string);
37
+ /**
38
+ * Launch an agent, returning a handle with the PtySession and detectors.
39
+ */
40
+ launch(opts: LaunchOptions): Promise<LaunchedAgent>;
41
+ }
42
+ //# sourceMappingURL=launcher.d.ts.map