@autopilot-harness/cli 0.1.0 → 0.2.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/README.md +1 -1
- package/assets/autopilot-harness-hook.mjs +228 -36
- package/assets/vendor/runtime.mjs +397 -15
- package/dist/assets/autopilot-harness-hook.mjs +228 -36
- package/dist/assets/vendor/runtime.mjs +397 -15
- package/dist/bin.js +6 -3
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/init/claude-settings-merge.d.ts +51 -0
- package/dist/init/claude-settings-merge.d.ts.map +1 -0
- package/dist/init/claude-settings-merge.js +318 -0
- package/dist/init/claude-settings-merge.js.map +1 -0
- package/dist/init/install.d.ts +4 -1
- package/dist/init/install.d.ts.map +1 -1
- package/dist/init/install.js +258 -83
- package/dist/init/install.js.map +1 -1
- package/dist/init/platforms.d.ts +5 -0
- package/dist/init/platforms.d.ts.map +1 -1
- package/dist/init/platforms.js +17 -0
- package/dist/init/platforms.js.map +1 -1
- package/dist/init/types.d.ts +1 -1
- package/dist/init/types.js +1 -1
- package/dist/init/wizard-helpers.d.ts.map +1 -1
- package/dist/init/wizard-helpers.js +21 -10
- package/dist/init/wizard-helpers.js.map +1 -1
- package/dist/status-doctor.d.ts.map +1 -1
- package/dist/status-doctor.js +121 -55
- package/dist/status-doctor.js.map +1 -1
- package/dist/uninstall.d.ts.map +1 -1
- package/dist/uninstall.js +163 -5
- package/dist/uninstall.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +100 -2
- package/dist/upgrade.js.map +1 -1
- package/dist/vendor-entry.d.ts +4 -1
- package/dist/vendor-entry.d.ts.map +1 -1
- package/dist/vendor-entry.js +6 -2
- package/dist/vendor-entry.js.map +1 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CLI for **Autopilot** — a vibecoding harness that turns open-ended agent chat into **structured planning → checklist execution → multi-lens self-review**.
|
|
4
4
|
|
|
5
|
-
Bin name: `autopilot-harness`. Requires **Node.js 22+**. **v0.
|
|
5
|
+
Bin name: `autopilot-harness`. Requires **Node.js 22+**. **v0.2 ships Cursor and Claude Code.**
|
|
6
6
|
|
|
7
7
|
Autopilot does **not** guarantee bug-free software. It raises confidence that work was planned, checklist-scoped, and pressure-tested under several review lenses.
|
|
8
8
|
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Autopilot hook entry — marker: autopilot-harness
|
|
3
3
|
* Installed at .autopilot/bin/autopilot-harness-hook.mjs (copy, not symlink).
|
|
4
4
|
*
|
|
5
5
|
* Prefers bundled vendor/runtime.mjs (shipped by init/upgrade) so empty
|
|
6
6
|
* consumer projects work without @autopilot-harness/* in node_modules.
|
|
7
7
|
* Falls back to project-local packages, then fail-open.
|
|
8
|
+
*
|
|
9
|
+
* Events:
|
|
10
|
+
* Cursor: beforeSubmitPrompt | afterFileEdit | stop
|
|
11
|
+
* Claude Code: UserPromptSubmit | PostToolUse | Stop | StopFailure
|
|
8
12
|
*/
|
|
9
13
|
import fs from "node:fs";
|
|
10
14
|
import path from "node:path";
|
|
@@ -12,14 +16,29 @@ import { createRequire } from "node:module";
|
|
|
12
16
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
17
|
|
|
14
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const projectRoot =
|
|
19
|
+
const projectRoot = (() => {
|
|
20
|
+
const resolved = path.resolve(__dirname, "..", "..");
|
|
21
|
+
try {
|
|
22
|
+
return fs.realpathSync(resolved);
|
|
23
|
+
} catch {
|
|
24
|
+
return resolved;
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
const CURSOR_EVENTS = new Set([
|
|
29
|
+
"beforeSubmitPrompt",
|
|
30
|
+
"afterFileEdit",
|
|
31
|
+
"stop",
|
|
32
|
+
]);
|
|
33
|
+
const CLAUDE_EVENTS = new Set([
|
|
34
|
+
"UserPromptSubmit",
|
|
35
|
+
"PostToolUse",
|
|
36
|
+
"Stop",
|
|
37
|
+
"StopFailure",
|
|
38
|
+
]);
|
|
16
39
|
|
|
17
40
|
function parseArgs(argv) {
|
|
18
|
-
const allowed = new Set([
|
|
19
|
-
"beforeSubmitPrompt",
|
|
20
|
-
"afterFileEdit",
|
|
21
|
-
"stop",
|
|
22
|
-
]);
|
|
41
|
+
const allowed = new Set([...CURSOR_EVENTS, ...CLAUDE_EVENTS]);
|
|
23
42
|
const out = { event: "beforeSubmitPrompt" };
|
|
24
43
|
for (let i = 0; i < argv.length; i++) {
|
|
25
44
|
if (argv[i] === "--event" && argv[i + 1]) {
|
|
@@ -30,6 +49,10 @@ function parseArgs(argv) {
|
|
|
30
49
|
return out;
|
|
31
50
|
}
|
|
32
51
|
|
|
52
|
+
function isClaudeEvent(event) {
|
|
53
|
+
return CLAUDE_EVENTS.has(event);
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
async function readStdin() {
|
|
34
57
|
const chunks = [];
|
|
35
58
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -91,10 +114,10 @@ async function loadVendorRuntime() {
|
|
|
91
114
|
return tryImport(pathToFileURL(vendor).href);
|
|
92
115
|
}
|
|
93
116
|
|
|
94
|
-
async function
|
|
117
|
+
async function loadPortPackage(pkgName) {
|
|
95
118
|
try {
|
|
96
119
|
const require = createRequire(path.join(projectRoot, "package.json"));
|
|
97
|
-
const resolved = require.resolve(
|
|
120
|
+
const resolved = require.resolve(pkgName);
|
|
98
121
|
return tryImport(pathToFileURL(resolved).href);
|
|
99
122
|
} catch {
|
|
100
123
|
return null;
|
|
@@ -102,15 +125,15 @@ async function loadPortFromNodeModules() {
|
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
async function loadCoreFromNodeModules() {
|
|
105
|
-
|
|
106
|
-
const require = createRequire(path.join(projectRoot, "package.json"));
|
|
107
|
-
const resolved = require.resolve("@autopilot-harness/core");
|
|
108
|
-
return tryImport(pathToFileURL(resolved).href);
|
|
109
|
-
} catch {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
128
|
+
return loadPortPackage("@autopilot-harness/core");
|
|
112
129
|
}
|
|
113
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Fail-open shapes must match the host:
|
|
133
|
+
* - Cursor submit → { continue: true }
|
|
134
|
+
* - Claude UserPromptSubmit → {} (allow; no decision:block)
|
|
135
|
+
* - other events → {}
|
|
136
|
+
*/
|
|
114
137
|
function failOpen(event) {
|
|
115
138
|
if (event === "beforeSubmitPrompt") {
|
|
116
139
|
writeReply(JSON.stringify({ continue: true }));
|
|
@@ -128,15 +151,137 @@ function writeReply(text) {
|
|
|
128
151
|
replied = true;
|
|
129
152
|
}
|
|
130
153
|
|
|
154
|
+
function createEngine(coreMod, store) {
|
|
155
|
+
return typeof coreMod.createConfiguredReviewEngine === "function"
|
|
156
|
+
? coreMod.createConfiguredReviewEngine(store, projectRoot)
|
|
157
|
+
: new coreMod.ReviewEngine(store, {
|
|
158
|
+
confirmRounds: 5,
|
|
159
|
+
reviewScope: "executing_only",
|
|
160
|
+
verifyEnabled: false,
|
|
161
|
+
verifyCommands: [],
|
|
162
|
+
maxIdleStops: 5,
|
|
163
|
+
maxErrorsBeforePause: 0,
|
|
164
|
+
projectRoot,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cursorStopHandler(port) {
|
|
169
|
+
if (typeof port.handleCursorStop === "function") {
|
|
170
|
+
return port.handleCursorStop;
|
|
171
|
+
}
|
|
172
|
+
// Dual/legacy vendor: deprecated handleStop === Cursor only when Cursor
|
|
173
|
+
// submit exists. Never fall through to Claude-only package handleStop.
|
|
174
|
+
if (
|
|
175
|
+
typeof port.handleStop === "function" &&
|
|
176
|
+
typeof port.handleBeforeSubmitPrompt === "function"
|
|
177
|
+
) {
|
|
178
|
+
return port.handleStop;
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve Claude Stop handler without falling through to Cursor's handleStop
|
|
185
|
+
* on the dual-port vendor (where deprecated `handleStop` === handleCursorStop).
|
|
186
|
+
* node_modules `@autopilot-harness/port-claude-code` exports Claude as handleStop
|
|
187
|
+
* and has no Cursor submit handler.
|
|
188
|
+
*/
|
|
189
|
+
function claudeStopHandler(port) {
|
|
190
|
+
if (typeof port.handleClaudeStop === "function") {
|
|
191
|
+
return port.handleClaudeStop;
|
|
192
|
+
}
|
|
193
|
+
if (
|
|
194
|
+
typeof port.handleStop === "function" &&
|
|
195
|
+
typeof port.handleBeforeSubmitPrompt !== "function"
|
|
196
|
+
) {
|
|
197
|
+
return port.handleStop;
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cursor IDE may also execute `.claude/settings.json` Stop hooks ("claude-project
|
|
204
|
+
* config") on the same user Stop. Those payloads are Cursor-shaped (`status`,
|
|
205
|
+
* lowercase `hook_event_name: "stop"`). Route them to the Cursor port so abort
|
|
206
|
+
* halts instead of Claude recover (decision:block), which Cursor merges back
|
|
207
|
+
* into followup and fights the real abort path.
|
|
208
|
+
*
|
|
209
|
+
* Heuristic (order matters):
|
|
210
|
+
* 1) Explicit Claude hook names (`Stop` / `StopFailure`) → not Cursor
|
|
211
|
+
* 2) Lowercase `stop` → Cursor
|
|
212
|
+
* 3) `stop_hook_active` present (Claude continuum) → not Cursor
|
|
213
|
+
* 4) Cursor status vocab + `conversation_id` → Cursor; bare `session_id` → Claude
|
|
214
|
+
*/
|
|
215
|
+
function isCursorShapedStopPayload(payload) {
|
|
216
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
const hookName = String(
|
|
220
|
+
payload.hook_event_name ?? payload.hookEventName ?? "",
|
|
221
|
+
).trim();
|
|
222
|
+
if (hookName === "Stop" || /^stopfailure$/i.test(hookName)) return false;
|
|
223
|
+
if (hookName === "stop") return true;
|
|
224
|
+
|
|
225
|
+
// Claude Stop threads stop_hook_active (bool); Cursor uses loop_count.
|
|
226
|
+
if (
|
|
227
|
+
typeof payload.stop_hook_active === "boolean" ||
|
|
228
|
+
typeof payload.stopHookActive === "boolean"
|
|
229
|
+
) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const statusRaw = String(payload.status ?? "")
|
|
234
|
+
.toLowerCase()
|
|
235
|
+
.trim();
|
|
236
|
+
const cursorStatus =
|
|
237
|
+
statusRaw === "aborted" ||
|
|
238
|
+
statusRaw === "cancelled" ||
|
|
239
|
+
statusRaw === "canceled" ||
|
|
240
|
+
statusRaw === "completed" ||
|
|
241
|
+
statusRaw === "error" ||
|
|
242
|
+
statusRaw === "failed";
|
|
243
|
+
if (!cursorStatus) return false;
|
|
244
|
+
|
|
245
|
+
const conversationId = String(
|
|
246
|
+
payload.conversation_id ?? payload.conversationId ?? "",
|
|
247
|
+
).trim();
|
|
248
|
+
if (conversationId) return true;
|
|
249
|
+
|
|
250
|
+
const sessionId = String(
|
|
251
|
+
payload.session_id ?? payload.sessionId ?? "",
|
|
252
|
+
).trim();
|
|
253
|
+
// Claude-shaped id without conversation_id → keep Claude path
|
|
254
|
+
if (sessionId) return false;
|
|
255
|
+
|
|
256
|
+
// Abort/cancel with no ids: prefer Cursor halt (no-op {}) over Claude recover
|
|
257
|
+
return (
|
|
258
|
+
statusRaw === "aborted" ||
|
|
259
|
+
statusRaw === "cancelled" ||
|
|
260
|
+
statusRaw === "canceled"
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let bootEvent = "beforeSubmitPrompt";
|
|
265
|
+
|
|
131
266
|
async function main() {
|
|
132
267
|
const { event } = parseArgs(process.argv.slice(2));
|
|
268
|
+
bootEvent = event;
|
|
133
269
|
try {
|
|
134
270
|
const payload = await readStdin();
|
|
271
|
+
const claude = isClaudeEvent(event);
|
|
135
272
|
|
|
136
273
|
const vendor = await loadVendorRuntime();
|
|
137
|
-
const port = vendor
|
|
274
|
+
const port = vendor
|
|
275
|
+
? vendor
|
|
276
|
+
: claude
|
|
277
|
+
? await loadPortPackage("@autopilot-harness/port-claude-code")
|
|
278
|
+
: await loadPortPackage("@autopilot-harness/port-cursor");
|
|
138
279
|
const coreMod = vendor ?? (await loadCoreFromNodeModules());
|
|
139
|
-
|
|
280
|
+
|
|
281
|
+
const portReady = claude
|
|
282
|
+
? typeof port?.handleUserPromptSubmit === "function"
|
|
283
|
+
: typeof port?.handleBeforeSubmitPrompt === "function";
|
|
284
|
+
if (!portReady || !coreMod?.StateStore) {
|
|
140
285
|
failOpen(event);
|
|
141
286
|
return;
|
|
142
287
|
}
|
|
@@ -144,8 +289,12 @@ async function main() {
|
|
|
144
289
|
const store = new coreMod.StateStore(projectRoot);
|
|
145
290
|
try {
|
|
146
291
|
if (event === "beforeSubmitPrompt") {
|
|
147
|
-
const result = port.handleBeforeSubmitPrompt(
|
|
148
|
-
|
|
292
|
+
const result = port.handleBeforeSubmitPrompt(
|
|
293
|
+
store,
|
|
294
|
+
payload,
|
|
295
|
+
projectRoot,
|
|
296
|
+
);
|
|
297
|
+
writeReply(JSON.stringify(result ?? {}));
|
|
149
298
|
return;
|
|
150
299
|
}
|
|
151
300
|
if (event === "afterFileEdit") {
|
|
@@ -154,21 +303,63 @@ async function main() {
|
|
|
154
303
|
return;
|
|
155
304
|
}
|
|
156
305
|
if (event === "stop") {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
306
|
+
const stopFn = cursorStopHandler(port);
|
|
307
|
+
if (typeof stopFn !== "function") {
|
|
308
|
+
failOpen(event);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const result = stopFn(createEngine(coreMod, store), payload);
|
|
312
|
+
writeReply(JSON.stringify(result ?? {}));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (event === "UserPromptSubmit") {
|
|
316
|
+
const result = port.handleUserPromptSubmit(
|
|
317
|
+
store,
|
|
318
|
+
payload,
|
|
319
|
+
projectRoot,
|
|
320
|
+
);
|
|
321
|
+
writeReply(JSON.stringify(result ?? {}));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (event === "PostToolUse") {
|
|
325
|
+
port.handlePostToolUse?.(store, payload, projectRoot);
|
|
326
|
+
writeReply("{}");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (event === "Stop") {
|
|
330
|
+
// Cursor cross-fires claude-project Stop hooks with IDE stop payloads.
|
|
331
|
+
if (isCursorShapedStopPayload(payload)) {
|
|
332
|
+
const stopFn = cursorStopHandler(port);
|
|
333
|
+
if (typeof stopFn !== "function") {
|
|
334
|
+
failOpen(event);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const result = stopFn(createEngine(coreMod, store), payload);
|
|
338
|
+
writeReply(JSON.stringify(result ?? {}));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const stopFn = claudeStopHandler(port);
|
|
342
|
+
if (typeof stopFn !== "function") {
|
|
343
|
+
failOpen(event);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const result = stopFn(createEngine(coreMod, store), payload);
|
|
347
|
+
writeReply(JSON.stringify(result ?? {}));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (event === "StopFailure") {
|
|
351
|
+
let failFn = port.handleStopFailure;
|
|
352
|
+
if (typeof failFn !== "function") {
|
|
353
|
+
const stopFn = claudeStopHandler(port);
|
|
354
|
+
if (typeof stopFn === "function") {
|
|
355
|
+
failFn = (engine, p) => stopFn(engine, p, { status: "error" });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (typeof failFn !== "function") {
|
|
359
|
+
failOpen(event);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const result = failFn(createEngine(coreMod, store), payload);
|
|
172
363
|
writeReply(JSON.stringify(result ?? {}));
|
|
173
364
|
return;
|
|
174
365
|
}
|
|
@@ -188,6 +379,7 @@ async function main() {
|
|
|
188
379
|
|
|
189
380
|
main().catch((err) => {
|
|
190
381
|
console.error("[autopilot-harness] hook error:", err?.message ?? err);
|
|
191
|
-
|
|
382
|
+
// Prefer the parsed event when main() assigned it; else Cursor-safe default.
|
|
383
|
+
failOpen(bootEvent);
|
|
192
384
|
process.exitCode = 0;
|
|
193
385
|
});
|