@autopilot-harness/cli 0.1.0 → 0.2.1

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 (46) hide show
  1. package/README.md +1 -1
  2. package/assets/autopilot-harness-hook.mjs +292 -39
  3. package/assets/vendor/runtime.mjs +397 -15
  4. package/dist/assets/autopilot-harness-hook.mjs +292 -39
  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 +56 -0
  13. package/dist/init/claude-settings-merge.d.ts.map +1 -0
  14. package/dist/init/claude-settings-merge.js +347 -0
  15. package/dist/init/claude-settings-merge.js.map +1 -0
  16. package/dist/init/hooks-merge.d.ts +12 -0
  17. package/dist/init/hooks-merge.d.ts.map +1 -1
  18. package/dist/init/hooks-merge.js +55 -1
  19. package/dist/init/hooks-merge.js.map +1 -1
  20. package/dist/init/install.d.ts +4 -1
  21. package/dist/init/install.d.ts.map +1 -1
  22. package/dist/init/install.js +258 -83
  23. package/dist/init/install.js.map +1 -1
  24. package/dist/init/platforms.d.ts +5 -0
  25. package/dist/init/platforms.d.ts.map +1 -1
  26. package/dist/init/platforms.js +17 -0
  27. package/dist/init/platforms.js.map +1 -1
  28. package/dist/init/types.d.ts +1 -1
  29. package/dist/init/types.js +1 -1
  30. package/dist/init/wizard-helpers.d.ts.map +1 -1
  31. package/dist/init/wizard-helpers.js +21 -10
  32. package/dist/init/wizard-helpers.js.map +1 -1
  33. package/dist/status-doctor.d.ts.map +1 -1
  34. package/dist/status-doctor.js +132 -56
  35. package/dist/status-doctor.js.map +1 -1
  36. package/dist/uninstall.d.ts.map +1 -1
  37. package/dist/uninstall.js +163 -5
  38. package/dist/uninstall.js.map +1 -1
  39. package/dist/upgrade.d.ts.map +1 -1
  40. package/dist/upgrade.js +100 -2
  41. package/dist/upgrade.js.map +1 -1
  42. package/dist/vendor-entry.d.ts +4 -1
  43. package/dist/vendor-entry.d.ts.map +1 -1
  44. package/dist/vendor-entry.js +6 -2
  45. package/dist/vendor-entry.js.map +1 -1
  46. package/package.json +5 -4
@@ -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,24 +16,71 @@ 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
+ ]);
39
+ const KNOWN_PLATFORMS = new Set(["cursor", "claude-code"]);
16
40
 
17
41
  function parseArgs(argv) {
18
- const allowed = new Set([
19
- "beforeSubmitPrompt",
20
- "afterFileEdit",
21
- "stop",
22
- ]);
23
- const out = { event: "beforeSubmitPrompt" };
42
+ const allowed = new Set([...CURSOR_EVENTS, ...CLAUDE_EVENTS]);
43
+ const out = { event: "beforeSubmitPrompt", platform: null };
24
44
  for (let i = 0; i < argv.length; i++) {
25
45
  if (argv[i] === "--event" && argv[i + 1]) {
26
- const ev = argv[++i];
46
+ const ev = String(argv[i + 1]);
47
+ // Do not consume the next flag as a value (`--event --platform …`).
48
+ if (ev.startsWith("--")) continue;
49
+ i += 1;
27
50
  out.event = allowed.has(ev) ? ev : "beforeSubmitPrompt";
51
+ } else if (argv[i] === "--platform" && argv[i + 1]) {
52
+ const raw = String(argv[i + 1]);
53
+ if (raw.startsWith("--")) continue;
54
+ i += 1;
55
+ const p = raw.trim().toLowerCase();
56
+ out.platform = KNOWN_PLATFORMS.has(p) ? p : null;
28
57
  }
29
58
  }
30
59
  return out;
31
60
  }
32
61
 
62
+ function isClaudeEvent(event) {
63
+ return CLAUDE_EVENTS.has(event);
64
+ }
65
+
66
+ /** Strong Claude Stop markers (override a lying `--platform cursor`). */
67
+ function isClaudeShapedStopPayload(payload) {
68
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
69
+ return false;
70
+ }
71
+ const hookName = String(
72
+ payload.hook_event_name ?? payload.hookEventName ?? "",
73
+ ).trim();
74
+ if (hookName === "Stop" || /^stopfailure$/i.test(hookName)) return true;
75
+ if (
76
+ typeof payload.stop_hook_active === "boolean" ||
77
+ typeof payload.stopHookActive === "boolean"
78
+ ) {
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
33
84
  async function readStdin() {
34
85
  const chunks = [];
35
86
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -91,10 +142,10 @@ async function loadVendorRuntime() {
91
142
  return tryImport(pathToFileURL(vendor).href);
92
143
  }
93
144
 
94
- async function loadPortFromNodeModules() {
145
+ async function loadPortPackage(pkgName) {
95
146
  try {
96
147
  const require = createRequire(path.join(projectRoot, "package.json"));
97
- const resolved = require.resolve("@autopilot-harness/port-cursor");
148
+ const resolved = require.resolve(pkgName);
98
149
  return tryImport(pathToFileURL(resolved).href);
99
150
  } catch {
100
151
  return null;
@@ -102,15 +153,15 @@ async function loadPortFromNodeModules() {
102
153
  }
103
154
 
104
155
  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
- }
156
+ return loadPortPackage("@autopilot-harness/core");
112
157
  }
113
158
 
159
+ /**
160
+ * Fail-open shapes must match the host:
161
+ * - Cursor submit → { continue: true }
162
+ * - Claude UserPromptSubmit → {} (allow; no decision:block)
163
+ * - other events → {}
164
+ */
114
165
  function failOpen(event) {
115
166
  if (event === "beforeSubmitPrompt") {
116
167
  writeReply(JSON.stringify({ continue: true }));
@@ -128,15 +179,146 @@ function writeReply(text) {
128
179
  replied = true;
129
180
  }
130
181
 
182
+ function createEngine(coreMod, store) {
183
+ return typeof coreMod.createConfiguredReviewEngine === "function"
184
+ ? coreMod.createConfiguredReviewEngine(store, projectRoot)
185
+ : new coreMod.ReviewEngine(store, {
186
+ confirmRounds: 5,
187
+ reviewScope: "executing_only",
188
+ verifyEnabled: false,
189
+ verifyCommands: [],
190
+ maxIdleStops: 5,
191
+ maxErrorsBeforePause: 0,
192
+ projectRoot,
193
+ });
194
+ }
195
+
196
+ function cursorStopHandler(port) {
197
+ if (typeof port.handleCursorStop === "function") {
198
+ return port.handleCursorStop;
199
+ }
200
+ // Dual/legacy vendor: deprecated handleStop === Cursor only when Cursor
201
+ // submit exists. Never fall through to Claude-only package handleStop.
202
+ if (
203
+ typeof port.handleStop === "function" &&
204
+ typeof port.handleBeforeSubmitPrompt === "function"
205
+ ) {
206
+ return port.handleStop;
207
+ }
208
+ return undefined;
209
+ }
210
+
211
+ /**
212
+ * Resolve Claude Stop handler without falling through to Cursor's handleStop
213
+ * on the dual-port vendor (where deprecated `handleStop` === handleCursorStop).
214
+ * node_modules `@autopilot-harness/port-claude-code` exports Claude as handleStop
215
+ * and has no Cursor submit handler.
216
+ */
217
+ function claudeStopHandler(port) {
218
+ if (typeof port.handleClaudeStop === "function") {
219
+ return port.handleClaudeStop;
220
+ }
221
+ if (
222
+ typeof port.handleStop === "function" &&
223
+ typeof port.handleBeforeSubmitPrompt !== "function"
224
+ ) {
225
+ return port.handleStop;
226
+ }
227
+ return undefined;
228
+ }
229
+
230
+ /**
231
+ * Cursor IDE may also execute `.claude/settings.json` Stop hooks ("claude-project
232
+ * config") on the same user Stop. Those payloads are Cursor-shaped (`status`,
233
+ * lowercase `hook_event_name: "stop"`). Route them to the Cursor port so abort
234
+ * halts instead of Claude recover (decision:block), which Cursor merges back
235
+ * into followup and fights the real abort path.
236
+ *
237
+ * Heuristic (order matters):
238
+ * 1) Explicit Claude hook names (`Stop` / `StopFailure`) → not Cursor
239
+ * 2) Lowercase `stop` → Cursor
240
+ * 3) `stop_hook_active` present (Claude continuum) → not Cursor
241
+ * 4) Cursor status vocab + `conversation_id` → Cursor; bare `session_id` → Claude
242
+ */
243
+ function isCursorShapedStopPayload(payload) {
244
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
245
+ return false;
246
+ }
247
+ const hookName = String(
248
+ payload.hook_event_name ?? payload.hookEventName ?? "",
249
+ ).trim();
250
+ if (hookName === "Stop" || /^stopfailure$/i.test(hookName)) return false;
251
+ if (hookName === "stop") return true;
252
+
253
+ // Claude Stop threads stop_hook_active (bool); Cursor uses loop_count.
254
+ if (
255
+ typeof payload.stop_hook_active === "boolean" ||
256
+ typeof payload.stopHookActive === "boolean"
257
+ ) {
258
+ return false;
259
+ }
260
+
261
+ const statusRaw = String(payload.status ?? "")
262
+ .toLowerCase()
263
+ .trim();
264
+ const cursorStatus =
265
+ statusRaw === "aborted" ||
266
+ statusRaw === "cancelled" ||
267
+ statusRaw === "canceled" ||
268
+ statusRaw === "completed" ||
269
+ statusRaw === "error" ||
270
+ statusRaw === "failed";
271
+ if (!cursorStatus) return false;
272
+
273
+ const conversationId = String(
274
+ payload.conversation_id ?? payload.conversationId ?? "",
275
+ ).trim();
276
+ if (conversationId) return true;
277
+
278
+ const sessionId = String(
279
+ payload.session_id ?? payload.sessionId ?? "",
280
+ ).trim();
281
+ // Claude-shaped id without conversation_id → keep Claude path
282
+ if (sessionId) return false;
283
+
284
+ // Abort/cancel with no ids: prefer Cursor halt (no-op {}) over Claude recover
285
+ return (
286
+ statusRaw === "aborted" ||
287
+ statusRaw === "cancelled" ||
288
+ statusRaw === "canceled"
289
+ );
290
+ }
291
+
292
+ let bootEvent = "beforeSubmitPrompt";
293
+
131
294
  async function main() {
132
- const { event } = parseArgs(process.argv.slice(2));
295
+ const { event, platform: declaredPlatform } = parseArgs(
296
+ process.argv.slice(2),
297
+ );
298
+ bootEvent = event;
133
299
  try {
134
300
  const payload = await readStdin();
301
+ // Layer A: --platform; fall back to event-name heuristics for legacy installs.
302
+ const preferClaudePort =
303
+ declaredPlatform === "claude-code"
304
+ ? true
305
+ : declaredPlatform === "cursor"
306
+ ? false
307
+ : isClaudeEvent(event);
308
+ const claude = preferClaudePort;
135
309
 
136
310
  const vendor = await loadVendorRuntime();
137
- const port = vendor ?? (await loadPortFromNodeModules());
311
+ const port = vendor
312
+ ? vendor
313
+ : claude
314
+ ? await loadPortPackage("@autopilot-harness/port-claude-code")
315
+ : await loadPortPackage("@autopilot-harness/port-cursor");
138
316
  const coreMod = vendor ?? (await loadCoreFromNodeModules());
139
- if (!port?.handleBeforeSubmitPrompt || !coreMod?.StateStore) {
317
+
318
+ const portReady = claude
319
+ ? typeof port?.handleUserPromptSubmit === "function"
320
+ : typeof port?.handleBeforeSubmitPrompt === "function";
321
+ if (!portReady || !coreMod?.StateStore) {
140
322
  failOpen(event);
141
323
  return;
142
324
  }
@@ -144,8 +326,12 @@ async function main() {
144
326
  const store = new coreMod.StateStore(projectRoot);
145
327
  try {
146
328
  if (event === "beforeSubmitPrompt") {
147
- const result = port.handleBeforeSubmitPrompt(store, payload, projectRoot);
148
- writeReply(JSON.stringify(result));
329
+ const result = port.handleBeforeSubmitPrompt(
330
+ store,
331
+ payload,
332
+ projectRoot,
333
+ );
334
+ writeReply(JSON.stringify(result ?? {}));
149
335
  return;
150
336
  }
151
337
  if (event === "afterFileEdit") {
@@ -154,21 +340,87 @@ async function main() {
154
340
  return;
155
341
  }
156
342
  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);
343
+ const stopFn = cursorStopHandler(port);
344
+ if (typeof stopFn !== "function") {
345
+ failOpen(event);
346
+ return;
347
+ }
348
+ const result = stopFn(createEngine(coreMod, store), payload);
349
+ writeReply(JSON.stringify(result ?? {}));
350
+ return;
351
+ }
352
+ if (event === "UserPromptSubmit") {
353
+ const result = port.handleUserPromptSubmit(
354
+ store,
355
+ payload,
356
+ projectRoot,
357
+ );
358
+ writeReply(JSON.stringify(result ?? {}));
359
+ return;
360
+ }
361
+ if (event === "PostToolUse") {
362
+ port.handlePostToolUse?.(store, payload, projectRoot);
363
+ writeReply("{}");
364
+ return;
365
+ }
366
+ if (event === "Stop") {
367
+ // Layer C: payload shape vs declared --platform (cross-fire / lying argv).
368
+ let useCursorStop = false;
369
+ if (isCursorShapedStopPayload(payload)) {
370
+ useCursorStop = true;
371
+ } else if (isClaudeShapedStopPayload(payload)) {
372
+ useCursorStop = false;
373
+ } else if (declaredPlatform === "cursor") {
374
+ useCursorStop = true;
375
+ } else {
376
+ useCursorStop = false;
377
+ }
378
+ if (useCursorStop) {
379
+ let stopFn = cursorStopHandler(port);
380
+ // Non-vendor Claude-only load + Cursor-shaped cross-fire needs Cursor port.
381
+ if (typeof stopFn !== "function") {
382
+ const cursorPort = await loadPortPackage(
383
+ "@autopilot-harness/port-cursor",
384
+ );
385
+ if (cursorPort) stopFn = cursorStopHandler(cursorPort);
386
+ }
387
+ if (typeof stopFn !== "function") {
388
+ failOpen(event);
389
+ return;
390
+ }
391
+ const result = stopFn(createEngine(coreMod, store), payload);
392
+ writeReply(JSON.stringify(result ?? {}));
393
+ return;
394
+ }
395
+ let stopFn = claudeStopHandler(port);
396
+ // Non-vendor Cursor-only load + Claude-shaped Stop needs Claude port.
397
+ if (typeof stopFn !== "function") {
398
+ const claudePort = await loadPortPackage(
399
+ "@autopilot-harness/port-claude-code",
400
+ );
401
+ if (claudePort) stopFn = claudeStopHandler(claudePort);
402
+ }
403
+ if (typeof stopFn !== "function") {
404
+ failOpen(event);
405
+ return;
406
+ }
407
+ const result = stopFn(createEngine(coreMod, store), payload);
408
+ writeReply(JSON.stringify(result ?? {}));
409
+ return;
410
+ }
411
+ if (event === "StopFailure") {
412
+ let failFn = port.handleStopFailure;
413
+ if (typeof failFn !== "function") {
414
+ const stopFn = claudeStopHandler(port);
415
+ if (typeof stopFn === "function") {
416
+ failFn = (engine, p) => stopFn(engine, p, { status: "error" });
417
+ }
418
+ }
419
+ if (typeof failFn !== "function") {
420
+ failOpen(event);
421
+ return;
422
+ }
423
+ const result = failFn(createEngine(coreMod, store), payload);
172
424
  writeReply(JSON.stringify(result ?? {}));
173
425
  return;
174
426
  }
@@ -188,6 +440,7 @@ async function main() {
188
440
 
189
441
  main().catch((err) => {
190
442
  console.error("[autopilot-harness] hook error:", err?.message ?? err);
191
- failOpen("beforeSubmitPrompt");
443
+ // Prefer the parsed event when main() assigned it; else Cursor-safe default.
444
+ failOpen(bootEvent);
192
445
  process.exitCode = 0;
193
446
  });