@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
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
|