@glrs-dev/cli 2.4.0 → 2.6.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 +40 -0
- package/dist/{chunk-HQUCVJ4G.js → chunk-FBXSGZAA.js} +4 -0
- package/dist/chunk-J3FXSHMA.js +263 -0
- package/dist/{chunk-5ZVUFNCP.js → chunk-S6N5E2GG.js} +8 -1
- package/dist/{chunk-2VMFXAJH.js → chunk-UO7WHIKY.js} +18 -5
- package/dist/cli.js +10 -3
- package/dist/commands/autopilot-tui.d.ts +11 -1
- package/dist/commands/autopilot-tui.js +2 -1
- package/dist/commands/autopilot.d.ts +2 -0
- package/dist/commands/autopilot.js +62 -21
- package/dist/commands/debrief.d.ts +2 -0
- package/dist/commands/debrief.js +1 -1
- package/dist/commands/loop.d.ts +2 -0
- package/dist/commands/loop.js +33 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.d.ts +270 -0
- package/dist/node_modules/@glrs-dev/adapter-opencode/dist/index.js +506 -0
- package/dist/node_modules/@glrs-dev/adapter-opencode/package.json +8 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/auto-ship-EVLBKHUZ.js +7 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/changeset-generator-HAHYSSUR.js +15 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2X3CWH47.js +3288 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-2ZQ6SBV3.js +70 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-6JZQLIRP.js +781 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-AWRK6S6G.js +91 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-BLEIZHET.js +101 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-GXXCEGDD.js +251 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/chunk-S34HOCZ4.js +44 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/index.d.ts +1915 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/index.js +768 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/logger-3XLFMXLN.js +8 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/loop-session-YLCVJGPV.js +9 -0
- package/dist/node_modules/@glrs-dev/autopilot/dist/plan-enrichment-4SQYV5FC.js +17 -0
- package/dist/node_modules/@glrs-dev/autopilot/package.json +8 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/agents-md-writer.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/architecture-advisor.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-searcher.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/docs-maintainer.md +0 -8
- package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +1 -3
- package/dist/vendor/harness-opencode/dist/agents/prompts/lib-reader.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +0 -2
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +1 -1
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +78 -262
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +5 -14
- package/dist/vendor/harness-opencode/dist/agents/prompts/scoper.md +7 -2
- package/dist/vendor/harness-opencode/dist/autopilot/strategies/default.md +29 -0
- package/dist/vendor/harness-opencode/dist/index.js +112 -82
- package/dist/vendor/harness-opencode/package.json +1 -1
- package/package.json +9 -7
|
@@ -0,0 +1,1915 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { Logger } from 'pino';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AgentAdapter — abstract interface for driving an AI agent programmatically.
|
|
7
|
+
*
|
|
8
|
+
* Decouples the autopilot loop engine from any specific agent implementation
|
|
9
|
+
* (OpenCode, Claude Code, etc.). The concrete adapter is injected at runtime
|
|
10
|
+
* by the CLI command.
|
|
11
|
+
*/
|
|
12
|
+
interface AgentHandle {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
}
|
|
15
|
+
interface SessionResult$1 {
|
|
16
|
+
kind: "idle" | "error" | "stall" | "abort" | "question_rejected";
|
|
17
|
+
message?: string;
|
|
18
|
+
stallMs?: number;
|
|
19
|
+
title?: string;
|
|
20
|
+
}
|
|
21
|
+
interface AgentAdapter {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
start(opts: {
|
|
24
|
+
cwd: string;
|
|
25
|
+
agents?: Record<string, unknown>;
|
|
26
|
+
}): Promise<AgentHandle>;
|
|
27
|
+
createSession(handle: AgentHandle, opts: {
|
|
28
|
+
agentName?: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
}): Promise<string>;
|
|
31
|
+
sendAndWait(handle: AgentHandle, opts: {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
message: string;
|
|
34
|
+
stallMs?: number;
|
|
35
|
+
abortSignal?: AbortSignal;
|
|
36
|
+
onToolCall?: (name: string, arg?: string) => void;
|
|
37
|
+
onTextDelta?: (chars: number) => void;
|
|
38
|
+
onCostUpdate?: (cost: number, tokens: {
|
|
39
|
+
input: number;
|
|
40
|
+
output: number;
|
|
41
|
+
}) => void;
|
|
42
|
+
}): Promise<SessionResult$1>;
|
|
43
|
+
getLastResponse(handle: AgentHandle, sessionId: string): Promise<string>;
|
|
44
|
+
getSessionCost(handle: AgentHandle, sessionId: string): Promise<number>;
|
|
45
|
+
/** Return cost + token totals for a session. Optional — callers fall back to getSessionCost when absent. */
|
|
46
|
+
getSessionStats?(handle: AgentHandle, sessionId: string): Promise<{
|
|
47
|
+
cost: number;
|
|
48
|
+
tokensIn: number;
|
|
49
|
+
tokensOut: number;
|
|
50
|
+
}>;
|
|
51
|
+
shutdown(handle: AgentHandle): Promise<void>;
|
|
52
|
+
enhanceError?(message: string): Promise<string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Typed SessionEvent discriminated union for the autopilot event stream.
|
|
57
|
+
*
|
|
58
|
+
* Every event has `type` (the discriminant) and `timestamp` (ISO 8601 string).
|
|
59
|
+
* Consumers narrow to a specific variant via the exported type guards.
|
|
60
|
+
*
|
|
61
|
+
* Channel 1: EventEmitter (in-process, consumed by CLI renderer)
|
|
62
|
+
* Channel 2: NDJSON file at .agent/autopilot-events.jsonl (consumed by TUI / --status)
|
|
63
|
+
*/
|
|
64
|
+
interface SessionStartEvent {
|
|
65
|
+
type: "session:start";
|
|
66
|
+
timestamp: string;
|
|
67
|
+
planPath: string;
|
|
68
|
+
cwd: string;
|
|
69
|
+
fast: boolean;
|
|
70
|
+
resume: boolean;
|
|
71
|
+
enrichModel?: string;
|
|
72
|
+
executeModel?: string;
|
|
73
|
+
adapter?: string;
|
|
74
|
+
branch?: string;
|
|
75
|
+
}
|
|
76
|
+
interface SessionDoneEvent {
|
|
77
|
+
type: "session:done";
|
|
78
|
+
timestamp: string;
|
|
79
|
+
exitReason: string;
|
|
80
|
+
iterations: number;
|
|
81
|
+
cumulativeCostUsd?: number;
|
|
82
|
+
message: string;
|
|
83
|
+
}
|
|
84
|
+
interface EnrichStartEvent {
|
|
85
|
+
type: "enrich:start";
|
|
86
|
+
timestamp: string;
|
|
87
|
+
planPath: string;
|
|
88
|
+
fileCount: number;
|
|
89
|
+
}
|
|
90
|
+
interface EnrichFileStartEvent {
|
|
91
|
+
type: "enrich:file:start";
|
|
92
|
+
timestamp: string;
|
|
93
|
+
file: string;
|
|
94
|
+
}
|
|
95
|
+
interface EnrichFileDoneEvent {
|
|
96
|
+
type: "enrich:file:done";
|
|
97
|
+
timestamp: string;
|
|
98
|
+
file: string;
|
|
99
|
+
toolCalls: number;
|
|
100
|
+
specFile?: string;
|
|
101
|
+
}
|
|
102
|
+
interface EnrichFileSkipEvent {
|
|
103
|
+
type: "enrich:file:skip";
|
|
104
|
+
timestamp: string;
|
|
105
|
+
file: string;
|
|
106
|
+
reason: string;
|
|
107
|
+
}
|
|
108
|
+
interface EnrichFileErrorEvent {
|
|
109
|
+
type: "enrich:file:error";
|
|
110
|
+
timestamp: string;
|
|
111
|
+
file: string;
|
|
112
|
+
error: string;
|
|
113
|
+
}
|
|
114
|
+
interface EnrichDoneEvent {
|
|
115
|
+
type: "enrich:done";
|
|
116
|
+
timestamp: string;
|
|
117
|
+
filesProcessed: number;
|
|
118
|
+
}
|
|
119
|
+
interface PhaseStartEvent {
|
|
120
|
+
type: "phase:start";
|
|
121
|
+
timestamp: string;
|
|
122
|
+
phase: string;
|
|
123
|
+
laneId: string;
|
|
124
|
+
current: number;
|
|
125
|
+
total: number;
|
|
126
|
+
}
|
|
127
|
+
interface PhaseDoneEvent {
|
|
128
|
+
type: "phase:done";
|
|
129
|
+
timestamp: string;
|
|
130
|
+
phase: string;
|
|
131
|
+
laneId: string;
|
|
132
|
+
completed: boolean;
|
|
133
|
+
iterations: number;
|
|
134
|
+
costUsd: number;
|
|
135
|
+
}
|
|
136
|
+
interface IterationStartEvent {
|
|
137
|
+
type: "iteration:start";
|
|
138
|
+
timestamp: string;
|
|
139
|
+
iteration: number;
|
|
140
|
+
maxIterations: number;
|
|
141
|
+
laneId?: string;
|
|
142
|
+
}
|
|
143
|
+
interface IterationDoneEvent {
|
|
144
|
+
type: "iteration:done";
|
|
145
|
+
timestamp: string;
|
|
146
|
+
iteration: number;
|
|
147
|
+
durationMs: number;
|
|
148
|
+
madeProgress: boolean;
|
|
149
|
+
filesChanged?: number;
|
|
150
|
+
commitSubject?: string;
|
|
151
|
+
costUsd?: number;
|
|
152
|
+
cumulativeCostUsd?: number;
|
|
153
|
+
laneId?: string;
|
|
154
|
+
}
|
|
155
|
+
interface ToolCallEvent {
|
|
156
|
+
type: "tool:call";
|
|
157
|
+
timestamp: string;
|
|
158
|
+
toolName: string;
|
|
159
|
+
firstArg?: string;
|
|
160
|
+
iteration: number;
|
|
161
|
+
laneId?: string;
|
|
162
|
+
}
|
|
163
|
+
interface CostUpdateEvent {
|
|
164
|
+
type: "cost:update";
|
|
165
|
+
timestamp: string;
|
|
166
|
+
cumulativeCostUsd: number;
|
|
167
|
+
isEstimated: boolean;
|
|
168
|
+
iteration: number;
|
|
169
|
+
tokensIn?: number;
|
|
170
|
+
tokensOut?: number;
|
|
171
|
+
}
|
|
172
|
+
interface ThinkingEvent {
|
|
173
|
+
type: "thinking";
|
|
174
|
+
timestamp: string;
|
|
175
|
+
iteration: number;
|
|
176
|
+
/** Cumulative characters streamed in the current reasoning block. */
|
|
177
|
+
chars: number;
|
|
178
|
+
/** Seconds since the last tool call (or iteration start). */
|
|
179
|
+
elapsedSec: number;
|
|
180
|
+
laneId?: string;
|
|
181
|
+
}
|
|
182
|
+
interface ErrorEvent {
|
|
183
|
+
type: "error";
|
|
184
|
+
timestamp: string;
|
|
185
|
+
message: string;
|
|
186
|
+
iteration?: number;
|
|
187
|
+
phase?: string;
|
|
188
|
+
}
|
|
189
|
+
interface CredentialExpiredEvent {
|
|
190
|
+
type: "credential:expired";
|
|
191
|
+
timestamp: string;
|
|
192
|
+
provider: string;
|
|
193
|
+
message: string;
|
|
194
|
+
iteration: number;
|
|
195
|
+
}
|
|
196
|
+
interface VerifyStartEvent {
|
|
197
|
+
type: "verify:start";
|
|
198
|
+
timestamp: string;
|
|
199
|
+
phase: string;
|
|
200
|
+
itemCount: number;
|
|
201
|
+
}
|
|
202
|
+
interface VerifyResultEvent {
|
|
203
|
+
type: "verify:result";
|
|
204
|
+
timestamp: string;
|
|
205
|
+
phase: string;
|
|
206
|
+
itemId: string;
|
|
207
|
+
command: string;
|
|
208
|
+
passed: boolean;
|
|
209
|
+
stderr?: string;
|
|
210
|
+
}
|
|
211
|
+
interface VerifyDoneEvent {
|
|
212
|
+
type: "verify:done";
|
|
213
|
+
timestamp: string;
|
|
214
|
+
phase: string;
|
|
215
|
+
passed: number;
|
|
216
|
+
failed: number;
|
|
217
|
+
}
|
|
218
|
+
type SessionEvent = SessionStartEvent | SessionDoneEvent | EnrichStartEvent | EnrichFileStartEvent | EnrichFileDoneEvent | EnrichFileSkipEvent | EnrichFileErrorEvent | EnrichDoneEvent | PhaseStartEvent | PhaseDoneEvent | IterationStartEvent | IterationDoneEvent | ToolCallEvent | CostUpdateEvent | ThinkingEvent | ErrorEvent | CredentialExpiredEvent | VerifyStartEvent | VerifyResultEvent | VerifyDoneEvent;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* NDJSON event stream writer and reader for the autopilot event stream.
|
|
222
|
+
*
|
|
223
|
+
* Channel 2: .agent/autopilot-events.jsonl
|
|
224
|
+
*
|
|
225
|
+
* Writer: append-only, sync writes (crash-safe).
|
|
226
|
+
* Reader: full read or tail mode (byte offset).
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Append-only NDJSON writer. Opens the file for append on construction,
|
|
231
|
+
* writes each event as a JSON line synchronously (crash-safe), and closes
|
|
232
|
+
* the file descriptor on `close()`.
|
|
233
|
+
*/
|
|
234
|
+
declare class EventStreamWriter {
|
|
235
|
+
private fd;
|
|
236
|
+
constructor(filePath: string);
|
|
237
|
+
/**
|
|
238
|
+
* Serialize `event` as a JSON line and write it synchronously.
|
|
239
|
+
* Sync write ensures the line is on disk even if the process crashes
|
|
240
|
+
* immediately after.
|
|
241
|
+
*/
|
|
242
|
+
emit(event: SessionEvent): void;
|
|
243
|
+
/**
|
|
244
|
+
* Close the underlying file descriptor. Safe to call multiple times
|
|
245
|
+
* (subsequent calls are no-ops).
|
|
246
|
+
*/
|
|
247
|
+
close(): void;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* NDJSON reader for the autopilot event stream.
|
|
251
|
+
*
|
|
252
|
+
* Supports full reads and tail mode (byte offset). Truncated or malformed
|
|
253
|
+
* last lines are silently skipped — they indicate a crash mid-write.
|
|
254
|
+
*/
|
|
255
|
+
declare class EventStreamReader {
|
|
256
|
+
private readonly filePath;
|
|
257
|
+
constructor(filePath: string);
|
|
258
|
+
/**
|
|
259
|
+
* Read all events from the file. Returns an empty array if the file
|
|
260
|
+
* does not exist or is empty.
|
|
261
|
+
*/
|
|
262
|
+
readAll(): SessionEvent[];
|
|
263
|
+
/**
|
|
264
|
+
* Read events starting at `byteOffset`. Returns the parsed events and
|
|
265
|
+
* the new byte offset (= file size after reading). Useful for polling
|
|
266
|
+
* (tail mode): pass the returned `newOffset` on the next call to read
|
|
267
|
+
* only new events.
|
|
268
|
+
*
|
|
269
|
+
* Truncated or malformed last lines are skipped.
|
|
270
|
+
*/
|
|
271
|
+
readFrom(byteOffset: number): {
|
|
272
|
+
events: SessionEvent[];
|
|
273
|
+
newOffset: number;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Plan-parser module for the autopilot engine.
|
|
279
|
+
*
|
|
280
|
+
* Parses both single-file plans (one .md file with checkboxes or a
|
|
281
|
+
* plan-state fence) and multi-file plans (a directory containing
|
|
282
|
+
* main.md + phase_N.md files). Returns structured progress data.
|
|
283
|
+
*
|
|
284
|
+
* Never throws — all errors degrade to zero-count results so the
|
|
285
|
+
* loop's heartbeat can fall back to plan-blind mode safely.
|
|
286
|
+
*/
|
|
287
|
+
interface PlanPhase {
|
|
288
|
+
file: string;
|
|
289
|
+
totalItems: number;
|
|
290
|
+
checkedItems: number;
|
|
291
|
+
}
|
|
292
|
+
interface PlanState {
|
|
293
|
+
type: "single" | "multi";
|
|
294
|
+
totalItems: number;
|
|
295
|
+
checkedItems: number;
|
|
296
|
+
phaseCount: number;
|
|
297
|
+
phasesCompleted: number;
|
|
298
|
+
phases: PlanPhase[];
|
|
299
|
+
}
|
|
300
|
+
interface PlanFileEntry {
|
|
301
|
+
path: string;
|
|
302
|
+
isNew: boolean;
|
|
303
|
+
change: string;
|
|
304
|
+
}
|
|
305
|
+
interface PlanItem {
|
|
306
|
+
id: string;
|
|
307
|
+
intent: string;
|
|
308
|
+
files: PlanFileEntry[];
|
|
309
|
+
tests: string[];
|
|
310
|
+
verify: string;
|
|
311
|
+
checked: boolean;
|
|
312
|
+
proof?: string;
|
|
313
|
+
proof_type?: string;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Parse a plan at the given path and return structured progress data.
|
|
317
|
+
*
|
|
318
|
+
* - If `planPath` is a directory containing `main.md`, it's treated as
|
|
319
|
+
* a multi-file plan.
|
|
320
|
+
* - If `planPath` is a `.md` file, it's treated as a single-file plan.
|
|
321
|
+
* - Any error (missing file, parse failure, etc.) returns a degraded
|
|
322
|
+
* result with zero counts — never throws.
|
|
323
|
+
*/
|
|
324
|
+
declare function parsePlanState(planPath: string): PlanState;
|
|
325
|
+
/**
|
|
326
|
+
* Parse individual plan items from a plan-state fence block.
|
|
327
|
+
*
|
|
328
|
+
* Each item starts with `- [ ] id:` or `- [x] id:` and contains
|
|
329
|
+
* optional `intent:`, `files:`, `tests:`, and `verify:` fields.
|
|
330
|
+
*
|
|
331
|
+
* Returns an empty array if no fence is found or the content is malformed.
|
|
332
|
+
* Never throws.
|
|
333
|
+
*/
|
|
334
|
+
declare function parseItems(content: string): PlanItem[];
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Verify-command runner for the post-phase test gate (item 4.1).
|
|
338
|
+
*
|
|
339
|
+
* After a phase reports `phaseComplete === true` (every checkbox checked),
|
|
340
|
+
* the orchestrator runs each plan-state item's `verify:` command in a
|
|
341
|
+
* child process and reports pass/fail. Phases whose verify commands
|
|
342
|
+
* fail are treated as incomplete — the failure is fed back into the
|
|
343
|
+
* next iteration's prompt so the agent can fix it.
|
|
344
|
+
*
|
|
345
|
+
* Each command runs via `/bin/sh -c` so users can write shell features
|
|
346
|
+
* (pipes, redirects, env vars) into a `verify:` field. Per-command
|
|
347
|
+
* timeout is determined by the item's proof_type:
|
|
348
|
+
* - unit_test: 30s, api_check: 10s, structural/typecheck: 60s, e2e: 120s
|
|
349
|
+
* - unknown/custom: uses config.verify_timeout (default 5 minutes).
|
|
350
|
+
* On timeout we synthesize a stderr message and return `passed: false`
|
|
351
|
+
* rather than throwing — `runVerifyCommands` always returns a
|
|
352
|
+
* `VerifyResult[]` so callers can build a result table without wrapping
|
|
353
|
+
* every call in try/catch.
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
declare const execFileDefault$3: typeof execFile.__promisify__;
|
|
357
|
+
/** Result of running a single `verify:` command. */
|
|
358
|
+
interface VerifyResult {
|
|
359
|
+
/** Plan-state item id (e.g. "4.1"). */
|
|
360
|
+
itemId: string;
|
|
361
|
+
/** The exact command string that was run. */
|
|
362
|
+
command: string;
|
|
363
|
+
/** True iff the child process exited with code 0 within the timeout. */
|
|
364
|
+
passed: boolean;
|
|
365
|
+
stdout: string;
|
|
366
|
+
stderr: string;
|
|
367
|
+
/** Wall-clock duration of the command in milliseconds. */
|
|
368
|
+
durationMs: number;
|
|
369
|
+
}
|
|
370
|
+
interface RunVerifyCommandsOptions {
|
|
371
|
+
/** Per-command timeout in milliseconds (default: 5 minutes). */
|
|
372
|
+
timeoutMs?: number;
|
|
373
|
+
/**
|
|
374
|
+
* Test-only: inject execFile replacement.
|
|
375
|
+
* @internal
|
|
376
|
+
*/
|
|
377
|
+
_deps?: {
|
|
378
|
+
execFile?: typeof execFileDefault$3;
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Run each item's `verify:` command in `cwd` and return the results.
|
|
383
|
+
*
|
|
384
|
+
* Items without a `verify:` field are skipped (no result emitted). The
|
|
385
|
+
* function never throws — every executed command yields exactly one
|
|
386
|
+
* `VerifyResult`, including timeouts and spawn failures.
|
|
387
|
+
*/
|
|
388
|
+
declare function runVerifyCommands(items: PlanItem[], cwd: string, opts?: RunVerifyCommandsOptions): Promise<VerifyResult[]>;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Structured logger for the autopilot CLI.
|
|
392
|
+
*
|
|
393
|
+
* Single sink: **file** at trace level (captures everything, always).
|
|
394
|
+
* JSON — files are for grep/telemetry/debugging, not eyes.
|
|
395
|
+
* Default path: `<cwd>/.agent/autopilot-logs/<ISO-timestamp>.log`.
|
|
396
|
+
* Override via GLRS_AUTOPILOT_LOG_FILE=<path> or "off" to disable.
|
|
397
|
+
*
|
|
398
|
+
* User-facing output goes to stderr via plain `process.stderr.write`
|
|
399
|
+
* in the calling code (TUI-style). Pino does NOT write to stderr —
|
|
400
|
+
* the two channels are separate by design.
|
|
401
|
+
*
|
|
402
|
+
* Environment variables:
|
|
403
|
+
* - GLRS_AUTOPILOT_LOG_FILE: explicit path, or "off" to disable file sink
|
|
404
|
+
*/
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Per-run logger state. Created once per autopilot invocation.
|
|
408
|
+
*/
|
|
409
|
+
interface AutopilotLogger {
|
|
410
|
+
/** The pino root logger. Call .child({ component }) for module-level loggers. */
|
|
411
|
+
root: Logger;
|
|
412
|
+
/** Path to the file log sink, if enabled. */
|
|
413
|
+
logFilePath: string | null;
|
|
414
|
+
/** Flush all streams. Call before process exit. */
|
|
415
|
+
flush: () => Promise<void>;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Create the autopilot logger with file sink only.
|
|
419
|
+
* Call ONCE per autopilot run. Pass `cwd` so the default file path
|
|
420
|
+
* resolves relative to the user's project.
|
|
421
|
+
*
|
|
422
|
+
* User-facing output goes to stderr via process.stderr.write in the
|
|
423
|
+
* calling code. Pino is for structured file logging only.
|
|
424
|
+
*
|
|
425
|
+
* @deprecated The autopilot code path now uses the typed SessionEvent stream
|
|
426
|
+
* (session-events.ts + event-stream.ts) instead of pino for user-visible output.
|
|
427
|
+
* This function is kept for non-autopilot consumers and as a verbose debug
|
|
428
|
+
* channel inside the loop engine. New code should use SessionRunner + EventStreamWriter.
|
|
429
|
+
*/
|
|
430
|
+
declare function createAutopilotLogger(opts: {
|
|
431
|
+
cwd: string;
|
|
432
|
+
level?: string;
|
|
433
|
+
}): AutopilotLogger;
|
|
434
|
+
/**
|
|
435
|
+
* Convenience factory for module-level loggers when the root logger is
|
|
436
|
+
* already created. Use this inside modules that receive a root logger
|
|
437
|
+
* from the caller:
|
|
438
|
+
*
|
|
439
|
+
* const log = childLogger(root, "autopilot.loop");
|
|
440
|
+
* log.info("hello"); // emits with component: "autopilot.loop"
|
|
441
|
+
*/
|
|
442
|
+
declare function childLogger(root: Logger, component: string): Logger;
|
|
443
|
+
|
|
444
|
+
type LoopExitReason = "sentinel" | "max-iterations" | "timeout" | "struggle" | "kill-switch" | "stall" | "error" | "aborted";
|
|
445
|
+
interface LoopResult {
|
|
446
|
+
exitReason: LoopExitReason;
|
|
447
|
+
iterations: number;
|
|
448
|
+
message: string;
|
|
449
|
+
/** The OpenCode session ID for the loop session. Used by the debrief module. */
|
|
450
|
+
sessionId?: string;
|
|
451
|
+
/** Cumulative cost in USD for the loop session. Used by the debrief module. */
|
|
452
|
+
cumulativeCostUsd?: number;
|
|
453
|
+
/**
|
|
454
|
+
* Per-phase cost breakdown for multi-phase plans.
|
|
455
|
+
* Populated by the multi-phase runner (loop-session.ts) when available.
|
|
456
|
+
*/
|
|
457
|
+
phaseBreakdown?: Array<{
|
|
458
|
+
phaseFile: string;
|
|
459
|
+
iterations: number;
|
|
460
|
+
costUsd: number;
|
|
461
|
+
}>;
|
|
462
|
+
/**
|
|
463
|
+
* Per-lane cost breakdown for parallel-lane runs (item 3.5).
|
|
464
|
+
* Populated by `loop-session.ts` only when `--parallel > 1` produced
|
|
465
|
+
* a real parallel run. `Map<laneId, costUsd>`. Optional to preserve
|
|
466
|
+
* backward compatibility for callers that never enabled parallelism.
|
|
467
|
+
*/
|
|
468
|
+
laneCosts?: Map<string, number>;
|
|
469
|
+
/**
|
|
470
|
+
* Surviving worktree paths from a partially-failed parallel run
|
|
471
|
+
* (item 3.6). When a lane's merge failed, its worktree is left on
|
|
472
|
+
* disk and the path is reported here so the user can manually
|
|
473
|
+
* `git worktree remove --force <path>`. Empty/undefined on clean runs.
|
|
474
|
+
*/
|
|
475
|
+
orphanedWorktrees?: string[];
|
|
476
|
+
/**
|
|
477
|
+
* Per-phase verify-command results from the post-phase test gate
|
|
478
|
+
* (item 4.1). Populated by `loop-session.ts` after each phase runs
|
|
479
|
+
* its `verify:` commands (one per plan-state item). When any verify
|
|
480
|
+
* command fails the phase is treated as incomplete (its main.md
|
|
481
|
+
* checkbox is NOT marked) and the failures are surfaced here so the
|
|
482
|
+
* debrief can render a results table. Optional to preserve back-
|
|
483
|
+
* compat for callers that never enabled verify gating.
|
|
484
|
+
*/
|
|
485
|
+
verifyResults?: Array<{
|
|
486
|
+
phaseFile: string;
|
|
487
|
+
results: VerifyResult[];
|
|
488
|
+
}>;
|
|
489
|
+
/**
|
|
490
|
+
* Path to the changeset file generated after all phases passed
|
|
491
|
+
* (item 4.6). Absent when generation was skipped (test deps in
|
|
492
|
+
* use, no phases run, or a prior failure short-circuited the run).
|
|
493
|
+
*/
|
|
494
|
+
changesetPath?: string;
|
|
495
|
+
/**
|
|
496
|
+
* URL of the PR opened by --ship (item 4.7). Absent when --ship was
|
|
497
|
+
* not passed or auto-ship failed. The hard-rule guards (no force-
|
|
498
|
+
* push, no main/master push, no merge) live inside `auto-ship.ts`.
|
|
499
|
+
*/
|
|
500
|
+
prUrl?: string;
|
|
501
|
+
/**
|
|
502
|
+
* The agent adapter + handle, kept alive for the caller to reuse
|
|
503
|
+
* (e.g., for the debrief). The caller is responsible for calling
|
|
504
|
+
* `adapter.shutdown(handle)` when done. Present only when `keepAlive` is
|
|
505
|
+
* true in RalphLoopOptions (default: false — adapter is shut down
|
|
506
|
+
* in the finally block).
|
|
507
|
+
*/
|
|
508
|
+
agentHandle?: {
|
|
509
|
+
adapter: AgentAdapter;
|
|
510
|
+
handle: AgentHandle;
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
interface RalphLoopOptions {
|
|
514
|
+
/** The prompt to send to PRIME each iteration. */
|
|
515
|
+
prompt: string;
|
|
516
|
+
/** Working directory for the OpenCode server. */
|
|
517
|
+
cwd: string;
|
|
518
|
+
/** Agent name for the session (default: "autopilot-prime"). */
|
|
519
|
+
agentName?: string;
|
|
520
|
+
/** Maximum number of iterations (default: MAX_ITERATIONS). */
|
|
521
|
+
maxIterations?: number;
|
|
522
|
+
/** Total wall-clock timeout in ms (default: TIMEOUT_MS). */
|
|
523
|
+
timeoutMs?: number;
|
|
524
|
+
/** Per-iteration stall timeout in ms (default: STALL_MS). */
|
|
525
|
+
stallMs?: number;
|
|
526
|
+
/** Struggle threshold — consecutive zero-progress iterations (default: STRUGGLE_THRESHOLD). */
|
|
527
|
+
struggleThreshold?: number;
|
|
528
|
+
/**
|
|
529
|
+
* Optional lane id for parallel-lane execution (item 3.4). When set,
|
|
530
|
+
* all childLogger names are scoped under `autopilot.loop.lane.<id>`
|
|
531
|
+
* (and tool/stream/status equivalents) so pino emits a `name` field
|
|
532
|
+
* the user-facing formatter can prefix as `[lane-N]`. Status snapshots
|
|
533
|
+
* also include this id so status.ts can render multi-lane state.
|
|
534
|
+
* When unset (default), logging is scoped at `autopilot.loop` as before.
|
|
535
|
+
*/
|
|
536
|
+
laneId?: string;
|
|
537
|
+
/**
|
|
538
|
+
* Webhook URL to POST lifecycle events to (optional).
|
|
539
|
+
* Supports plain webhooks and Slack incoming webhooks (auto-detected).
|
|
540
|
+
*/
|
|
541
|
+
notifyUrl?: string;
|
|
542
|
+
/**
|
|
543
|
+
* Webhook event types to send (optional).
|
|
544
|
+
* When empty or undefined, all events are sent.
|
|
545
|
+
* Valid event types: "iteration_complete", "phase_complete", "run_complete", "error", "struggle", "stall".
|
|
546
|
+
* Events outside this list are silently dropped before any network call.
|
|
547
|
+
*/
|
|
548
|
+
notifyEvents?: Array<"iteration_complete" | "phase_complete" | "run_complete" | "error" | "struggle" | "stall">;
|
|
549
|
+
/**
|
|
550
|
+
* When true, the agent adapter is NOT shut down in the finally block.
|
|
551
|
+
* Instead, the adapter + handle are exposed on the LoopResult so the
|
|
552
|
+
* caller can reuse them (e.g., for the debrief). The caller is
|
|
553
|
+
* responsible for calling `adapter.shutdown(handle)`.
|
|
554
|
+
* Default: false.
|
|
555
|
+
*/
|
|
556
|
+
keepAlive?: boolean;
|
|
557
|
+
/**
|
|
558
|
+
* Pre-created logger to reuse. When provided, the loop skips creating
|
|
559
|
+
* its own `createAutopilotLogger` and uses this one instead. This lets
|
|
560
|
+
* the entire autopilot session (enrichment + loop + debrief) share a
|
|
561
|
+
* single log file.
|
|
562
|
+
*/
|
|
563
|
+
logger?: AutopilotLogger;
|
|
564
|
+
/**
|
|
565
|
+
* Optional event emitter for typed SessionEvents (Channel 1).
|
|
566
|
+
* When provided, the loop emits iteration:start, iteration:done,
|
|
567
|
+
* tool:call, cost:update, error, and credential:expired events.
|
|
568
|
+
* The pino logger is kept as a verbose debug channel.
|
|
569
|
+
*/
|
|
570
|
+
emitter?: SessionEventEmitter;
|
|
571
|
+
/**
|
|
572
|
+
* Agent adapter to use for driving the AI agent.
|
|
573
|
+
* Required in production — the CLI injects the OpenCode adapter.
|
|
574
|
+
* Optional only when _deps.startServer is provided (legacy test path).
|
|
575
|
+
*/
|
|
576
|
+
adapter?: AgentAdapter;
|
|
577
|
+
/**
|
|
578
|
+
* Resolved model ID for this loop session. When provided, overrides
|
|
579
|
+
* the default model resolution based on agentName or adapter config.
|
|
580
|
+
* This field enables config-driven model routing for workflow stages.
|
|
581
|
+
*/
|
|
582
|
+
model?: string;
|
|
583
|
+
/**
|
|
584
|
+
* Per-call agent overrides (item 4.2). When provided, passed to the
|
|
585
|
+
* adapter's start() method to apply per-phase agent configuration.
|
|
586
|
+
* Each phase can have its own agent-to-model mapping.
|
|
587
|
+
*/
|
|
588
|
+
agentOverrides?: Record<string, unknown>;
|
|
589
|
+
/**
|
|
590
|
+
* Optional config object for iteration budget tuning (item 3.2),
|
|
591
|
+
* commit settings (item 3.5), and other autopilot features.
|
|
592
|
+
* - config.stall_timeout: per-iteration stall timeout in ms (overrides tier default)
|
|
593
|
+
* - config.auto_commit: boolean (default true) — whether to commit on SIGINT/SIGTERM
|
|
594
|
+
* - config.commit_prefix: string (optional) — prepended to commit subjects
|
|
595
|
+
* When provided, extracted values override tier-based defaults in the loop.
|
|
596
|
+
*/
|
|
597
|
+
config?: unknown;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Run the Ralph loop: send prompt → wait for idle → inspect response →
|
|
601
|
+
* retry or exit.
|
|
602
|
+
*/
|
|
603
|
+
declare function runRalphLoop(opts: RalphLoopOptions): Promise<LoopResult>;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Shared type definitions for the loop session orchestrator and plan session.
|
|
607
|
+
* Defined here so both loop-session.ts, plan-session.ts (in autopilot) and
|
|
608
|
+
* the CLI interactive command can import them without circular dependencies.
|
|
609
|
+
*/
|
|
610
|
+
|
|
611
|
+
interface PlanSessionOptions {
|
|
612
|
+
scopePath: string;
|
|
613
|
+
planDir: string;
|
|
614
|
+
slug: string;
|
|
615
|
+
}
|
|
616
|
+
interface PlanSessionResult {
|
|
617
|
+
planPath: string;
|
|
618
|
+
}
|
|
619
|
+
interface LoopSessionOptions {
|
|
620
|
+
planPath: string;
|
|
621
|
+
cwd: string;
|
|
622
|
+
/** When true, use the fast executor agent (mid-execute tier). */
|
|
623
|
+
fast?: boolean;
|
|
624
|
+
/**
|
|
625
|
+
* When true, read `<cwd>/.agent/autopilot-checkpoint.json` and skip
|
|
626
|
+
* phases already listed in `completedPhases` (provided the checkpoint's
|
|
627
|
+
* `planPath` matches the current `--plan`). On full completion the
|
|
628
|
+
* checkpoint is deleted.
|
|
629
|
+
*/
|
|
630
|
+
resume?: boolean;
|
|
631
|
+
/**
|
|
632
|
+
* Per-phase iteration budget override. When unset, defaults are
|
|
633
|
+
* picked by tier (see MAX_ITERATIONS_PER_PHASE_BY_TIER):
|
|
634
|
+
* - deep: 5
|
|
635
|
+
* - mid-execute / autopilot-execute: 10
|
|
636
|
+
* - fast: 10
|
|
637
|
+
* A phase that hits `max-iterations` is treated as a soft failure:
|
|
638
|
+
* checkpoint is written, a warning is logged, and the run continues
|
|
639
|
+
* to the next phase rather than terminating.
|
|
640
|
+
*/
|
|
641
|
+
maxIterationsPerPhase?: number;
|
|
642
|
+
/**
|
|
643
|
+
* Maximum number of retry attempts per phase when verify fails.
|
|
644
|
+
* Default: 3. Set to 1 to disable retries (single attempt per phase).
|
|
645
|
+
*/
|
|
646
|
+
maxPhaseRetries?: number;
|
|
647
|
+
/**
|
|
648
|
+
* Number of parallel lanes for phase execution (item 3.3).
|
|
649
|
+
* Default: 1 (sequential — preserves the original semantics exactly).
|
|
650
|
+
* When > 1 AND the conflict graph reveals at least one independent
|
|
651
|
+
* pair, phases are dispatched to per-lane git worktrees and merged
|
|
652
|
+
* back on completion (items 3.2 + 3.6). When ≤ 1 OR no parallelism
|
|
653
|
+
* is possible (every phase conflicts with every other), the runner
|
|
654
|
+
* falls back to the sequential path (item 3.7) — no worktree overhead.
|
|
655
|
+
*/
|
|
656
|
+
parallel?: number;
|
|
657
|
+
/**
|
|
658
|
+
* Auto-ship after all phases pass (item 4.7). When true, the runner
|
|
659
|
+
* pushes the current branch and opens a PR via `gh pr create` after
|
|
660
|
+
* verify passes and a changeset has been generated. When false (the
|
|
661
|
+
* default), the runner stops at "all phases complete, run `/ship` to
|
|
662
|
+
* finalize." Push targets and the no-force/no-merge invariants are
|
|
663
|
+
* enforced inside `auto-ship.ts`.
|
|
664
|
+
*/
|
|
665
|
+
ship?: boolean;
|
|
666
|
+
/**
|
|
667
|
+
* Pre-created logger shared across the entire autopilot session
|
|
668
|
+
* (enrichment + loop + debrief). When provided, the loop reuses
|
|
669
|
+
* this logger instead of creating its own.
|
|
670
|
+
*/
|
|
671
|
+
logger?: AutopilotLogger;
|
|
672
|
+
/**
|
|
673
|
+
* Optional event emitter for typed SessionEvents (Channel 1).
|
|
674
|
+
* When provided, loop-session.ts emits phase:start, phase:done,
|
|
675
|
+
* verify:*, and error events. The emitter is also forwarded to
|
|
676
|
+
* runRalphLoop so iteration-level events flow through.
|
|
677
|
+
*/
|
|
678
|
+
emitter?: SessionEventEmitter;
|
|
679
|
+
/**
|
|
680
|
+
* Agent adapter to use for driving the AI agent.
|
|
681
|
+
* Required in production — the CLI injects the OpenCode adapter.
|
|
682
|
+
*/
|
|
683
|
+
adapter?: AgentAdapter;
|
|
684
|
+
/**
|
|
685
|
+
* Resolved autopilot configuration from `.glrs/autopilot.yaml` (plan-specific > project > defaults).
|
|
686
|
+
* Unused in this wave — wired for future consumers that need to read individual config fields.
|
|
687
|
+
*/
|
|
688
|
+
config?: unknown;
|
|
689
|
+
/**
|
|
690
|
+
* Optional abort signal for graceful shutdown. When the signal fires,
|
|
691
|
+
* the loop writes a checkpoint and returns with exitReason: "aborted"
|
|
692
|
+
* at the next phase boundary. Mid-tool-call iterations are not
|
|
693
|
+
* interrupted — the abort is checked between phases only.
|
|
694
|
+
*/
|
|
695
|
+
signal?: AbortSignal;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
interface SessionRunnerOptions {
|
|
699
|
+
planPath: string;
|
|
700
|
+
cwd: string;
|
|
701
|
+
fast?: boolean;
|
|
702
|
+
resume?: boolean;
|
|
703
|
+
parallel?: number;
|
|
704
|
+
ship?: boolean;
|
|
705
|
+
maxIterationsPerPhase?: number;
|
|
706
|
+
/**
|
|
707
|
+
* The agent adapter (e.g. OpenCodeAdapter). Required for production use.
|
|
708
|
+
* The adapter drives the actual agent CLI — start server, create session,
|
|
709
|
+
* send prompts, wait for idle.
|
|
710
|
+
*/
|
|
711
|
+
adapter?: AgentAdapter;
|
|
712
|
+
/**
|
|
713
|
+
* Path for the NDJSON event stream file.
|
|
714
|
+
* Defaults to `<cwd>/.agent/autopilot-events.jsonl`.
|
|
715
|
+
*/
|
|
716
|
+
eventStreamPath?: string;
|
|
717
|
+
/**
|
|
718
|
+
* Enrichment configuration (strategy, retry, timeouts).
|
|
719
|
+
* Passed through to enrichPlanForFastModel when --fast is used.
|
|
720
|
+
*/
|
|
721
|
+
enrichmentConfig?: {
|
|
722
|
+
strategy?: string;
|
|
723
|
+
retry?: boolean;
|
|
724
|
+
max_retries?: number;
|
|
725
|
+
stall_timeout?: number;
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Resolved autopilot configuration from `.glrs/autopilot.yaml`.
|
|
729
|
+
* Passed through to enrichment, loop execution, and debrief for model resolution.
|
|
730
|
+
*/
|
|
731
|
+
config?: unknown;
|
|
732
|
+
/**
|
|
733
|
+
* Injectable dependencies for testing.
|
|
734
|
+
* @internal
|
|
735
|
+
*/
|
|
736
|
+
_deps?: SessionRunnerDeps;
|
|
737
|
+
}
|
|
738
|
+
interface SessionResult {
|
|
739
|
+
planPath: string;
|
|
740
|
+
loopResult: LoopResult;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Injectable dependencies for testing.
|
|
744
|
+
* @internal
|
|
745
|
+
*/
|
|
746
|
+
interface SessionRunnerDeps {
|
|
747
|
+
/** Override enrichPlanForFastModel for testing. */
|
|
748
|
+
enrichPlan?: (cwd: string, planPath: string, logger?: AutopilotLogger) => Promise<string>;
|
|
749
|
+
/** Override runLoopSession for testing. */
|
|
750
|
+
runLoopSession?: (opts: LoopSessionOptions & {
|
|
751
|
+
_deps?: unknown;
|
|
752
|
+
}) => Promise<LoopResult>;
|
|
753
|
+
/** Override createAutopilotLogger for testing. */
|
|
754
|
+
createLogger?: (opts: {
|
|
755
|
+
cwd: string;
|
|
756
|
+
}) => AutopilotLogger;
|
|
757
|
+
/** Override EventStreamWriter constructor for testing. */
|
|
758
|
+
createWriter?: (filePath: string) => EventStreamWriter;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Typed EventEmitter for SessionEvents. Wraps Node's EventEmitter with a
|
|
762
|
+
* typed `emit` method so callers get type-checked event payloads.
|
|
763
|
+
*/
|
|
764
|
+
declare class SessionEventEmitter extends EventEmitter {
|
|
765
|
+
emitEvent(event: SessionEvent): void;
|
|
766
|
+
}
|
|
767
|
+
declare class SessionRunner {
|
|
768
|
+
/** Channel 1: in-process event emitter. Subscribe to event types or "event" for all. */
|
|
769
|
+
readonly events: SessionEventEmitter;
|
|
770
|
+
private readonly opts;
|
|
771
|
+
private _abortController;
|
|
772
|
+
private _abortCount;
|
|
773
|
+
constructor(opts: SessionRunnerOptions);
|
|
774
|
+
/**
|
|
775
|
+
* Request graceful shutdown. First call signals the running loop session
|
|
776
|
+
* to stop at the next phase boundary (checkpoint is written). Second call
|
|
777
|
+
* force-exits via process.exit(1).
|
|
778
|
+
*/
|
|
779
|
+
abort(): void;
|
|
780
|
+
/**
|
|
781
|
+
* Run the autopilot session:
|
|
782
|
+
* 1. Emit session:start
|
|
783
|
+
* 2. If --fast, run enrichment (emitting enrich:* events)
|
|
784
|
+
* 3. Run the loop session (emitting phase:* events via loop-session.ts)
|
|
785
|
+
* 4. Emit session:done
|
|
786
|
+
*
|
|
787
|
+
* Returns a SessionResult with the final LoopResult.
|
|
788
|
+
*/
|
|
789
|
+
run(): Promise<SessionResult>;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Loop session runner for the interactive autopilot orchestrator.
|
|
794
|
+
*
|
|
795
|
+
* For multi-file plans (directory with main.md + phase_N.md files):
|
|
796
|
+
* - Reads main.md to extract Goal and Constraints sections
|
|
797
|
+
* - Detects unchecked phase files from the ## Phases section
|
|
798
|
+
* - Creates a fresh runRalphLoop session per phase with a prompt
|
|
799
|
+
* containing Goal + Constraints + full phase file contents
|
|
800
|
+
* - After each successful phase, updates main.md's phase checkbox
|
|
801
|
+
* - Stops early if a phase exits with struggle/stall/error/timeout
|
|
802
|
+
*
|
|
803
|
+
* For single-file plans (.md file):
|
|
804
|
+
* - Unchanged behavior: single runRalphLoop call with a direct prompt
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Injectable dependencies for testing.
|
|
809
|
+
* @internal
|
|
810
|
+
*/
|
|
811
|
+
interface LoopSessionDeps {
|
|
812
|
+
runRalphLoop?: (opts: RalphLoopOptions) => Promise<LoopResult>;
|
|
813
|
+
/** Override filesystem stat check for testing. */
|
|
814
|
+
isDirectory?: (p: string) => boolean;
|
|
815
|
+
/** Override fs.readFileSync for testing. */
|
|
816
|
+
readFileSync?: (p: string) => string;
|
|
817
|
+
/** Override fs.writeFileSync for testing. */
|
|
818
|
+
writeFileSync?: (p: string, content: string) => void;
|
|
819
|
+
/**
|
|
820
|
+
* Override the post-phase verify-command runner (item 4.1) for tests
|
|
821
|
+
* that don't want to actually shell out. Default: real implementation
|
|
822
|
+
* from `verify-runner.ts`.
|
|
823
|
+
*/
|
|
824
|
+
runVerifyCommands?: typeof runVerifyCommands;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Run a headless loop session against a plan.
|
|
828
|
+
*
|
|
829
|
+
* Detects whether planPath is a directory (multi-file plan) or a file
|
|
830
|
+
* (single-file plan) and shapes the prompt accordingly, then delegates
|
|
831
|
+
* to runRalphLoop.
|
|
832
|
+
*/
|
|
833
|
+
declare function runLoopSession(opts: LoopSessionOptions & {
|
|
834
|
+
_deps?: LoopSessionDeps;
|
|
835
|
+
}): Promise<LoopResult>;
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Adapter-aware model resolver. Resolves a model specifier to a concrete model ID
|
|
839
|
+
* based on the active adapter (OpenCode or Claude Code CLI).
|
|
840
|
+
*
|
|
841
|
+
* Both adapters support:
|
|
842
|
+
* - Full model IDs containing "/" (e.g., "amazon-bedrock/claude-opus") pass through unchanged
|
|
843
|
+
*
|
|
844
|
+
* OpenCode adapter:
|
|
845
|
+
* - Tier names resolve through `opencodeTiers` map
|
|
846
|
+
* - Unknown tier names fall back to `opencodeTiers?.deep` with a warning
|
|
847
|
+
* - Array-valued tiers return the first element
|
|
848
|
+
*
|
|
849
|
+
* Claude Code CLI adapter:
|
|
850
|
+
* - Known tier aliases map to specific Claude model IDs
|
|
851
|
+
* - Unknown literals pass through unchanged
|
|
852
|
+
*/
|
|
853
|
+
type AdapterName = "opencode" | "claude-code-cli";
|
|
854
|
+
declare function resolveModel(specifier: string, adapterName: AdapterName, opencodeTiers?: Record<string, string | string[]>): string;
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Plan enrichment for fast-model execution.
|
|
858
|
+
*
|
|
859
|
+
* When --fast is used, this module reads the markdown plan files and
|
|
860
|
+
* generates `spec/*.yaml` files from them. The spec IS the enriched
|
|
861
|
+
* artifact — one LLM pass per file that reads the markdown + codebase
|
|
862
|
+
* and produces structured YAML with enrichment context included:
|
|
863
|
+
* - mirror: references to similar existing files (pattern-match targets)
|
|
864
|
+
* - context: key function signatures, 10-20 lines of code for modified files
|
|
865
|
+
* - conventions: import style, export pattern, test framework, naming
|
|
866
|
+
*
|
|
867
|
+
* Per-file iteration: each plan file gets its own fresh session and its
|
|
868
|
+
* own short-context prompt. This keeps each enrichment call's context
|
|
869
|
+
* small and lets a SIGINT halfway through preserve the already-generated
|
|
870
|
+
* spec files. Whole-plan single-pass enrichment is gone — the cost
|
|
871
|
+
* savings of context-locality more than compensate for the per-file
|
|
872
|
+
* session overhead.
|
|
873
|
+
*
|
|
874
|
+
* Idempotency (item 4.3): before opening any session, check whether
|
|
875
|
+
* spec/ already exists with all items enriched (100% threshold). If so,
|
|
876
|
+
* skip the entire pass. This saves an Opus call on re-runs of `--fast`
|
|
877
|
+
* against an already-enriched plan.
|
|
878
|
+
*/
|
|
879
|
+
|
|
880
|
+
interface EnrichmentRunConfig {
|
|
881
|
+
retry?: boolean;
|
|
882
|
+
max_retries?: number;
|
|
883
|
+
stall_timeout?: number;
|
|
884
|
+
strategy?: string;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Returns true when a file has no structured plan items — no checkboxes
|
|
888
|
+
* (`- [ ]`) and no numbered headings (`### N.N`). Such files need
|
|
889
|
+
* decomposition before the enrichment loop can process them.
|
|
890
|
+
*/
|
|
891
|
+
declare function isFreeformFile(resolvedPath: string): boolean;
|
|
892
|
+
/**
|
|
893
|
+
* Enrich a plan for fast-model execution by generating spec/*.yaml files.
|
|
894
|
+
*
|
|
895
|
+
* For multi-file plans (directory with main.md), generates a spec YAML file
|
|
896
|
+
* for each markdown plan file in its own session. For single-file plans,
|
|
897
|
+
* generates a spec file for that file. Each per-file session is independent —
|
|
898
|
+
* failures in one file are logged and skipped, the loop moves to the next.
|
|
899
|
+
*
|
|
900
|
+
* Idempotency: if spec/ already exists with all items fully enriched,
|
|
901
|
+
* enriched, the entire pass is skipped. Per-file: if a spec YAML already
|
|
902
|
+
* exists and is sufficiently enriched, that file is skipped.
|
|
903
|
+
*
|
|
904
|
+
* Retry: when enrichment stalls or errors, kills the server and retries the
|
|
905
|
+
* entire pass. The per-file idempotency check skips already-enriched files,
|
|
906
|
+
* so retries only pay for failures. Controlled by enrichmentConfig.retry
|
|
907
|
+
* (default true) and enrichmentConfig.max_retries (default 3).
|
|
908
|
+
*/
|
|
909
|
+
declare function enrichPlanForFastModel(cwd: string, planPath: string, logger?: AutopilotLogger, emitter?: SessionEventEmitter, adapter?: AgentAdapter, enrichmentConfig?: EnrichmentRunConfig, config?: unknown): Promise<string>;
|
|
910
|
+
|
|
911
|
+
interface ValidationResult {
|
|
912
|
+
valid: boolean;
|
|
913
|
+
errors: string[];
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Validate a parsed main spec object. Returns structured errors rather
|
|
917
|
+
* than throwing so callers can surface clear messages.
|
|
918
|
+
*/
|
|
919
|
+
declare function validateMainSpec(raw: unknown): ValidationResult;
|
|
920
|
+
/**
|
|
921
|
+
* Validate a parsed phase spec object. Returns structured errors rather
|
|
922
|
+
* than throwing so callers can surface clear messages.
|
|
923
|
+
*/
|
|
924
|
+
declare function validatePhaseSpec(raw: unknown): ValidationResult;
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* YAML spec parser for the autopilot engine.
|
|
928
|
+
*
|
|
929
|
+
* Reads spec/main.yaml and spec/<phase>.yaml files, validates them
|
|
930
|
+
* against the schema, and converts to the canonical PlanState/PlanItem
|
|
931
|
+
* types used throughout the autopilot.
|
|
932
|
+
*
|
|
933
|
+
* Never throws — all errors degrade to zero-count results or empty
|
|
934
|
+
* arrays so callers can fall back gracefully.
|
|
935
|
+
*/
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Check whether a plan directory has a YAML spec (spec/main.yaml exists).
|
|
939
|
+
* This is the gate that determines whether to use the YAML path or fall
|
|
940
|
+
* back to the markdown parser.
|
|
941
|
+
*/
|
|
942
|
+
declare function hasSpec(planDir: string): boolean;
|
|
943
|
+
/**
|
|
944
|
+
* Parse a phase YAML file and return PlanItem[].
|
|
945
|
+
* Returns [] on any error (missing file, invalid YAML, schema failure).
|
|
946
|
+
*/
|
|
947
|
+
declare function parseSpecItems(phasePath: string): Array<PlanItem & {
|
|
948
|
+
mirror?: string;
|
|
949
|
+
context?: string;
|
|
950
|
+
conventions?: string;
|
|
951
|
+
proof?: string;
|
|
952
|
+
proof_type?: string;
|
|
953
|
+
}>;
|
|
954
|
+
/**
|
|
955
|
+
* Detect phase files from spec/main.yaml. Returns sorted list of phase
|
|
956
|
+
* filenames (e.g., ["wave_0.yaml", "wave_1.yaml"]).
|
|
957
|
+
*/
|
|
958
|
+
declare function detectSpecPhases(planDir: string): string[];
|
|
959
|
+
/**
|
|
960
|
+
* Read the goal field from spec/main.yaml. Returns "" on failure.
|
|
961
|
+
*/
|
|
962
|
+
declare function readSpecGoal(planDir: string): string;
|
|
963
|
+
/**
|
|
964
|
+
* Read the constraints field from spec/main.yaml. Returns "" on failure.
|
|
965
|
+
*/
|
|
966
|
+
declare function readSpecConstraints(planDir: string): string;
|
|
967
|
+
/**
|
|
968
|
+
* Filter a list of phase filenames to only those not yet completed.
|
|
969
|
+
* Uses yaml.parse() on spec/main.yaml to read the `completed` field
|
|
970
|
+
* on each phase entry — no hand-rolled regex.
|
|
971
|
+
*
|
|
972
|
+
* Returns `phaseFiles` unchanged on any error (fail-open so the caller
|
|
973
|
+
* can still attempt all phases rather than silently skipping them).
|
|
974
|
+
*/
|
|
975
|
+
declare function filterUncheckedSpecPhases(phaseFiles: string[], planDir: string): string[];
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Mark a phase as completed in spec/main.yaml.
|
|
979
|
+
*
|
|
980
|
+
* @param planDir - The plan directory (parent of spec/)
|
|
981
|
+
* @param phaseFile - The phase filename to mark completed (e.g., "wave_0.yaml")
|
|
982
|
+
*/
|
|
983
|
+
declare function markPhaseCompleted(planDir: string, phaseFile: string): void;
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Autopilot run checkpoint persistence.
|
|
987
|
+
*
|
|
988
|
+
* After each phase completes, the multi-phase loop writes a checkpoint
|
|
989
|
+
* to `<cwd>/.agent/autopilot-checkpoint.json`. On startup with
|
|
990
|
+
* `--resume` (or whenever the checkpoint matches the current `--plan`),
|
|
991
|
+
* the loop skips already-completed phases and continues from the next
|
|
992
|
+
* unchecked one.
|
|
993
|
+
*
|
|
994
|
+
* On successful run completion (all phases done), the checkpoint is
|
|
995
|
+
* deleted.
|
|
996
|
+
*
|
|
997
|
+
* Design notes:
|
|
998
|
+
* - Atomic write via tmp-file + rename (mirrors cost-tracker's rollup
|
|
999
|
+
* pattern at src/plugins/cost-tracker.ts lines 280-305).
|
|
1000
|
+
* - Read failures (corrupt JSON, missing file, perm denial) return
|
|
1001
|
+
* `null`. This module never throws — failure modes are silent.
|
|
1002
|
+
* - The checkpoint is purely advisory: callers must validate the
|
|
1003
|
+
* `planPath` matches the current run's plan before trusting any
|
|
1004
|
+
* `completedPhases` list.
|
|
1005
|
+
*/
|
|
1006
|
+
interface Checkpoint {
|
|
1007
|
+
/**
|
|
1008
|
+
* Absolute path to the plan (file or directory) that this checkpoint
|
|
1009
|
+
* was created for. Resume only valid when the current --plan matches.
|
|
1010
|
+
*/
|
|
1011
|
+
planPath: string;
|
|
1012
|
+
/** Phase filenames that have completed (e.g. ["wave_1.md"]). */
|
|
1013
|
+
completedPhases: string[];
|
|
1014
|
+
/** Cumulative cost across completed phases, in USD. */
|
|
1015
|
+
totalCostUsd: number;
|
|
1016
|
+
/** Cumulative iteration count across completed phases. */
|
|
1017
|
+
totalIterations: number;
|
|
1018
|
+
/** ISO 8601 timestamp of the last write. */
|
|
1019
|
+
timestamp: string;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Injectable filesystem deps for testing. Production code should call
|
|
1023
|
+
* the exported functions without `_deps`; tests inject mocks.
|
|
1024
|
+
*
|
|
1025
|
+
* @internal
|
|
1026
|
+
*/
|
|
1027
|
+
interface CheckpointDeps {
|
|
1028
|
+
readFileSync?: (p: string) => string;
|
|
1029
|
+
writeFileSync?: (p: string, content: string) => void;
|
|
1030
|
+
unlinkSync?: (p: string) => void;
|
|
1031
|
+
renameSync?: (from: string, to: string) => void;
|
|
1032
|
+
existsSync?: (p: string) => boolean;
|
|
1033
|
+
mkdirSync?: (p: string, opts: {
|
|
1034
|
+
recursive: boolean;
|
|
1035
|
+
}) => void;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Atomically write the checkpoint file. Never throws — write errors
|
|
1039
|
+
* are swallowed (best-effort persistence; the run continues even if
|
|
1040
|
+
* the checkpoint can't be persisted).
|
|
1041
|
+
*/
|
|
1042
|
+
declare function writeCheckpoint(cwd: string, state: Checkpoint, deps?: CheckpointDeps): void;
|
|
1043
|
+
/**
|
|
1044
|
+
* Read and parse the checkpoint file. Returns `null` if the file is
|
|
1045
|
+
* missing, unreadable, or contains corrupt JSON / wrong shape.
|
|
1046
|
+
*/
|
|
1047
|
+
declare function readCheckpoint(cwd: string, deps?: CheckpointDeps): Checkpoint | null;
|
|
1048
|
+
/**
|
|
1049
|
+
* Delete the checkpoint file. Silent on missing / permission errors.
|
|
1050
|
+
*/
|
|
1051
|
+
declare function deleteCheckpoint(cwd: string, deps?: CheckpointDeps): void;
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Default budgets and constants for the Ralph loop autopilot engine.
|
|
1055
|
+
*/
|
|
1056
|
+
/** Maximum number of loop iterations before giving up. */
|
|
1057
|
+
declare const MAX_ITERATIONS = 50;
|
|
1058
|
+
/**
|
|
1059
|
+
* Number of consecutive zero-progress iterations before the loop
|
|
1060
|
+
* declares the agent is struggling and exits.
|
|
1061
|
+
*/
|
|
1062
|
+
declare const STRUGGLE_THRESHOLD = 3;
|
|
1063
|
+
/** Total wall-clock timeout for one `glrs oc autopilot` invocation (4 hours). */
|
|
1064
|
+
declare const TIMEOUT_MS: number;
|
|
1065
|
+
/**
|
|
1066
|
+
* Per-iteration stall timeout, keyed by model tier. If a single
|
|
1067
|
+
* iteration produces no idle signal within the tier's window,
|
|
1068
|
+
* something is broken.
|
|
1069
|
+
*
|
|
1070
|
+
* Deep models (Opus, Sonnet thinking) get the longest window — they're
|
|
1071
|
+
* permitted to chew on a hard problem for a while. Mid-tier executors
|
|
1072
|
+
* are faster and a long stall there is much more likely to be a stuck
|
|
1073
|
+
* call than a productive think. Fast models are the most responsive
|
|
1074
|
+
* and warrant the shortest window.
|
|
1075
|
+
*
|
|
1076
|
+
* Fallback defaults when config.stall_timeout is not set.
|
|
1077
|
+
* The CLI accepts `--stall-timeout <ms>` to override the tier-default,
|
|
1078
|
+
* and config.stall_timeout overrides both the CLI default and tier lookup.
|
|
1079
|
+
*/
|
|
1080
|
+
declare const STALL_MS_BY_TIER: {
|
|
1081
|
+
readonly deep: number;
|
|
1082
|
+
readonly mid: number;
|
|
1083
|
+
readonly "mid-execute": number;
|
|
1084
|
+
readonly "autopilot-execute": number;
|
|
1085
|
+
readonly fast: number;
|
|
1086
|
+
};
|
|
1087
|
+
/** Backwards-compatible default (used when no tier is resolved). */
|
|
1088
|
+
declare const STALL_MS: number;
|
|
1089
|
+
/**
|
|
1090
|
+
* Phase-level iteration budgets, keyed by model tier (item 2.7).
|
|
1091
|
+
* Fallback defaults when config.max_iterations_per_phase is not set.
|
|
1092
|
+
*/
|
|
1093
|
+
declare const MAX_ITERATIONS_PER_PHASE_BY_TIER: {
|
|
1094
|
+
readonly deep: 5;
|
|
1095
|
+
readonly mid: 8;
|
|
1096
|
+
readonly "mid-execute": 10;
|
|
1097
|
+
readonly "autopilot-execute": 10;
|
|
1098
|
+
readonly fast: 10;
|
|
1099
|
+
};
|
|
1100
|
+
/**
|
|
1101
|
+
* Status-heartbeat interval. Every N milliseconds the loop emits an
|
|
1102
|
+
* info-level "working" log line summarizing elapsed time, iteration
|
|
1103
|
+
* count, and cumulative cost. Default: 5 minutes.
|
|
1104
|
+
* Override via GLRS_AUTOPILOT_STATUS_INTERVAL_MS (parsed as integer,
|
|
1105
|
+
* clamped to [1000, 1h]).
|
|
1106
|
+
*/
|
|
1107
|
+
declare const STATUS_INTERVAL_MS: number;
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* CLI flag override application for autopilot configuration.
|
|
1111
|
+
*
|
|
1112
|
+
* Maps CLI flags to their config field equivalents and applies them with
|
|
1113
|
+
* proper precedence. All fields are optional; undefined flags are ignored.
|
|
1114
|
+
* Returns a new config object without mutating the input.
|
|
1115
|
+
*/
|
|
1116
|
+
interface CLIFlags {
|
|
1117
|
+
/** Adapter name override (--adapter / -a) */
|
|
1118
|
+
adapter?: string;
|
|
1119
|
+
/** Use fast executor model (--fast / -f) */
|
|
1120
|
+
fast?: boolean;
|
|
1121
|
+
/** Resume from checkpoint (--resume) */
|
|
1122
|
+
resume?: boolean;
|
|
1123
|
+
/** Per-phase iteration budget override (--max-iterations-per-phase) */
|
|
1124
|
+
maxIterationsPerPhase?: number;
|
|
1125
|
+
/** Number of parallel lanes (--parallel) */
|
|
1126
|
+
parallel?: number;
|
|
1127
|
+
/** Auto-ship after completion (--ship) */
|
|
1128
|
+
ship?: boolean;
|
|
1129
|
+
/** Per-iteration stall timeout in ms (--stall-timeout) */
|
|
1130
|
+
stallTimeout?: number;
|
|
1131
|
+
/** Webhook URL for notifications (--notify) */
|
|
1132
|
+
notify?: string;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Applies CLI flag overrides to autopilot configuration.
|
|
1136
|
+
*
|
|
1137
|
+
* Maps each CLI flag to its config field equivalent:
|
|
1138
|
+
* - `--adapter` → `adapter` (highest precedence)
|
|
1139
|
+
* - `--fast` → `models.execution` (resolved by adapter)
|
|
1140
|
+
* - `--parallel N` → `execution_order: "parallel"` + `parallel_lanes: N`
|
|
1141
|
+
* - `--ship` → `auto_ship: true`
|
|
1142
|
+
* - `--resume` → `checkpoint: true` (implicit)
|
|
1143
|
+
* - `--max-iterations-per-phase N` → `max_iterations_per_phase: N`
|
|
1144
|
+
* - `--stall-timeout N` → `stall_timeout: N`
|
|
1145
|
+
* - `--notify URL` → `notify_url: URL`
|
|
1146
|
+
*
|
|
1147
|
+
* Undefined flags are skipped (no override applied).
|
|
1148
|
+
* `--fast` resolves to the active adapter's execution tier.
|
|
1149
|
+
*
|
|
1150
|
+
* @param config The resolved autopilot configuration
|
|
1151
|
+
* @param flags CLI flags to apply
|
|
1152
|
+
* @returns A new config object with overrides applied (input is not mutated)
|
|
1153
|
+
*/
|
|
1154
|
+
declare function applyCLIOverrides(config: unknown, flags: CLIFlags): unknown;
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Status-heartbeat helper for the autopilot loop.
|
|
1158
|
+
*
|
|
1159
|
+
* Fires a periodic "still working" summary log at info level every N
|
|
1160
|
+
* milliseconds (see STATUS_INTERVAL_MS in config.ts). The summary
|
|
1161
|
+
* reports:
|
|
1162
|
+
*
|
|
1163
|
+
* - elapsed wall time since loop start
|
|
1164
|
+
* - iteration count (completed iterations, not current)
|
|
1165
|
+
* - cumulative session cost in USD (sampled via session.get().data.cost)
|
|
1166
|
+
* - a 1-line "working successfully" signal (or a warning if the last
|
|
1167
|
+
* iteration hit an error)
|
|
1168
|
+
*
|
|
1169
|
+
* Decoupled from the Ralph loop's iteration accounting — the heartbeat
|
|
1170
|
+
* reads a shared state snapshot that the loop updates between iterations.
|
|
1171
|
+
* Fires on a setInterval timer, not per-iteration, so the user sees
|
|
1172
|
+
* activity even during long single-iteration sessions (PRIME streaming
|
|
1173
|
+
* text / running a slow test suite / waiting on an LLM response).
|
|
1174
|
+
*/
|
|
1175
|
+
|
|
1176
|
+
interface StatusState {
|
|
1177
|
+
/** Wall-clock timestamp when the loop started. */
|
|
1178
|
+
startedAt: number;
|
|
1179
|
+
/** Completed iteration count (not the current in-flight iteration). */
|
|
1180
|
+
iterationsCompleted: number;
|
|
1181
|
+
/** Latest cumulative session cost in USD, sampled at iteration boundaries. */
|
|
1182
|
+
cumulativeCostUsd: number;
|
|
1183
|
+
/** Whether the most recent iteration made filesystem progress. */
|
|
1184
|
+
lastIterationProgress: boolean;
|
|
1185
|
+
/** Whether the most recent iteration errored. */
|
|
1186
|
+
lastIterationErrored: boolean;
|
|
1187
|
+
/**
|
|
1188
|
+
* When true, `cumulativeCostUsd` is an estimate from token counts
|
|
1189
|
+
* (not API-reported). Displayed as "~$X.XXX est".
|
|
1190
|
+
*/
|
|
1191
|
+
costIsEstimated?: boolean;
|
|
1192
|
+
/** Total number of phases in the plan (multi-file plans only). */
|
|
1193
|
+
phaseCount?: number;
|
|
1194
|
+
/** Number of phases completed so far. */
|
|
1195
|
+
phasesCompleted?: number;
|
|
1196
|
+
/** Total checkbox items in main.md (multi-file plans only). */
|
|
1197
|
+
mainCheckboxesTotal?: number;
|
|
1198
|
+
/** Checked checkbox items in main.md. */
|
|
1199
|
+
mainCheckboxesCompleted?: number;
|
|
1200
|
+
/**
|
|
1201
|
+
* Active parallel lanes (item 3.4). Only set when the orchestrator is
|
|
1202
|
+
* running > 1 lane; the sequential path leaves this undefined and
|
|
1203
|
+
* `composeStatusMessage` falls back to the single-stream output.
|
|
1204
|
+
*/
|
|
1205
|
+
lanes?: Record<string, LaneState>;
|
|
1206
|
+
}
|
|
1207
|
+
interface LaneState {
|
|
1208
|
+
/** Phase filename currently running on this lane. */
|
|
1209
|
+
phaseFile: string;
|
|
1210
|
+
/** Iteration number within the lane's current phase (1-based). */
|
|
1211
|
+
iteration: number;
|
|
1212
|
+
/** Most-recent tool name observed on the lane (optional). */
|
|
1213
|
+
lastTool?: string;
|
|
1214
|
+
}
|
|
1215
|
+
interface StatusHeartbeat {
|
|
1216
|
+
/** Start the heartbeat timer. Idempotent — safe to call multiple times. */
|
|
1217
|
+
start(): void;
|
|
1218
|
+
/** Stop the timer. Safe to call if not started. */
|
|
1219
|
+
stop(): void;
|
|
1220
|
+
/** Update the shared state. Call between iterations. */
|
|
1221
|
+
update(patch: Partial<StatusState>): void;
|
|
1222
|
+
/** Read the current state snapshot (for testing). */
|
|
1223
|
+
getState(): StatusState;
|
|
1224
|
+
}
|
|
1225
|
+
interface StatusHeartbeatOptions {
|
|
1226
|
+
logger: Logger;
|
|
1227
|
+
intervalMs: number;
|
|
1228
|
+
/** Optional async function to poll current session cost. Called on
|
|
1229
|
+
* each heartbeat tick. If provided, the returned cost replaces
|
|
1230
|
+
* the heartbeat's cumulativeCostUsd on each tick. */
|
|
1231
|
+
pollCost?: () => Promise<number>;
|
|
1232
|
+
/**
|
|
1233
|
+
* Optional path to write the status snapshot as JSON on each tick.
|
|
1234
|
+
* Written atomically via tmp-file-then-rename. Non-fatal on error.
|
|
1235
|
+
* Default: undefined (no file write).
|
|
1236
|
+
*/
|
|
1237
|
+
statusFilePath?: string;
|
|
1238
|
+
/** Test-only: inject clock and timer functions. */
|
|
1239
|
+
_deps?: {
|
|
1240
|
+
now?: () => number;
|
|
1241
|
+
setInterval?: (handler: () => void, ms: number) => ReturnType<typeof setInterval>;
|
|
1242
|
+
clearInterval?: (id: ReturnType<typeof setInterval>) => void;
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Format elapsed milliseconds as "Xh Ym Zs" (Xh is omitted when 0).
|
|
1247
|
+
* Public for testing.
|
|
1248
|
+
*/
|
|
1249
|
+
declare function formatElapsed(ms: number): string;
|
|
1250
|
+
/**
|
|
1251
|
+
* Format cost as USD with 3 decimal places, or "$0.00" for zero.
|
|
1252
|
+
* When `estimated` is true, prepends "~" and appends " est" to indicate
|
|
1253
|
+
* the cost is an estimate (e.g., from token counts when API cost is unavailable).
|
|
1254
|
+
* Public for testing.
|
|
1255
|
+
*/
|
|
1256
|
+
declare function formatCost(usd: number, estimated?: boolean): string;
|
|
1257
|
+
declare function createStatusHeartbeat(opts: StatusHeartbeatOptions): StatusHeartbeat;
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Sentinel detection for the Ralph loop autopilot engine.
|
|
1261
|
+
*
|
|
1262
|
+
* The agent emits `<autopilot-done>` as a standalone tag (not inside a
|
|
1263
|
+
* code fence or inline backtick) to signal that all work is complete.
|
|
1264
|
+
*/
|
|
1265
|
+
/**
|
|
1266
|
+
* Returns true if the text contains the `<autopilot-done>` sentinel tag
|
|
1267
|
+
* outside of any code fence (``` ... ```) or inline backtick span.
|
|
1268
|
+
*
|
|
1269
|
+
* Detection rules:
|
|
1270
|
+
* - Case-sensitive: `<AUTOPILOT-DONE>` does NOT match.
|
|
1271
|
+
* - Tag inside a fenced code block (``` ... ```) does NOT match.
|
|
1272
|
+
* - Tag inside an inline backtick span (` ... `) does NOT match.
|
|
1273
|
+
* - Partial tags (`<autopilot-done` or `autopilot-done>`) do NOT match.
|
|
1274
|
+
*/
|
|
1275
|
+
declare function detectSentinel(text: string): boolean;
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Struggle detection for the Ralph loop autopilot engine.
|
|
1279
|
+
*
|
|
1280
|
+
* Tracks consecutive zero-progress iterations. "Progress" is defined as
|
|
1281
|
+
* at least one filesystem write (non-empty `git diff --stat` output)
|
|
1282
|
+
* during the iteration.
|
|
1283
|
+
*/
|
|
1284
|
+
/**
|
|
1285
|
+
* Tracks consecutive zero-progress iterations and signals when the
|
|
1286
|
+
* agent is struggling (no progress for `threshold` consecutive iterations).
|
|
1287
|
+
*/
|
|
1288
|
+
declare class StruggleDetector {
|
|
1289
|
+
private _consecutiveStalls;
|
|
1290
|
+
private readonly _threshold;
|
|
1291
|
+
constructor(threshold: number);
|
|
1292
|
+
/** Number of consecutive stall iterations recorded so far. */
|
|
1293
|
+
get consecutiveStalls(): number;
|
|
1294
|
+
/**
|
|
1295
|
+
* Record the result of one iteration.
|
|
1296
|
+
* @param madeProgress - true if the agent made filesystem changes this iteration.
|
|
1297
|
+
*/
|
|
1298
|
+
record(madeProgress: boolean): void;
|
|
1299
|
+
/**
|
|
1300
|
+
* Returns true if the agent has stalled for `threshold` consecutive
|
|
1301
|
+
* iterations without making progress.
|
|
1302
|
+
*/
|
|
1303
|
+
isStruggling(): boolean;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Returns true if the kill-switch file exists at `.agent/autopilot-disable`
|
|
1307
|
+
* relative to `cwd`. The loop should exit immediately when this returns true.
|
|
1308
|
+
*/
|
|
1309
|
+
declare function checkKillSwitch(cwd: string): boolean;
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Phase-level git safety: record-and-soft-reset helpers.
|
|
1313
|
+
*
|
|
1314
|
+
* Used by the multi-phase loop runner (loop-session.ts) to capture
|
|
1315
|
+
* HEAD before each phase. If a phase fails (struggle / stall / error)
|
|
1316
|
+
* and the user requested it (or running in --fast mode where there's
|
|
1317
|
+
* no human to ask), `resetSoft` rolls the worktree back to that SHA,
|
|
1318
|
+
* preserving any changes in the index so nothing is lost.
|
|
1319
|
+
*
|
|
1320
|
+
* Safety invariants (per AGENTS.md and Wave 2 plan):
|
|
1321
|
+
* - NEVER use `git reset --hard`. Soft reset only.
|
|
1322
|
+
* - Any git failure here is non-fatal — log and continue.
|
|
1323
|
+
* - Helpers use `execFile` from node:child_process via promisify.
|
|
1324
|
+
*/
|
|
1325
|
+
/**
|
|
1326
|
+
* Injectable execFile for testing.
|
|
1327
|
+
* @internal
|
|
1328
|
+
*/
|
|
1329
|
+
interface GitSafetyDeps {
|
|
1330
|
+
/**
|
|
1331
|
+
* Mock the git invocation. Receives the args array; returns
|
|
1332
|
+
* { stdout, stderr } or throws to simulate a git failure.
|
|
1333
|
+
*/
|
|
1334
|
+
execGit?: (args: string[], cwd: string) => Promise<{
|
|
1335
|
+
stdout: string;
|
|
1336
|
+
stderr: string;
|
|
1337
|
+
}>;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Record the current HEAD SHA. Returns "HEAD" on any failure
|
|
1341
|
+
* (matches the existing getHeadSha shape in loop.ts so callers can
|
|
1342
|
+
* pass the result to git operations without crashing).
|
|
1343
|
+
*/
|
|
1344
|
+
declare function recordHead(cwd: string, deps?: GitSafetyDeps): Promise<string>;
|
|
1345
|
+
/**
|
|
1346
|
+
* Soft-reset the worktree to the given SHA. Changes since `sha` move
|
|
1347
|
+
* to the staging area; nothing is destroyed. Any failure is logged
|
|
1348
|
+
* via the optional `onWarn` callback and swallowed.
|
|
1349
|
+
*
|
|
1350
|
+
* Returns true on success, false on failure.
|
|
1351
|
+
*/
|
|
1352
|
+
declare function resetSoft(cwd: string, sha: string, opts?: GitSafetyDeps & {
|
|
1353
|
+
onWarn?: (msg: string) => void;
|
|
1354
|
+
}): Promise<boolean>;
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Conflict-graph builder for parallel phase scheduling.
|
|
1358
|
+
*
|
|
1359
|
+
* Two phases conflict if they share any file path in their `files:` lists
|
|
1360
|
+
* (parsed via `plan-parser`'s `parseItems` from each phase's plan-state
|
|
1361
|
+
* fence). Phases with no shared files are independent and can run in
|
|
1362
|
+
* parallel lanes.
|
|
1363
|
+
*
|
|
1364
|
+
* Conservative fallback: when a phase yields zero parsed items (e.g., no
|
|
1365
|
+
* fenced plan-state block, or the phase is written as plain markdown
|
|
1366
|
+
* checkboxes with `mirror:` / `files:` as prose) it is treated as
|
|
1367
|
+
* conflicting with EVERY other phase. The runtime then collapses to
|
|
1368
|
+
* sequential execution for that phase — which preserves correctness at
|
|
1369
|
+
* the cost of opportunistic parallelism. Explicit, machine-parseable
|
|
1370
|
+
* `files:` entries inside a plan-state fence are required to unlock
|
|
1371
|
+
* parallel scheduling.
|
|
1372
|
+
*
|
|
1373
|
+
* Pure module — no I/O, no logging. Inputs are already-parsed PlanItems.
|
|
1374
|
+
*/
|
|
1375
|
+
|
|
1376
|
+
interface PhaseInput {
|
|
1377
|
+
/** Phase filename (e.g., "wave_1.md"). */
|
|
1378
|
+
file: string;
|
|
1379
|
+
/** Items parsed from this phase's plan-state fence. */
|
|
1380
|
+
items: PlanItem[];
|
|
1381
|
+
}
|
|
1382
|
+
interface ConflictGraph {
|
|
1383
|
+
/** Phase filenames in input order. */
|
|
1384
|
+
phases: string[];
|
|
1385
|
+
/** Map: phase -> set of phases it conflicts with. */
|
|
1386
|
+
conflicts: Map<string, Set<string>>;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Build a conflict graph from a list of phases.
|
|
1390
|
+
*
|
|
1391
|
+
* Two phases A and B conflict iff:
|
|
1392
|
+
* - either phase has unknown file footprint (null from collectPhaseFiles), OR
|
|
1393
|
+
* - their file sets share at least one path.
|
|
1394
|
+
*
|
|
1395
|
+
* Self-conflicts are not recorded (a phase doesn't conflict with itself).
|
|
1396
|
+
*/
|
|
1397
|
+
declare function buildConflictGraph(phases: PhaseInput[]): ConflictGraph;
|
|
1398
|
+
/**
|
|
1399
|
+
* Convenience: returns true if the graph has any independent pair —
|
|
1400
|
+
* i.e., at least one group from `findIndependentPhases` has size > 1.
|
|
1401
|
+
* Used by the orchestrator to decide whether parallel scheduling can
|
|
1402
|
+
* yield any speedup, vs. falling back to sequential to skip overhead.
|
|
1403
|
+
*/
|
|
1404
|
+
declare function hasParallelism(graph: ConflictGraph): boolean;
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Parallel-lane orchestrator (item 3.3).
|
|
1408
|
+
*
|
|
1409
|
+
* Replaces the sequential per-phase loop in `loop-session.ts` with an
|
|
1410
|
+
* N-lane scheduler. Each lane runs one phase at a time. When a lane
|
|
1411
|
+
* finishes, it picks up the next phase in the queue that does NOT
|
|
1412
|
+
* conflict (per the conflict graph from item 3.1) with any currently-
|
|
1413
|
+
* running phase. Phases that conflict with all currently-running phases
|
|
1414
|
+
* wait in the queue.
|
|
1415
|
+
*
|
|
1416
|
+
* Pure scheduler — no I/O, no git, no worktree creation. The caller
|
|
1417
|
+
* provides a `runPhase` callback that takes a phase filename plus a
|
|
1418
|
+
* lane id and returns a `PhaseResult`. Worktree create/merge/cleanup
|
|
1419
|
+
* (item 3.2) lives at the call site, not here.
|
|
1420
|
+
*
|
|
1421
|
+
* Sequential fallback (item 3.7): when `laneCount === 1`, the
|
|
1422
|
+
* orchestrator processes phases strictly in the input order, one at a
|
|
1423
|
+
* time — semantically identical to the original `for (const phase of
|
|
1424
|
+
* uncheckedPhases)` loop.
|
|
1425
|
+
*
|
|
1426
|
+
* Cancellation: an injected `AbortSignal` aborts pending phases (no
|
|
1427
|
+
* new phases are dispatched) but does not interrupt phases already in
|
|
1428
|
+
* flight — that's the runPhase callback's responsibility.
|
|
1429
|
+
*/
|
|
1430
|
+
|
|
1431
|
+
interface PhaseResult {
|
|
1432
|
+
/** Phase filename (e.g., "wave_1.md"). */
|
|
1433
|
+
phaseFile: string;
|
|
1434
|
+
/** Lane id this phase ran on. */
|
|
1435
|
+
laneId: string;
|
|
1436
|
+
/** Whether the phase completed successfully (all items checked). */
|
|
1437
|
+
ok: boolean;
|
|
1438
|
+
/**
|
|
1439
|
+
* Iterations consumed by this phase. Aggregated by the caller into
|
|
1440
|
+
* a session total.
|
|
1441
|
+
*/
|
|
1442
|
+
iterations: number;
|
|
1443
|
+
/** Cost in USD attributed to this phase. */
|
|
1444
|
+
costUsd: number;
|
|
1445
|
+
/** Optional structured payload returned by `runPhase`. */
|
|
1446
|
+
payload?: unknown;
|
|
1447
|
+
/**
|
|
1448
|
+
* When `ok === false`, this signals that the orchestrator should
|
|
1449
|
+
* stop scheduling new phases (the caller's contract is "first failure
|
|
1450
|
+
* stops the run", matching the sequential loop's behavior).
|
|
1451
|
+
*/
|
|
1452
|
+
fatal?: boolean;
|
|
1453
|
+
}
|
|
1454
|
+
interface RunLanesOptions {
|
|
1455
|
+
/** Phases to run, in queue order. */
|
|
1456
|
+
phases: string[];
|
|
1457
|
+
/** Conflict graph from item 3.1. */
|
|
1458
|
+
conflictGraph: ConflictGraph;
|
|
1459
|
+
/**
|
|
1460
|
+
* Number of parallel lanes. `1` falls back to sequential semantics
|
|
1461
|
+
* (no overlap, phases dispatched in `phases` order).
|
|
1462
|
+
*/
|
|
1463
|
+
laneCount: number;
|
|
1464
|
+
/**
|
|
1465
|
+
* Run a single phase. Receives the phase filename plus the lane id
|
|
1466
|
+
* the caller should use for logging / worktree naming. Must return a
|
|
1467
|
+
* `PhaseResult`. Errors thrown here propagate up to `runLanes`'s
|
|
1468
|
+
* caller — the orchestrator does not catch them.
|
|
1469
|
+
*/
|
|
1470
|
+
runPhase: (phaseFile: string, laneId: string) => Promise<PhaseResult>;
|
|
1471
|
+
/**
|
|
1472
|
+
* Optional cancellation. When aborted, no new phases are dispatched.
|
|
1473
|
+
* In-flight phases continue until their own runPhase returns.
|
|
1474
|
+
*/
|
|
1475
|
+
abortSignal?: AbortSignal;
|
|
1476
|
+
/**
|
|
1477
|
+
* Optional structured logger for scheduling decisions. Tests pass a
|
|
1478
|
+
* recording function; production passes a pino childLogger.
|
|
1479
|
+
*/
|
|
1480
|
+
logger?: {
|
|
1481
|
+
info?: (objOrMsg: unknown, msg?: string) => void;
|
|
1482
|
+
debug?: (objOrMsg: unknown, msg?: string) => void;
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
interface RunLanesResult {
|
|
1486
|
+
/** Per-phase results, in completion order. */
|
|
1487
|
+
results: PhaseResult[];
|
|
1488
|
+
/** Phases that were skipped because the run aborted. */
|
|
1489
|
+
skipped: string[];
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Run phases through N lanes with conflict-aware scheduling.
|
|
1493
|
+
*
|
|
1494
|
+
* Algorithm:
|
|
1495
|
+
* - Maintain a queue of pending phases (input order).
|
|
1496
|
+
* - Maintain a set of currently-running phases.
|
|
1497
|
+
* - Each scheduling step: fill idle lanes by picking the first queue
|
|
1498
|
+
* entry that doesn't conflict with anything running.
|
|
1499
|
+
* - Wait for any lane to finish (Promise.race), record its result,
|
|
1500
|
+
* re-evaluate the queue, repeat.
|
|
1501
|
+
* - Stop scheduling new phases when (a) the abort signal fires, OR
|
|
1502
|
+
* (b) any phase returns `fatal: true`.
|
|
1503
|
+
*
|
|
1504
|
+
* Sequential fallback (`laneCount === 1`): the queue is drained in
|
|
1505
|
+
* order, one phase at a time — `Promise.race` over a single promise
|
|
1506
|
+
* is just `await` semantics.
|
|
1507
|
+
*/
|
|
1508
|
+
declare function runLanes(opts: RunLanesOptions): Promise<RunLanesResult>;
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Scope-validator module for the autopilot (item 4.2).
|
|
1512
|
+
*
|
|
1513
|
+
* After each iteration, the orchestrator compares the files the agent
|
|
1514
|
+
* actually touched (`git diff --name-only <baseRef>`) against the
|
|
1515
|
+
* union of files declared in the current phase's plan-state items
|
|
1516
|
+
* (`PlanItem.files[]`). Mismatches are logged as warnings:
|
|
1517
|
+
*
|
|
1518
|
+
* - extra: agent edited a file NOT in the plan → "Scope drift"
|
|
1519
|
+
* - missing: plan expects a file but the agent didn't touch it → "Incomplete"
|
|
1520
|
+
*
|
|
1521
|
+
* Validation is informational — it never blocks the loop. Both arrays
|
|
1522
|
+
* may be empty on a clean iteration.
|
|
1523
|
+
*/
|
|
1524
|
+
|
|
1525
|
+
declare const execFileDefault$2: typeof execFile.__promisify__;
|
|
1526
|
+
interface ScopeValidation {
|
|
1527
|
+
/** Files the agent edited that are NOT in the expected list (scope drift). */
|
|
1528
|
+
extra: string[];
|
|
1529
|
+
/** Files the expected list contains but the agent did NOT edit (incomplete). */
|
|
1530
|
+
missing: string[];
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Compare two file lists and return the symmetric differences.
|
|
1534
|
+
*
|
|
1535
|
+
* Both inputs are normalized — duplicates removed, sorted alphabetically.
|
|
1536
|
+
* Path comparison is exact (no canonicalization); callers should pass
|
|
1537
|
+
* paths in the same form (e.g. all relative-to-cwd).
|
|
1538
|
+
*/
|
|
1539
|
+
declare function validateScope(expected: string[], actual: string[]): ScopeValidation;
|
|
1540
|
+
interface GetChangedFilesOptions {
|
|
1541
|
+
/**
|
|
1542
|
+
* Test-only: inject execFile replacement.
|
|
1543
|
+
* @internal
|
|
1544
|
+
*/
|
|
1545
|
+
_deps?: {
|
|
1546
|
+
execFile?: typeof execFileDefault$2;
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Return the list of files changed in `cwd` since `baseRef` (a git SHA
|
|
1551
|
+
* or ref). Uses `git diff --name-only` so committed AND uncommitted
|
|
1552
|
+
* working-tree changes both count.
|
|
1553
|
+
*
|
|
1554
|
+
* Never throws — degrades to `[]` on git failure so callers can treat
|
|
1555
|
+
* "git unavailable" the same as "no files changed".
|
|
1556
|
+
*/
|
|
1557
|
+
declare function getChangedFiles(cwd: string, baseRef: string, opts?: GetChangedFilesOptions): Promise<string[]>;
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Plan-validator module for the autopilot (item 4.5).
|
|
1561
|
+
*
|
|
1562
|
+
* Validates the structural integrity of a plan before execution starts:
|
|
1563
|
+
* - main.md exists for directory plans
|
|
1564
|
+
* - every phase file referenced in main.md exists on disk
|
|
1565
|
+
* - every phase file has at least one item with `intent:`
|
|
1566
|
+
* - soft warnings for items missing `files:`, `tests:`, or `verify:`
|
|
1567
|
+
*
|
|
1568
|
+
* Errors block execution (the orchestrator returns early). Warnings are
|
|
1569
|
+
* informational — the run continues so the agent can still patch the
|
|
1570
|
+
* plan in-flight.
|
|
1571
|
+
*
|
|
1572
|
+
* Pure function: only reads `fs` synchronously. Never throws — degrades
|
|
1573
|
+
* to `{ errors: [], warnings: [...] }` on parse failure so callers can
|
|
1574
|
+
* always render the report.
|
|
1575
|
+
*/
|
|
1576
|
+
interface ValidationError {
|
|
1577
|
+
/** Stable machine-readable code (e.g., "missing-main", "missing-phase-file"). */
|
|
1578
|
+
code: string;
|
|
1579
|
+
/** Human-readable description of what's wrong. */
|
|
1580
|
+
message: string;
|
|
1581
|
+
/** Plan file path (relative to plan dir or absolute) when applicable. */
|
|
1582
|
+
file?: string;
|
|
1583
|
+
/** Plan-state item id (e.g., "4.1") when applicable. */
|
|
1584
|
+
itemId?: string;
|
|
1585
|
+
}
|
|
1586
|
+
interface ValidationWarning {
|
|
1587
|
+
code: string;
|
|
1588
|
+
message: string;
|
|
1589
|
+
file?: string;
|
|
1590
|
+
itemId?: string;
|
|
1591
|
+
}
|
|
1592
|
+
interface ValidationReport {
|
|
1593
|
+
errors: ValidationError[];
|
|
1594
|
+
warnings: ValidationWarning[];
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Validate the plan at `planPath`. Returns a `ValidationReport`.
|
|
1598
|
+
*
|
|
1599
|
+
* For directory plans (path is a directory), checks main.md + each
|
|
1600
|
+
* referenced phase file. For single-file plans, only the soft per-item
|
|
1601
|
+
* checks apply.
|
|
1602
|
+
*
|
|
1603
|
+
* Never throws — any I/O failure becomes a warning rather than a thrown
|
|
1604
|
+
* exception so callers can degrade gracefully.
|
|
1605
|
+
*/
|
|
1606
|
+
declare function validatePlan(planPath: string): ValidationReport;
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Git worktree helpers for parallel-lane execution (item 3.2).
|
|
1610
|
+
*
|
|
1611
|
+
* Each parallel lane runs against its own worktree branched from the
|
|
1612
|
+
* current HEAD. On phase completion, the worktree's branch is merged
|
|
1613
|
+
* back into the main repo with `--no-ff` to preserve per-phase commits.
|
|
1614
|
+
*
|
|
1615
|
+
* All git invocations follow the same `execFile + try/catch` shape used
|
|
1616
|
+
* by `loop.ts` (lines 126-146 — see file-level comment in that module).
|
|
1617
|
+
*
|
|
1618
|
+
* Cleanup is best-effort: failures log warnings via the injected logger
|
|
1619
|
+
* but never throw. Orphaned worktrees on disk surface in the debrief
|
|
1620
|
+
* (item 3.6 wiring) so the user can manually `git worktree remove --force`.
|
|
1621
|
+
*
|
|
1622
|
+
* Pure runtime — no test pollution. Tests inject `_deps.execFile` to mock
|
|
1623
|
+
* git invocations.
|
|
1624
|
+
*/
|
|
1625
|
+
|
|
1626
|
+
declare const execFileDefault$1: typeof execFile.__promisify__;
|
|
1627
|
+
/**
|
|
1628
|
+
* Minimal logger surface — accepts any subset of pino-like methods so
|
|
1629
|
+
* tests can pass a no-op or a recording array. The real call sites pass
|
|
1630
|
+
* pino childLoggers.
|
|
1631
|
+
*/
|
|
1632
|
+
interface WorktreeLogger {
|
|
1633
|
+
warn?: (objOrMsg: unknown, msg?: string) => void;
|
|
1634
|
+
info?: (objOrMsg: unknown, msg?: string) => void;
|
|
1635
|
+
debug?: (objOrMsg: unknown, msg?: string) => void;
|
|
1636
|
+
}
|
|
1637
|
+
interface WorktreeHandle {
|
|
1638
|
+
/** Filesystem path of the new worktree. */
|
|
1639
|
+
path: string;
|
|
1640
|
+
/** Branch name created for this worktree. */
|
|
1641
|
+
branch: string;
|
|
1642
|
+
/**
|
|
1643
|
+
* Best-effort cleanup: `git worktree remove` then `git branch -D`.
|
|
1644
|
+
* Failures are logged via the logger passed to createWorktree and never
|
|
1645
|
+
* thrown. Safe to call multiple times (subsequent calls no-op).
|
|
1646
|
+
*/
|
|
1647
|
+
cleanup: () => Promise<void>;
|
|
1648
|
+
}
|
|
1649
|
+
interface CreateWorktreeOptions {
|
|
1650
|
+
/** Slug for the lane — used in branch and path naming. */
|
|
1651
|
+
laneSlug: string;
|
|
1652
|
+
/** Optional logger for cleanup-warning emission. */
|
|
1653
|
+
logger?: WorktreeLogger;
|
|
1654
|
+
/** Test-only: inject execFile replacement. */
|
|
1655
|
+
_deps?: {
|
|
1656
|
+
execFile?: typeof execFileDefault$1;
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
interface MergeWorktreeOptions {
|
|
1660
|
+
/** Branch name to merge back into the current HEAD of repoRoot. */
|
|
1661
|
+
branch: string;
|
|
1662
|
+
/** Test-only: inject execFile replacement. */
|
|
1663
|
+
_deps?: {
|
|
1664
|
+
execFile?: typeof execFileDefault$1;
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
interface MergeResult {
|
|
1668
|
+
ok: boolean;
|
|
1669
|
+
/** Files reported as conflicting by `git merge`, when ok=false. */
|
|
1670
|
+
conflicts?: string[];
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Create a new git worktree branched from the current HEAD of `repoRoot`.
|
|
1674
|
+
*
|
|
1675
|
+
* Path: `<repoRoot>/.agent/worktrees/<laneSlug>-<epoch>`
|
|
1676
|
+
* Branch: `autopilot/<laneSlug>`
|
|
1677
|
+
*
|
|
1678
|
+
* The `.agent/worktrees/` parent matches the existing `.agent/` convention
|
|
1679
|
+
* for kill-switch and status state — keeps lane artefacts grouped with
|
|
1680
|
+
* other autopilot runtime files.
|
|
1681
|
+
*
|
|
1682
|
+
* Throws on failure (the caller decides whether to fall back to sequential).
|
|
1683
|
+
* Cleanup-via-handle is best-effort and logs only.
|
|
1684
|
+
*/
|
|
1685
|
+
declare function createWorktree(repoRoot: string, opts: CreateWorktreeOptions): Promise<WorktreeHandle>;
|
|
1686
|
+
/**
|
|
1687
|
+
* Merge a worktree's branch back into the current HEAD of repoRoot.
|
|
1688
|
+
* Uses `git merge --no-ff` so each phase keeps an explicit merge commit
|
|
1689
|
+
* in history, making it trivial to bisect across parallel lanes.
|
|
1690
|
+
*
|
|
1691
|
+
* Returns `{ ok: true }` on success. On merge conflict, returns
|
|
1692
|
+
* `{ ok: false, conflicts: [...] }` and aborts the merge so the
|
|
1693
|
+
* working tree is restored — the caller falls back to sequential
|
|
1694
|
+
* execution for that phase per item 3.2's conventions.
|
|
1695
|
+
*/
|
|
1696
|
+
declare function mergeWorktree(repoRoot: string, opts: MergeWorktreeOptions): Promise<MergeResult>;
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Auto-ship module for the autopilot (item 4.7).
|
|
1700
|
+
*
|
|
1701
|
+
* After all phases pass and a changeset has been generated, --ship
|
|
1702
|
+
* pushes the current branch upstream and opens a PR via `gh pr create`.
|
|
1703
|
+
*
|
|
1704
|
+
* Hard rules (mirrored from AGENTS.md and the SPEAR Resolve stage):
|
|
1705
|
+
* - Never `git push --force` or `git push -f`
|
|
1706
|
+
* - Never push to `main` or `master`
|
|
1707
|
+
* - Never `--no-verify`
|
|
1708
|
+
* - Never merge a PR — only open one
|
|
1709
|
+
*
|
|
1710
|
+
* The PR title is the plan's H1 (verbatim). The PR body is the literal
|
|
1711
|
+
* contents of main.md (or the single-file plan), passed to gh via
|
|
1712
|
+
* `--body-file` to dodge shell-escaping bugs.
|
|
1713
|
+
*
|
|
1714
|
+
* Failure modes (all surface as thrown errors so the caller can log
|
|
1715
|
+
* and degrade gracefully):
|
|
1716
|
+
* - Branch is `main`/`master`/detached HEAD → ABORT
|
|
1717
|
+
* - `git push` returns non-zero → ABORT
|
|
1718
|
+
* - `gh` is not installed or not authenticated → ABORT
|
|
1719
|
+
* - PR already exists for the branch → ABORT (call /ship to update)
|
|
1720
|
+
*/
|
|
1721
|
+
|
|
1722
|
+
declare const execFileDefault: typeof execFile.__promisify__;
|
|
1723
|
+
interface AutoShipOptions {
|
|
1724
|
+
/** Plan path (file or directory). The PR title comes from the plan's H1. */
|
|
1725
|
+
planPath: string;
|
|
1726
|
+
/** Repo root (used as cwd for git/gh invocations). */
|
|
1727
|
+
repoRoot: string;
|
|
1728
|
+
/**
|
|
1729
|
+
* Test-only: inject execFile replacement.
|
|
1730
|
+
* @internal
|
|
1731
|
+
*/
|
|
1732
|
+
_deps?: {
|
|
1733
|
+
execFile?: typeof execFileDefault;
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
interface AutoShipResult {
|
|
1737
|
+
prUrl: string;
|
|
1738
|
+
branch: string;
|
|
1739
|
+
/** Title used for the PR. */
|
|
1740
|
+
title: string;
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Push the current branch and open a PR. Returns the PR URL on success;
|
|
1744
|
+
* throws on any of the abort conditions documented at the module top.
|
|
1745
|
+
*/
|
|
1746
|
+
declare function autoShip(opts: AutoShipOptions): Promise<AutoShipResult>;
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Session state derivation from event streams.
|
|
1750
|
+
*
|
|
1751
|
+
* `deriveState` is a pure function: given a sequence of SessionEvents,
|
|
1752
|
+
* it returns a `SessionHandle` describing the current state of the session.
|
|
1753
|
+
* It handles partial streams (process died mid-session) and all event types.
|
|
1754
|
+
*/
|
|
1755
|
+
|
|
1756
|
+
type SessionStatus = "running" | "enriching" | "verifying" | "complete" | "error" | "stale";
|
|
1757
|
+
interface SessionHandle {
|
|
1758
|
+
/** Derived from planPath + startedAt — stable across polls. */
|
|
1759
|
+
id: string;
|
|
1760
|
+
planPath: string;
|
|
1761
|
+
cwd: string;
|
|
1762
|
+
/** fast-model flag from session:start */
|
|
1763
|
+
fast: boolean;
|
|
1764
|
+
/** resume flag from session:start */
|
|
1765
|
+
resume: boolean;
|
|
1766
|
+
status: SessionStatus;
|
|
1767
|
+
currentPhase?: {
|
|
1768
|
+
phase: string;
|
|
1769
|
+
current: number;
|
|
1770
|
+
total: number;
|
|
1771
|
+
};
|
|
1772
|
+
currentIteration?: {
|
|
1773
|
+
iteration: number;
|
|
1774
|
+
max: number;
|
|
1775
|
+
};
|
|
1776
|
+
totalIterations: number;
|
|
1777
|
+
cost: number;
|
|
1778
|
+
startedAt: string;
|
|
1779
|
+
lastEventAt: string;
|
|
1780
|
+
error?: string;
|
|
1781
|
+
exitReason?: string;
|
|
1782
|
+
enrichProgress?: {
|
|
1783
|
+
done: number;
|
|
1784
|
+
total: number;
|
|
1785
|
+
};
|
|
1786
|
+
verifyProgress?: {
|
|
1787
|
+
passed: number;
|
|
1788
|
+
total: number;
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Pure reduce over a sequence of SessionEvents.
|
|
1793
|
+
*
|
|
1794
|
+
* Returns `null` if no `session:start` event is found (empty or pre-start stream).
|
|
1795
|
+
* Handles partial streams: a stream without `session:done` is treated as "running"
|
|
1796
|
+
* (or "enriching"/"verifying" depending on the last active event).
|
|
1797
|
+
*/
|
|
1798
|
+
declare function deriveState(events: SessionEvent[]): SessionHandle | null;
|
|
1799
|
+
|
|
1800
|
+
/**
|
|
1801
|
+
* Changeset generator for the autopilot (item 4.6).
|
|
1802
|
+
*
|
|
1803
|
+
* After all phases complete successfully, the autopilot generates a
|
|
1804
|
+
* changeset file in the repo's `.changeset/` directory. The file follows
|
|
1805
|
+
* Changesets v2 format:
|
|
1806
|
+
*
|
|
1807
|
+
* ---
|
|
1808
|
+
* "@glrs-dev/harness-plugin-opencode": <bump-level>
|
|
1809
|
+
* ---
|
|
1810
|
+
*
|
|
1811
|
+
* <description>
|
|
1812
|
+
*
|
|
1813
|
+
* Bump-level inference (per the wave_4 spec):
|
|
1814
|
+
* - "fix"/"bug" in title → patch
|
|
1815
|
+
* - "remove"/"break"/"v2" in title → major
|
|
1816
|
+
* - otherwise → minor (default)
|
|
1817
|
+
*
|
|
1818
|
+
* The package name is hard-coded to `@glrs-dev/harness-plugin-opencode`
|
|
1819
|
+
* — the autopilot only ships changesets for that package today.
|
|
1820
|
+
*
|
|
1821
|
+
* Filename format: `<slug>-<random6>.md` to avoid collisions when the
|
|
1822
|
+
* same plan re-runs.
|
|
1823
|
+
*/
|
|
1824
|
+
type BumpLevel = "patch" | "minor" | "major";
|
|
1825
|
+
interface GenerateChangesetResult {
|
|
1826
|
+
/** Absolute path of the generated changeset file. */
|
|
1827
|
+
path: string;
|
|
1828
|
+
/** The contents written to the file. */
|
|
1829
|
+
content: string;
|
|
1830
|
+
/** Bump level chosen. */
|
|
1831
|
+
bumpLevel: BumpLevel;
|
|
1832
|
+
}
|
|
1833
|
+
interface GenerateChangesetOptions {
|
|
1834
|
+
/** Override the package name (default: "@glrs-dev/harness-plugin-opencode"). */
|
|
1835
|
+
packageName?: string;
|
|
1836
|
+
/** Override the bump level (default: inferred from title keywords). */
|
|
1837
|
+
bumpLevel?: BumpLevel;
|
|
1838
|
+
/**
|
|
1839
|
+
* Test-only: deterministic random suffix for filename collision avoidance.
|
|
1840
|
+
* @internal
|
|
1841
|
+
*/
|
|
1842
|
+
_randomSuffix?: () => string;
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1845
|
+
* Generate a Changesets v2 changeset file for a completed plan.
|
|
1846
|
+
*
|
|
1847
|
+
* Writes to `<repoRoot>/.changeset/<slug>-<random6>.md`. Creates the
|
|
1848
|
+
* `.changeset/` directory if missing (Changesets ships with one but
|
|
1849
|
+
* this is defensive in case a worktree drops it).
|
|
1850
|
+
*/
|
|
1851
|
+
declare function generateChangeset(planPath: string, repoRoot: string, opts?: GenerateChangesetOptions): Promise<GenerateChangesetResult>;
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Plan session runner for the interactive autopilot orchestrator.
|
|
1855
|
+
*
|
|
1856
|
+
* Runs a headless opencode session with the @plan agent, sends the
|
|
1857
|
+
* scope path as the prompt, and detects the plan output path from
|
|
1858
|
+
* the filesystem (multi-file directory or single-file .md).
|
|
1859
|
+
*/
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* Injectable server dependencies for testing.
|
|
1863
|
+
* @internal
|
|
1864
|
+
*/
|
|
1865
|
+
interface PlanSessionDeps {
|
|
1866
|
+
/** Override filesystem existence checks for testing. */
|
|
1867
|
+
existsSync?: (p: string) => boolean;
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Run a headless @plan session.
|
|
1871
|
+
*
|
|
1872
|
+
* Starts an opencode server, creates a session with the @plan agent,
|
|
1873
|
+
* sends a prompt instructing it to read the scope and produce a plan,
|
|
1874
|
+
* then detects the plan output path from the filesystem.
|
|
1875
|
+
*
|
|
1876
|
+
* @plan is headless — autoRejectPermissions is true so it can't
|
|
1877
|
+
* deadlock waiting on a human response.
|
|
1878
|
+
*/
|
|
1879
|
+
declare function runPlanSession(opts: PlanSessionOptions & {
|
|
1880
|
+
timeoutMs?: number;
|
|
1881
|
+
_deps?: PlanSessionDeps;
|
|
1882
|
+
adapter: AgentAdapter;
|
|
1883
|
+
}): Promise<PlanSessionResult>;
|
|
1884
|
+
|
|
1885
|
+
/**
|
|
1886
|
+
* Shared type definitions for the scoper session.
|
|
1887
|
+
* Defined here (in autopilot) so both the autopilot orchestrator
|
|
1888
|
+
* (interactive.ts) and the CLI scoper command can import them without
|
|
1889
|
+
* creating circular dependencies.
|
|
1890
|
+
*/
|
|
1891
|
+
interface ScoperSessionOptions {
|
|
1892
|
+
/** Directory where scope.md will be written. */
|
|
1893
|
+
planDir: string;
|
|
1894
|
+
/** Slug for the plan (used to name the subdirectory). */
|
|
1895
|
+
slug: string;
|
|
1896
|
+
/** The user's initial goal text (embedded in the first prompt). */
|
|
1897
|
+
initialGoal: string;
|
|
1898
|
+
/** Timeout in milliseconds per turn (default: 5 minutes). */
|
|
1899
|
+
timeoutMs?: number;
|
|
1900
|
+
/**
|
|
1901
|
+
* When provided, the scoper's initial prompt includes this plan content
|
|
1902
|
+
* so the scoper can ground its questions in the existing plan.
|
|
1903
|
+
*/
|
|
1904
|
+
existingPlanContent?: string;
|
|
1905
|
+
/**
|
|
1906
|
+
* Injectable dependencies for testing.
|
|
1907
|
+
* @internal — concrete types are defined in the CLI's scoper.ts
|
|
1908
|
+
*/
|
|
1909
|
+
_deps?: Record<string, unknown>;
|
|
1910
|
+
}
|
|
1911
|
+
interface ScoperSessionResult {
|
|
1912
|
+
scopePath: string;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
export { type AdapterName, type SessionResult$1 as AdapterSessionResult, type AgentAdapter, type AgentHandle, type AutopilotLogger, type CLIFlags, type Checkpoint, type CostUpdateEvent, type CredentialExpiredEvent, type EnrichDoneEvent, type EnrichFileDoneEvent, type EnrichFileErrorEvent, type EnrichFileSkipEvent, type EnrichFileStartEvent, type EnrichStartEvent, type ErrorEvent, EventStreamReader, EventStreamWriter, type IterationDoneEvent, type IterationStartEvent, type LoopExitReason, type LoopResult, type LoopSessionDeps, type LoopSessionOptions, MAX_ITERATIONS, MAX_ITERATIONS_PER_PHASE_BY_TIER, type PhaseDoneEvent, type PhaseResult, type PhaseStartEvent, type PlanItem, type PlanSessionOptions, type PlanSessionResult, type PlanState, type RalphLoopOptions, STALL_MS, STALL_MS_BY_TIER, STATUS_INTERVAL_MS, STRUGGLE_THRESHOLD, type ScoperSessionOptions, type ScoperSessionResult, type SessionDoneEvent, type SessionEvent, SessionEventEmitter, type SessionHandle, type SessionResult, SessionRunner, type SessionRunnerDeps, type SessionRunnerOptions, type SessionStartEvent, type SessionStatus, StruggleDetector, TIMEOUT_MS, type ThinkingEvent, type ToolCallEvent, type VerifyDoneEvent, type VerifyResult, type VerifyResultEvent, type VerifyStartEvent, type WorktreeHandle, applyCLIOverrides, autoShip, buildConflictGraph, checkKillSwitch, childLogger, createAutopilotLogger, createStatusHeartbeat, createWorktree, deleteCheckpoint, deriveState, detectSentinel, detectSpecPhases, enrichPlanForFastModel, filterUncheckedSpecPhases, formatCost, formatElapsed, generateChangeset, getChangedFiles, hasParallelism, hasSpec, isFreeformFile, markPhaseCompleted, mergeWorktree, parseItems, parsePlanState, parseSpecItems, readCheckpoint, readSpecConstraints, readSpecGoal, recordHead, resetSoft, resolveModel, runLanes, runLoopSession, runPlanSession, runRalphLoop, runVerifyCommands, validateMainSpec, validatePhaseSpec, validatePlan, validateScope, writeCheckpoint };
|