@autopilot-harness/cli 0.2.7 → 0.2.9

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.
@@ -1890,6 +1890,95 @@ function sessionReviewRunnable(session, reviewScope) {
1890
1890
  return isChecklistExecuting(session);
1891
1891
  }
1892
1892
 
1893
+ // ../core/src/plans-bind.ts
1894
+ import path5 from "node:path";
1895
+ var MULTI_PLAN_EDIT_TRACK = "_multi";
1896
+ function isBoundRunTrackId(trackId) {
1897
+ return typeof trackId === "string" && trackId !== "_pending" && trackId !== MULTI_PLAN_EDIT_TRACK && isSafeTrackSlug(trackId);
1898
+ }
1899
+ function extractPlansSlugFromPath(filePath, projectRoot, plansDir = "plans") {
1900
+ if (!filePath || filePath.includes("\0")) return null;
1901
+ const root = normalizeProjectRoot(projectRoot);
1902
+ if (!root) return null;
1903
+ const safePlans = normalizeInProjectPlansDir(root, plansDir);
1904
+ if (!safePlans) return null;
1905
+ let abs;
1906
+ try {
1907
+ abs = path5.isAbsolute(filePath) ? path5.resolve(filePath) : path5.resolve(root, filePath);
1908
+ } catch {
1909
+ return null;
1910
+ }
1911
+ if (!isLexicallyInsideProject(root, abs)) return null;
1912
+ let rel;
1913
+ try {
1914
+ rel = path5.relative(root, abs);
1915
+ } catch {
1916
+ return null;
1917
+ }
1918
+ if (!rel || rel.startsWith("..") || path5.isAbsolute(rel)) return null;
1919
+ const parts = rel.split(/[/\\]/).filter(Boolean);
1920
+ const dirParts = safePlans.split(/[/\\]/).filter(Boolean);
1921
+ if (parts.length < dirParts.length + 2) return null;
1922
+ for (let i = 0; i < dirParts.length; i++) {
1923
+ if (parts[i] !== dirParts[i]) return null;
1924
+ }
1925
+ const slug = parts[dirParts.length];
1926
+ if (!isSafeTrackSlug(slug)) return null;
1927
+ return slug;
1928
+ }
1929
+ function notePlansDirEdit(store, conversationId, projectRoot, filePath, plansDir = "plans") {
1930
+ const id = conversationId.trim();
1931
+ if (!id || !store.isConversationIdOk(id)) return;
1932
+ const slug = extractPlansSlugFromPath(filePath, projectRoot, plansDir);
1933
+ if (!slug) return;
1934
+ const root = normalizeProjectRoot(projectRoot) ?? projectRoot;
1935
+ store.exclusiveWrite(() => {
1936
+ const existing = store.getSession(id);
1937
+ if (!existing) {
1938
+ store.upsertSession({
1939
+ conversation_id: id,
1940
+ project_root: root,
1941
+ code_root: root,
1942
+ track_id: slug,
1943
+ phase: "idle",
1944
+ armed: 0,
1945
+ paused: 0,
1946
+ checklist_path: ""
1947
+ });
1948
+ return { commit: true, value: void 0 };
1949
+ }
1950
+ if (existing.phase === "executing") {
1951
+ return { commit: false, value: void 0 };
1952
+ }
1953
+ if (existing.pending_action === "run" || existing.pending_action === "replan") {
1954
+ return { commit: false, value: void 0 };
1955
+ }
1956
+ const tid = existing.track_id;
1957
+ if (tid === MULTI_PLAN_EDIT_TRACK) {
1958
+ return { commit: false, value: void 0 };
1959
+ }
1960
+ if (!tid || tid === "_pending") {
1961
+ store.upsertSession({
1962
+ conversation_id: id,
1963
+ project_root: existing.project_root,
1964
+ code_root: existing.code_root,
1965
+ track_id: slug
1966
+ });
1967
+ return { commit: true, value: void 0 };
1968
+ }
1969
+ if (tid === slug) {
1970
+ return { commit: false, value: void 0 };
1971
+ }
1972
+ store.upsertSession({
1973
+ conversation_id: id,
1974
+ project_root: existing.project_root,
1975
+ code_root: existing.code_root,
1976
+ track_id: MULTI_PLAN_EDIT_TRACK
1977
+ });
1978
+ return { commit: true, value: void 0 };
1979
+ });
1980
+ }
1981
+
1893
1982
  // ../core/src/transcript-followup.ts
1894
1983
  import fs6 from "node:fs";
1895
1984
 
@@ -3113,10 +3202,10 @@ var ReviewEngine = class {
3113
3202
  * salvage again — the prior tip does not cover the new failure.
3114
3203
  */
3115
3204
  classifyCompletedOrphan(transcriptPath) {
3116
- const path9 = transcriptPath?.trim();
3117
- if (!path9) return "none";
3205
+ const path10 = transcriptPath?.trim();
3206
+ if (!path10) return "none";
3118
3207
  try {
3119
- const events = readTranscriptTail(path9);
3208
+ const events = readTranscriptTail(path10);
3120
3209
  const errIdx = latestUnresolvedTurnEndedErrorIndex(events);
3121
3210
  if (errIdx < 0) return "none";
3122
3211
  for (let i = events.length - 1; i > errIdx; i--) {
@@ -4210,8 +4299,8 @@ var ReviewEngine = class {
4210
4299
  let unchecked = checklist.unchecked;
4211
4300
  let next = checklist.next;
4212
4301
  let targets = null;
4213
- const path9 = lockedSession.checklist_path?.trim() ?? "";
4214
- const onChecklistPath = isChecklistExecuting(lockedSession) && path9.length > 0;
4302
+ const path10 = lockedSession.checklist_path?.trim() ?? "";
4303
+ const onChecklistPath = isChecklistExecuting(lockedSession) && path10.length > 0;
4215
4304
  if (onChecklistPath) {
4216
4305
  const refreshed = this.parseSessionChecklist(lockedSession);
4217
4306
  if (!refreshed?.checklist) {
@@ -4431,7 +4520,9 @@ function applyOn(store, conversationId, projectRoot, opts) {
4431
4520
  userMessage: `Invalid track slug "${sanitizeSessionDisplayText(raw).slice(0, 64)}".`
4432
4521
  };
4433
4522
  }
4434
- const trackId = opts?.slug ?? session?.track_id ?? "_pending";
4523
+ const prevTid = session?.track_id ?? "_pending";
4524
+ const trackId = opts?.slug ?? (session?.track_id && isBoundRunTrackId(session.track_id) ? session.track_id : "_pending");
4525
+ const checklistPath = trackId === prevTid ? session?.checklist_path ?? "" : "";
4435
4526
  const platform = resolveSessionPlatform(
4436
4527
  opts?.platform,
4437
4528
  session?.platform ?? "cursor"
@@ -4445,7 +4536,10 @@ function applyOn(store, conversationId, projectRoot, opts) {
4445
4536
  armed: 0,
4446
4537
  paused: 0,
4447
4538
  paused_reason: null,
4448
- track_id: opts?.slug ?? session.track_id,
4539
+ track_id: trackId,
4540
+ checklist_path: checklistPath,
4541
+ pending_action: null,
4542
+ track_candidates_json: null,
4449
4543
  platform
4450
4544
  });
4451
4545
  return { ok: true, session: s2 };
@@ -4460,7 +4554,10 @@ function applyOn(store, conversationId, projectRoot, opts) {
4460
4554
  paused: 0,
4461
4555
  paused_reason: null,
4462
4556
  track_id: trackId,
4463
- checklist_path: session?.checklist_path ?? ""
4557
+ checklist_path: checklistPath,
4558
+ // ON returns to planning — drop mid-flow run/replan pick state.
4559
+ pending_action: null,
4560
+ track_candidates_json: null
4464
4561
  });
4465
4562
  return { ok: true, session: s };
4466
4563
  }
@@ -4568,7 +4665,7 @@ function applyResumeReview(store, conversationId) {
4568
4665
 
4569
4666
  // ../core/src/project-config.ts
4570
4667
  import fs7 from "node:fs";
4571
- import path5 from "node:path";
4668
+ import path6 from "node:path";
4572
4669
  var MAX_CONFIG_BYTES = 1e6;
4573
4670
  var DEFAULT_PROJECT_REVIEW_CONFIG = {
4574
4671
  confirmRounds: 5,
@@ -4728,7 +4825,7 @@ function loadProjectReviewConfig(projectRoot) {
4728
4825
  if (!root) {
4729
4826
  return cloneDefaultProjectReviewConfig();
4730
4827
  }
4731
- const configPath = path5.join(root, ".autopilot", "config.yml");
4828
+ const configPath = path6.join(root, ".autopilot", "config.yml");
4732
4829
  try {
4733
4830
  const nofollow = typeof fs7.constants.O_NOFOLLOW === "number" ? fs7.constants.O_NOFOLLOW : 0;
4734
4831
  if (nofollow === 0) {
@@ -4916,11 +5013,11 @@ function createConfiguredReviewEngine(store, projectRoot, localeBundle, preloade
4916
5013
 
4917
5014
  // ../core/src/phase-actions.ts
4918
5015
  import fs9 from "node:fs";
4919
- import path7 from "node:path";
5016
+ import path8 from "node:path";
4920
5017
 
4921
5018
  // ../core/src/list-tracks.ts
4922
5019
  import fs8 from "node:fs";
4923
- import path6 from "node:path";
5020
+ import path7 from "node:path";
4924
5021
  function isRunnableTrack(t) {
4925
5022
  if (t.paused) return false;
4926
5023
  const unchecked = t.checklistTotal - t.checklistDone;
@@ -4931,7 +5028,7 @@ function readPlansDir(root, plansDir = "plans") {
4931
5028
  if (typeof plansDir !== "string" || !root || root.includes("\0") || plansDir.includes("\0")) {
4932
5029
  return [];
4933
5030
  }
4934
- const dir = path6.join(root, plansDir);
5031
+ const dir = path7.join(root, plansDir);
4935
5032
  try {
4936
5033
  const lst = fs8.lstatSync(dir);
4937
5034
  if (lst.isSymbolicLink() || !lst.isDirectory()) return [];
@@ -4993,7 +5090,7 @@ function listTracks(root, store, filter = "all", plansDir = "plans") {
4993
5090
  const slugs = readPlansDir(root, plansDir);
4994
5091
  const tracks = [];
4995
5092
  for (const slug of slugs) {
4996
- const trackDir = path6.join(root, plansDir, slug);
5093
+ const trackDir = path7.join(root, plansDir, slug);
4997
5094
  try {
4998
5095
  const lst = fs8.lstatSync(trackDir);
4999
5096
  if (lst.isSymbolicLink() || !lst.isDirectory()) continue;
@@ -5001,8 +5098,8 @@ function listTracks(root, store, filter = "all", plansDir = "plans") {
5001
5098
  } catch {
5002
5099
  continue;
5003
5100
  }
5004
- const planPath = path6.join(trackDir, "plan.md");
5005
- const checklistPath = path6.join(trackDir, "checklist.md");
5101
+ const planPath = path7.join(trackDir, "plan.md");
5102
+ const checklistPath = path7.join(trackDir, "checklist.md");
5006
5103
  let checklistTotal = 0;
5007
5104
  let checklistDone = 0;
5008
5105
  const checklistInProject = isRealpathInsideProject(root, checklistPath);
@@ -5098,13 +5195,33 @@ function canEnterExecuting(options) {
5098
5195
  }
5099
5196
 
5100
5197
  // ../core/src/phase-actions.ts
5198
+ function displayUntrusted(raw, max = 64) {
5199
+ const text = typeof raw === "string" ? raw : String(raw ?? "");
5200
+ return sanitizeSessionDisplayText(text).slice(0, max) || "?";
5201
+ }
5202
+ function isChannelANeedPick(fail) {
5203
+ return fail.needPick === true && fail.busy !== true;
5204
+ }
5205
+ function formatOneExecutorBusyMessage(trackId, conversationId) {
5206
+ const occTrack = displayUntrusted(trackId || "(unknown)");
5207
+ const rawCid = typeof conversationId === "string" ? conversationId : String(conversationId ?? "");
5208
+ const occSession = displayUntrusted(shortConversationId(rawCid) || "?") || "?";
5209
+ return `Another session is already executing (track: ${occTrack}, session: ${occSession}). Send Autopilot OFF there or wait, then retry. If the host only shows opaque blocked, run: npx @autopilot-harness/cli status or npx @autopilot-harness/cli doctor`;
5210
+ }
5211
+ function logBusyOccupier(message) {
5212
+ try {
5213
+ process.stderr.write(`[autopilot] ${message}
5214
+ `);
5215
+ } catch {
5216
+ }
5217
+ }
5101
5218
  function nowIso2() {
5102
5219
  return (/* @__PURE__ */ new Date()).toISOString();
5103
5220
  }
5104
5221
  function checklistPathFor(projectRoot, slug, plansDir) {
5105
5222
  const root = normalizeProjectRoot(projectRoot);
5106
- if (!root) return path7.join(plansDir, slug, "checklist.md");
5107
- return path7.join(root, plansDir, slug, "checklist.md");
5223
+ if (!root) return path8.join(plansDir, slug, "checklist.md");
5224
+ return path8.join(root, plansDir, slug, "checklist.md");
5108
5225
  }
5109
5226
  function sameChecklistBinding(stored, rebuilt, projectRoot) {
5110
5227
  if (stored === rebuilt) return true;
@@ -5114,8 +5231,8 @@ function sameChecklistBinding(stored, rebuilt, projectRoot) {
5114
5231
  const root = normalizeProjectRoot(projectRoot);
5115
5232
  if (!root) return false;
5116
5233
  try {
5117
- const absStored = path7.isAbsolute(stored) ? path7.resolve(stored) : path7.resolve(root, stored);
5118
- const absRebuilt = path7.isAbsolute(rebuilt) ? path7.resolve(rebuilt) : path7.resolve(root, rebuilt);
5234
+ const absStored = path8.isAbsolute(stored) ? path8.resolve(stored) : path8.resolve(root, stored);
5235
+ const absRebuilt = path8.isAbsolute(rebuilt) ? path8.resolve(rebuilt) : path8.resolve(root, rebuilt);
5119
5236
  return absStored === absRebuilt;
5120
5237
  } catch {
5121
5238
  return false;
@@ -5136,7 +5253,7 @@ function isChecklistPathAllowed(projectRoot, checklistPath) {
5136
5253
  if (!root) return false;
5137
5254
  try {
5138
5255
  fs9.lstatSync(
5139
- path7.isAbsolute(checklistPath) ? checklistPath : path7.resolve(root, checklistPath)
5256
+ path8.isAbsolute(checklistPath) ? checklistPath : path8.resolve(root, checklistPath)
5140
5257
  );
5141
5258
  return isRealpathInsideProject(root, checklistPath);
5142
5259
  } catch {
@@ -5173,8 +5290,8 @@ function upsertTrack(store, slug, checklistPath, plansDir, projectRoot) {
5173
5290
  const ts = nowIso2();
5174
5291
  const root = normalizeProjectRoot(projectRoot);
5175
5292
  if (!root) return;
5176
- const planPath = path7.join(root, plansDir, slug, "plan.md");
5177
- const briefPath = path7.join(root, plansDir, slug, "brief.md");
5293
+ const planPath = path8.join(root, plansDir, slug, "plan.md");
5294
+ const briefPath = path8.join(root, plansDir, slug, "brief.md");
5178
5295
  store.db.prepare(
5179
5296
  `INSERT INTO tracks (track_id, slug, checklist_path, plan_path, brief_path, updated_at)
5180
5297
  VALUES (?, ?, ?, ?, ?, ?)
@@ -5197,7 +5314,7 @@ function candidatePayload(tracks) {
5197
5314
  return JSON.stringify(
5198
5315
  tracks.map((t) => ({
5199
5316
  slug: t.slug,
5200
- title: t.title,
5317
+ title: sanitizeSessionDisplayText(t.title || t.slug) || t.slug,
5201
5318
  phase: t.phase,
5202
5319
  progress: `${t.checklistDone}/${t.checklistTotal}`
5203
5320
  }))
@@ -5211,7 +5328,7 @@ function resolveRunSlug(store, session, projectRoot, plansDir, requestedSlug) {
5211
5328
  if (!isSafeTrackSlug(requestedSlug)) {
5212
5329
  return {
5213
5330
  kind: "none",
5214
- userMessage: `Invalid track slug "${requestedSlug}".`
5331
+ userMessage: `Invalid track slug "${displayUntrusted(requestedSlug)}".`
5215
5332
  };
5216
5333
  }
5217
5334
  const hit = runnable.find((t) => t.slug === requestedSlug);
@@ -5220,17 +5337,18 @@ function resolveRunSlug(store, session, projectRoot, plansDir, requestedSlug) {
5220
5337
  if (!all.some((t) => t.slug === requestedSlug)) {
5221
5338
  return {
5222
5339
  kind: "none",
5223
- userMessage: `Track "${requestedSlug}" not found or has no unchecked checklist items.`
5340
+ userMessage: `Track "${displayUntrusted(requestedSlug)}" not found or has no unchecked checklist items.`
5224
5341
  };
5225
5342
  }
5226
5343
  return {
5227
5344
  kind: "none",
5228
- userMessage: `Track "${requestedSlug}" is not runnable (paused or no unchecked items).`
5345
+ userMessage: `Track "${displayUntrusted(requestedSlug)}" is not runnable (paused or no unchecked items).`
5229
5346
  };
5230
5347
  }
5231
5348
  return { kind: "slug", slug: requestedSlug };
5232
5349
  }
5233
- if (session.track_id && session.track_id !== "_pending") {
5350
+ const midPick = session.pending_action === "run" || session.pending_action === "replan";
5351
+ if (!midPick && isBoundRunTrackId(session.track_id)) {
5234
5352
  const bound = runnable.find((t) => t.slug === session.track_id);
5235
5353
  if (bound) {
5236
5354
  return { kind: "slug", slug: bound.slug };
@@ -5243,6 +5361,9 @@ function resolveRunSlug(store, session, projectRoot, plansDir, requestedSlug) {
5243
5361
  };
5244
5362
  }
5245
5363
  if (runnable.length === 1) {
5364
+ if (session.pending_action === "replan") {
5365
+ return { kind: "pick", candidates: runnable };
5366
+ }
5246
5367
  return { kind: "slug", slug: runnable[0].slug };
5247
5368
  }
5248
5369
  return { kind: "pick", candidates: runnable };
@@ -5281,18 +5402,25 @@ function applyRun(store, conversationId, projectRoot, opts) {
5281
5402
  return { ok: false, userMessage: resolved.userMessage };
5282
5403
  }
5283
5404
  if (resolved.kind === "pick") {
5405
+ const clearingStaleBind = isBoundRunTrackId(session.track_id);
5406
+ const trackForPick = clearingStaleBind ? "_pending" : session.track_id;
5284
5407
  store.upsertSession({
5285
5408
  conversation_id: conversationId,
5286
5409
  project_root: session.project_root,
5287
5410
  code_root: session.code_root,
5411
+ track_id: trackForPick,
5412
+ // Drop checklist binding with the stale track so planning state stays consistent.
5413
+ ...clearingStaleBind ? { checklist_path: "" } : {},
5288
5414
  pending_action: "run",
5289
5415
  track_candidates_json: candidatePayload(resolved.candidates),
5290
5416
  armed: 0
5291
5417
  // phase unchanged — do not write executing
5292
5418
  });
5293
- const lines = resolved.candidates.map(
5294
- (t, i) => ` ${i + 1}. ${t.slug} \u2014 ${t.title} (${t.checklistTotal - t.checklistDone}/${t.checklistTotal} left)`
5295
- ).join("\n");
5419
+ const lines = resolved.candidates.map((t, i) => {
5420
+ const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
5421
+ const left = t.checklistTotal - t.checklistDone;
5422
+ return ` ${i + 1}. ${t.slug} \u2014 ${title} (${left}/${t.checklistTotal} left)`;
5423
+ }).join("\n");
5296
5424
  return {
5297
5425
  ok: false,
5298
5426
  needPick: true,
@@ -5308,7 +5436,7 @@ Reply with a number or /autopilot-run <slug>.`
5308
5436
  if (!isSafeTrackSlug(slug)) {
5309
5437
  return {
5310
5438
  ok: false,
5311
- userMessage: `Invalid track slug "${slug}".`
5439
+ userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
5312
5440
  };
5313
5441
  }
5314
5442
  const checklistPath = checklistPathFor(projectRoot, slug, plansDir);
@@ -5325,15 +5453,20 @@ Reply with a number or /autopilot-run <slug>.`
5325
5453
  };
5326
5454
  }
5327
5455
  try {
5328
- return store.exclusiveWrite(() => {
5456
+ const result = store.exclusiveWrite(() => {
5329
5457
  if (concurrencyMode === "one_executor") {
5330
5458
  const other = store.findExecutingSession(conversationId);
5331
5459
  if (other) {
5460
+ const userMessage = formatOneExecutorBusyMessage(
5461
+ other.track_id,
5462
+ other.conversation_id
5463
+ );
5332
5464
  return {
5333
5465
  commit: false,
5334
5466
  value: {
5335
5467
  ok: false,
5336
- userMessage: `Another session is already executing (${other.track_id}). Send Autopilot OFF there or wait, then retry.`
5468
+ busy: true,
5469
+ userMessage
5337
5470
  }
5338
5471
  };
5339
5472
  }
@@ -5379,12 +5512,19 @@ Reply with a number or /autopilot-run <slug>.`
5379
5512
  }
5380
5513
  return { commit: true, value: { ok: true, session: updated } };
5381
5514
  });
5515
+ if (!result.ok && result.busy === true) {
5516
+ logBusyOccupier(result.userMessage);
5517
+ }
5518
+ return result;
5382
5519
  } catch (err) {
5383
5520
  const msg = err instanceof Error ? err.message : String(err);
5384
5521
  if (/busy|locked|SQLITE_BUSY|SQLITE_LOCKED/i.test(msg)) {
5522
+ const userMessage = "State database is busy; retry Autopilot RUN in a moment.";
5523
+ logBusyOccupier(userMessage);
5385
5524
  return {
5386
5525
  ok: false,
5387
- userMessage: "State database is busy; retry Autopilot RUN in a moment."
5526
+ busy: true,
5527
+ userMessage
5388
5528
  };
5389
5529
  }
5390
5530
  throw err;
@@ -5406,29 +5546,40 @@ function applyReplan(store, conversationId, projectRoot, opts) {
5406
5546
  projectRoot,
5407
5547
  opts?.platform
5408
5548
  );
5409
- let slug = opts?.slug ?? session.track_id;
5410
- if (slug && slug !== "_pending" && !isSafeTrackSlug(slug)) {
5549
+ let slug = opts?.slug;
5550
+ if (!slug) {
5551
+ const tid = session.track_id;
5552
+ const midPick = session.pending_action === "run" || session.pending_action === "replan";
5553
+ slug = !midPick && isBoundRunTrackId(tid) ? tid : void 0;
5554
+ }
5555
+ if (slug && !isSafeTrackSlug(slug)) {
5411
5556
  return {
5412
5557
  ok: false,
5413
- userMessage: `Invalid track slug "${slug}".`
5558
+ userMessage: `Invalid track slug "${displayUntrusted(slug)}".`
5414
5559
  };
5415
5560
  }
5416
- if (!slug || slug === "_pending") {
5561
+ if (!slug) {
5417
5562
  const all = listTracks(projectRoot, store, "all", plansDir).filter(
5418
5563
  (t) => isSafeTrackSlug(t.slug)
5419
5564
  );
5420
- if (all.length === 1) {
5565
+ if (all.length === 1 && session.pending_action !== "run") {
5421
5566
  slug = all[0].slug;
5422
- } else if (all.length > 1) {
5567
+ } else if (all.length > 1 || all.length === 1 && session.pending_action === "run") {
5568
+ const clearingStaleBind = isBoundRunTrackId(session.track_id);
5423
5569
  store.upsertSession({
5424
5570
  conversation_id: conversationId,
5425
5571
  project_root: session.project_root,
5426
5572
  code_root: session.code_root,
5573
+ track_id: clearingStaleBind ? "_pending" : session.track_id,
5574
+ ...clearingStaleBind ? { checklist_path: "" } : {},
5427
5575
  pending_action: "replan",
5428
5576
  track_candidates_json: candidatePayload(all),
5429
5577
  armed: 0
5430
5578
  });
5431
- const lines = all.map((t, i) => ` ${i + 1}. ${t.slug} \u2014 ${t.title}`).join("\n");
5579
+ const lines = all.map((t, i) => {
5580
+ const title = sanitizeSessionDisplayText(t.title || t.slug) || t.slug;
5581
+ return ` ${i + 1}. ${t.slug} \u2014 ${title}`;
5582
+ }).join("\n");
5432
5583
  return {
5433
5584
  ok: false,
5434
5585
  needPick: true,
@@ -5496,7 +5647,7 @@ function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
5496
5647
  if (pending !== "run" && pending !== "replan") {
5497
5648
  return {
5498
5649
  ok: false,
5499
- userMessage: `Unknown pending action "${pending}".`
5650
+ userMessage: `Unknown pending action "${displayUntrusted(pending)}".`
5500
5651
  };
5501
5652
  }
5502
5653
  if (!session.track_candidates_json) {
@@ -5526,14 +5677,14 @@ function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
5526
5677
  if (!slug || !isSafeTrackSlug(slug)) {
5527
5678
  return {
5528
5679
  ok: false,
5529
- userMessage: `Invalid selection "${pick}". Choose 1\u2013${candidates.length}.`
5680
+ userMessage: `Invalid selection "${displayUntrusted(pick)}". Choose 1\u2013${candidates.length}.`
5530
5681
  };
5531
5682
  }
5532
5683
  } else {
5533
5684
  if (!isSafeTrackSlug(pick)) {
5534
5685
  return {
5535
5686
  ok: false,
5536
- userMessage: `Invalid track slug "${pick}".`
5687
+ userMessage: `Invalid track slug "${displayUntrusted(pick)}".`
5537
5688
  };
5538
5689
  }
5539
5690
  slug = pick;
@@ -5542,7 +5693,7 @@ function applyTrackPick(store, conversationId, projectRoot, pick, opts) {
5542
5693
  )) {
5543
5694
  return {
5544
5695
  ok: false,
5545
- userMessage: `Unknown slug "${pick}".`
5696
+ userMessage: `Unknown slug "${displayUntrusted(pick)}".`
5546
5697
  };
5547
5698
  }
5548
5699
  }
@@ -5565,7 +5716,7 @@ import { spawnSync } from "node:child_process";
5565
5716
 
5566
5717
  // ../core/src/autopilot-ignore.ts
5567
5718
  import fs10 from "node:fs";
5568
- import path8 from "node:path";
5719
+ import path9 from "node:path";
5569
5720
  var DEFAULT_AUTOPILOT_IGNORE_TEXT = `# Autopilot \u2014 paths that do NOT trigger self-review (gitignore syntax).
5570
5721
  #
5571
5722
  # What this file is:
@@ -5764,17 +5915,17 @@ function realpathForCompare(absPath) {
5764
5915
  return fs10.realpathSync(absPath);
5765
5916
  } catch {
5766
5917
  }
5767
- let dir = path8.dirname(absPath);
5768
- const base = path8.basename(absPath);
5918
+ let dir = path9.dirname(absPath);
5919
+ const base = path9.basename(absPath);
5769
5920
  const missing = [];
5770
5921
  for (; ; ) {
5771
5922
  try {
5772
5923
  const realDir = fs10.realpathSync(dir);
5773
- return path8.join(realDir, ...missing, base);
5924
+ return path9.join(realDir, ...missing, base);
5774
5925
  } catch {
5775
- const parent = path8.dirname(dir);
5926
+ const parent = path9.dirname(dir);
5776
5927
  if (parent === dir) return absPath;
5777
- missing.unshift(path8.basename(dir));
5928
+ missing.unshift(path9.basename(dir));
5778
5929
  dir = parent;
5779
5930
  }
5780
5931
  }
@@ -5784,19 +5935,19 @@ function toProjectRelativePath(filePath, projectRoot) {
5784
5935
  if (!projectRoot?.trim()) {
5785
5936
  return normalizeRelativePath(posix);
5786
5937
  }
5787
- const root = realpathForCompare(path8.resolve(projectRoot));
5938
+ const root = realpathForCompare(path9.resolve(projectRoot));
5788
5939
  const abs = realpathForCompare(
5789
- path8.isAbsolute(posix) ? path8.resolve(posix) : path8.resolve(root, posix)
5940
+ path9.isAbsolute(posix) ? path9.resolve(posix) : path9.resolve(root, posix)
5790
5941
  );
5791
- const rel = path8.relative(root, abs);
5792
- if (rel.startsWith("..") || path8.isAbsolute(rel)) {
5942
+ const rel = path9.relative(root, abs);
5943
+ if (rel.startsWith("..") || path9.isAbsolute(rel)) {
5793
5944
  return "";
5794
5945
  }
5795
5946
  return normalizeRelativePath(rel.replace(/\\/g, "/"));
5796
5947
  }
5797
5948
  function loadAutopilotIgnorePatterns(projectRoot) {
5798
- const root = path8.resolve(projectRoot);
5799
- const filePath = path8.join(root, ".autopilotignore");
5949
+ const root = path9.resolve(projectRoot);
5950
+ const filePath = path9.join(root, ".autopilotignore");
5800
5951
  let st;
5801
5952
  try {
5802
5953
  st = fs10.lstatSync(filePath);
@@ -5858,6 +6009,20 @@ function isProductCodeEdit(filePath, opts) {
5858
6009
  }
5859
6010
 
5860
6011
  // ../ports/cursor/src/index.ts
6012
+ function normalizeBlockSubmitMessage(message) {
6013
+ return typeof message === "string" && message.trim().length > 0 ? message : "Request blocked.";
6014
+ }
6015
+ function blockSubmit(message) {
6016
+ const text = normalizeBlockSubmitMessage(message);
6017
+ return {
6018
+ continue: false,
6019
+ user_message: text,
6020
+ userMessage: text
6021
+ };
6022
+ }
6023
+ function allowSubmit() {
6024
+ return { continue: true };
6025
+ }
5861
6026
  function cid(p) {
5862
6027
  return (p.conversation_id ?? p.conversationId ?? "").trim();
5863
6028
  }
@@ -5912,7 +6077,7 @@ function normalizeCursorStopStatus(payload) {
5912
6077
  }
5913
6078
  function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5914
6079
  const conversationId = cid(payload);
5915
- if (!conversationId) return { continue: true };
6080
+ if (!conversationId) return allowSubmit();
5916
6081
  const prompt = payload.prompt ?? payload.content ?? "";
5917
6082
  try {
5918
6083
  store.clearPendingFollowupIf(
@@ -5932,7 +6097,7 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5932
6097
  if (trigger) {
5933
6098
  if (trigger.kind === "off") {
5934
6099
  applyOff(store, conversationId);
5935
- return { continue: true };
6100
+ return allowSubmit();
5936
6101
  }
5937
6102
  if (trigger.kind === "on") {
5938
6103
  const result = applyOn(store, conversationId, projectRoot, {
@@ -5940,22 +6105,22 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5940
6105
  slug: trigger.slug
5941
6106
  });
5942
6107
  if (!result.ok) {
5943
- return { continue: false, userMessage: result.userMessage };
6108
+ return blockSubmit(result.userMessage);
5944
6109
  }
5945
- return { continue: true };
6110
+ return allowSubmit();
5946
6111
  }
5947
6112
  if (trigger.kind === "resume") {
5948
6113
  const result = applyResume(store, conversationId, {
5949
6114
  slug: trigger.slug
5950
6115
  });
5951
6116
  if (!result.ok) {
5952
- return { continue: false, userMessage: result.userMessage };
6117
+ return blockSubmit(result.userMessage);
5953
6118
  }
5954
- return { continue: true };
6119
+ return allowSubmit();
5955
6120
  }
5956
6121
  if (trigger.kind === "resume_review") {
5957
6122
  applyResumeReview(store, conversationId);
5958
- return { continue: true };
6123
+ return allowSubmit();
5959
6124
  }
5960
6125
  if (trigger.kind === "run") {
5961
6126
  const result = applyRun(store, conversationId, projectRoot, {
@@ -5963,9 +6128,12 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5963
6128
  config: actionConfig
5964
6129
  });
5965
6130
  if (!result.ok) {
5966
- return { continue: false, userMessage: result.userMessage };
6131
+ if (isChannelANeedPick(result)) {
6132
+ return allowSubmit();
6133
+ }
6134
+ return blockSubmit(result.userMessage);
5967
6135
  }
5968
- return { continue: true };
6136
+ return allowSubmit();
5969
6137
  }
5970
6138
  if (trigger.kind === "replan") {
5971
6139
  const result = applyReplan(store, conversationId, projectRoot, {
@@ -5973,9 +6141,9 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5973
6141
  config: actionConfig
5974
6142
  });
5975
6143
  if (!result.ok) {
5976
- return { continue: false, userMessage: result.userMessage };
6144
+ return blockSubmit(result.userMessage);
5977
6145
  }
5978
- return { continue: true };
6146
+ return allowSubmit();
5979
6147
  }
5980
6148
  if (trigger.kind === "track_pick" && trigger.trackPick) {
5981
6149
  const result = applyTrackPick(
@@ -5986,21 +6154,28 @@ function handleBeforeSubmitPrompt(store, payload, projectRoot, portConfig) {
5986
6154
  { config: actionConfig }
5987
6155
  );
5988
6156
  if (!result.ok) {
5989
- return { continue: false, userMessage: result.userMessage };
6157
+ if (isChannelANeedPick(result)) {
6158
+ return allowSubmit();
6159
+ }
6160
+ return blockSubmit(result.userMessage);
5990
6161
  }
5991
- return { continue: true };
6162
+ return allowSubmit();
5992
6163
  }
5993
- return { continue: true };
6164
+ return allowSubmit();
5994
6165
  }
5995
6166
  if (!isHarnessFollowupMessage(prompt)) {
5996
6167
  store.clearChainPending(conversationId);
5997
6168
  }
5998
- return { continue: true };
6169
+ return allowSubmit();
5999
6170
  }
6000
6171
  function handleAfterFileEdit(store, payload, projectRoot) {
6001
6172
  const conversationId = cid(payload);
6002
6173
  const filePath = payload.file_path ?? payload.filePath ?? "";
6003
6174
  if (!conversationId || !filePath) return;
6175
+ try {
6176
+ notePlansDirEdit(store, conversationId, projectRoot, filePath);
6177
+ } catch {
6178
+ }
6004
6179
  if (!isProductCodeEdit(filePath, { projectRoot })) return;
6005
6180
  const cfg = loadProjectReviewConfig(projectRoot);
6006
6181
  if (cfg.reviewScope === "project") {
@@ -6135,6 +6310,25 @@ function blockReason(message, fallback) {
6135
6310
  const m = typeof message === "string" ? message.trim() : "";
6136
6311
  return m || fallback;
6137
6312
  }
6313
+ function allowNeedPickContext(userMessage, candidates) {
6314
+ const fromMessage = typeof userMessage === "string" && userMessage.trim().length > 0 ? userMessage : "";
6315
+ const slugs = [
6316
+ ...new Set(
6317
+ (candidates ?? []).map((c) => c && typeof c.slug === "string" ? c.slug.trim() : "").filter((s) => s.length > 0 && isSafeTrackSlug(s))
6318
+ )
6319
+ ];
6320
+ const ctx = fromMessage || (slugs.length > 0 ? `Select a plan to execute:
6321
+
6322
+ ${slugs.map((s, i) => ` ${i + 1}. ${s}`).join("\n")}
6323
+
6324
+ Reply with a number or /autopilot-run <slug>.` : "Select a plan to execute. Reply with a number or /autopilot-run <slug>.");
6325
+ return {
6326
+ hookSpecificOutput: {
6327
+ hookEventName: "UserPromptSubmit",
6328
+ additionalContext: ctx
6329
+ }
6330
+ };
6331
+ }
6138
6332
  function filePathFromClaudeEdit(payload) {
6139
6333
  const input = payload.tool_input ?? payload.toolInput ?? {};
6140
6334
  if (!input || typeof input !== "object" || Array.isArray(input)) return "";
@@ -6232,6 +6426,9 @@ function handleUserPromptSubmit(store, payload, projectRoot, portConfig) {
6232
6426
  });
6233
6427
  if (!result.ok) {
6234
6428
  stampClaudePlatform(store, conversationId, projectRoot);
6429
+ if (isChannelANeedPick(result)) {
6430
+ return allowNeedPickContext(result.userMessage, result.candidates);
6431
+ }
6235
6432
  return {
6236
6433
  decision: "block",
6237
6434
  reason: blockReason(result.userMessage, gateFallback)
@@ -6264,6 +6461,9 @@ function handleUserPromptSubmit(store, payload, projectRoot, portConfig) {
6264
6461
  );
6265
6462
  if (!result.ok) {
6266
6463
  stampClaudePlatform(store, conversationId, projectRoot);
6464
+ if (isChannelANeedPick(result)) {
6465
+ return allowNeedPickContext(result.userMessage, result.candidates);
6466
+ }
6267
6467
  return {
6268
6468
  decision: "block",
6269
6469
  reason: blockReason(result.userMessage, gateFallback)
@@ -6285,6 +6485,10 @@ function handlePostToolUse(store, payload, projectRoot) {
6285
6485
  if (!conversationId || !isClaudeEditTool(toolName)) return;
6286
6486
  const filePath = filePathFromClaudeEdit(payload);
6287
6487
  if (!filePath) return;
6488
+ try {
6489
+ notePlansDirEdit(store, conversationId, projectRoot, filePath);
6490
+ } catch {
6491
+ }
6288
6492
  if (!isProductCodeEdit(filePath, { projectRoot })) return;
6289
6493
  const cfg = loadProjectReviewConfig(projectRoot);
6290
6494
  if (cfg.reviewScope === "project") {