@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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/assets/autopilot-harness-hook.mjs +228 -36
  3. package/assets/vendor/runtime.mjs +397 -15
  4. package/dist/assets/autopilot-harness-hook.mjs +228 -36
  5. package/dist/assets/vendor/runtime.mjs +397 -15
  6. package/dist/bin.js +6 -3
  7. package/dist/bin.js.map +1 -1
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/init/claude-settings-merge.d.ts +51 -0
  13. package/dist/init/claude-settings-merge.d.ts.map +1 -0
  14. package/dist/init/claude-settings-merge.js +318 -0
  15. package/dist/init/claude-settings-merge.js.map +1 -0
  16. package/dist/init/install.d.ts +4 -1
  17. package/dist/init/install.d.ts.map +1 -1
  18. package/dist/init/install.js +258 -83
  19. package/dist/init/install.js.map +1 -1
  20. package/dist/init/platforms.d.ts +5 -0
  21. package/dist/init/platforms.d.ts.map +1 -1
  22. package/dist/init/platforms.js +17 -0
  23. package/dist/init/platforms.js.map +1 -1
  24. package/dist/init/types.d.ts +1 -1
  25. package/dist/init/types.js +1 -1
  26. package/dist/init/wizard-helpers.d.ts.map +1 -1
  27. package/dist/init/wizard-helpers.js +21 -10
  28. package/dist/init/wizard-helpers.js.map +1 -1
  29. package/dist/status-doctor.d.ts.map +1 -1
  30. package/dist/status-doctor.js +121 -55
  31. package/dist/status-doctor.js.map +1 -1
  32. package/dist/uninstall.d.ts.map +1 -1
  33. package/dist/uninstall.js +163 -5
  34. package/dist/uninstall.js.map +1 -1
  35. package/dist/upgrade.d.ts.map +1 -1
  36. package/dist/upgrade.js +100 -2
  37. package/dist/upgrade.js.map +1 -1
  38. package/dist/vendor-entry.d.ts +4 -1
  39. package/dist/vendor-entry.d.ts.map +1 -1
  40. package/dist/vendor-entry.js +6 -2
  41. package/dist/vendor-entry.js.map +1 -1
  42. 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.1 is Cursor-first.**
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
- * Cursor hook entry — marker: autopilot-harness
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 = path.resolve(__dirname, "..", "..");
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 loadPortFromNodeModules() {
117
+ async function loadPortPackage(pkgName) {
95
118
  try {
96
119
  const require = createRequire(path.join(projectRoot, "package.json"));
97
- const resolved = require.resolve("@autopilot-harness/port-cursor");
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
- try {
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 ?? (await loadPortFromNodeModules());
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
- if (!port?.handleBeforeSubmitPrompt || !coreMod?.StateStore) {
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(store, payload, projectRoot);
148
- writeReply(JSON.stringify(result));
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
- // Vendor injects locale; core still applies config.yml. Fall back if
158
- // an older vendor/runtime lacks the factory (upgrade mid-flight).
159
- const engine =
160
- typeof coreMod.createConfiguredReviewEngine === "function"
161
- ? coreMod.createConfiguredReviewEngine(store, projectRoot)
162
- : new coreMod.ReviewEngine(store, {
163
- confirmRounds: 5,
164
- reviewScope: "executing_only",
165
- verifyEnabled: false,
166
- verifyCommands: [],
167
- maxIdleStops: 5,
168
- maxErrorsBeforePause: 0,
169
- projectRoot,
170
- });
171
- const result = port.handleStop(engine, payload);
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
- failOpen("beforeSubmitPrompt");
382
+ // Prefer the parsed event when main() assigned it; else Cursor-safe default.
383
+ failOpen(bootEvent);
192
384
  process.exitCode = 0;
193
385
  });