@dadado/agent-kit-cli 5.15.1 → 5.17.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 (72) hide show
  1. package/dashboard/dashboard-data.mjs +69 -65
  2. package/dashboard/lib/guards.d.mts +5 -0
  3. package/dashboard/lib/guards.mjs +31 -0
  4. package/dashboard/lib/identity-probe.mjs +56 -0
  5. package/dashboard/lib/pipeline-cache.mjs +58 -0
  6. package/dashboard/lib/processes.mjs +71 -0
  7. package/dashboard/serve.mjs +43 -1
  8. package/dashboard/start-broadcast.mjs +2 -12
  9. package/dashboard/start.mjs +2 -13
  10. package/dist/add-HTYIFKMN.js +150 -0
  11. package/dist/chunk-2IPXQFJE.js +253 -0
  12. package/dist/chunk-2RDRMGD3.js +82 -0
  13. package/dist/chunk-2YU2H7I6.js +42 -0
  14. package/dist/chunk-4IWG5CVE.js +214 -0
  15. package/dist/chunk-5VCOWTTP.js +6 -0
  16. package/dist/chunk-77V75A7O.js +39 -0
  17. package/dist/chunk-7HDQEK6T.js +26 -0
  18. package/dist/chunk-AW74INUW.js +1456 -0
  19. package/dist/chunk-B73KQ5KX.js +972 -0
  20. package/dist/chunk-CBCW5R5J.js +484 -0
  21. package/dist/chunk-DE3LQFSJ.js +18 -0
  22. package/dist/chunk-DQA434Y4.js +106 -0
  23. package/dist/chunk-F4VMYR7U.js +318 -0
  24. package/dist/chunk-F6C3GFKV.js +57 -0
  25. package/dist/chunk-HFUH3HVS.js +282 -0
  26. package/dist/chunk-HS7LVVLO.js +74 -0
  27. package/dist/chunk-IHROBTNM.js +135 -0
  28. package/dist/chunk-KD2MYBB6.js +213 -0
  29. package/dist/chunk-MAPOQEA2.js +43 -0
  30. package/dist/chunk-N3HTJVBR.js +341 -0
  31. package/dist/chunk-NQ7IQ5XT.js +26 -0
  32. package/dist/chunk-OUQZC744.js +1315 -0
  33. package/dist/chunk-PCCQUWE5.js +817 -0
  34. package/dist/chunk-PNFF6OU3.js +273 -0
  35. package/dist/chunk-PNG7LO2F.js +53 -0
  36. package/dist/chunk-QTON2GQX.js +203 -0
  37. package/dist/chunk-S6GMFDKV.js +50 -0
  38. package/dist/chunk-SMGFN3Z4.js +503 -0
  39. package/dist/chunk-STGFBAYT.js +641 -0
  40. package/dist/chunk-TFZ2JUXH.js +1081 -0
  41. package/dist/chunk-VY7MTPJ2.js +205 -0
  42. package/dist/chunk-W24CMQ32.js +525 -0
  43. package/dist/chunk-WW3YZKHW.js +165 -0
  44. package/dist/chunk-ZB43YMHW.js +20 -0
  45. package/dist/chunk-ZCTSUQRR.js +874 -0
  46. package/dist/chunk-ZXKZBTXE.js +17 -0
  47. package/dist/contribute-C6AW2P6J.js +367 -0
  48. package/dist/cursor-awareness-KTL2RGKD.js +77 -0
  49. package/dist/dashboard-YSNUGBFY.js +21 -0
  50. package/dist/dashboard-broadcast-M4CMW7RX.js +119 -0
  51. package/dist/diff-VHNR62SI.js +74 -0
  52. package/dist/doctor-PET2HRRJ.js +236 -0
  53. package/dist/dogfood-KKFVRWT6.js +366 -0
  54. package/dist/guard-EHXF76ID.js +530 -0
  55. package/dist/handoff-3UI6VKVG.js +302 -0
  56. package/dist/hook-DYIMKJYL.js +1074 -0
  57. package/dist/index.js +62 -14068
  58. package/dist/init-Q4LBM6KT.js +127 -0
  59. package/dist/install-RHOS3OYZ.js +34 -0
  60. package/dist/mission-control-LJACEVVN.js +89 -0
  61. package/dist/monitors-QV2IS3MP.js +176 -0
  62. package/dist/plan-index-JMC7Z4DW.js +44 -0
  63. package/dist/public-inbound-radar-WP3XF7GS.js +68 -0
  64. package/dist/remote-issues-SZ4HDQL5.js +44 -0
  65. package/dist/run-I3BFGULT.js +243 -0
  66. package/dist/run-plan-FO7CL4M3.js +118 -0
  67. package/dist/scan-NI2SWJ2E.js +33 -0
  68. package/dist/setup-global-M42GEODI.js +340 -0
  69. package/dist/status-XPVQVQMX.js +150 -0
  70. package/dist/update-FJ2EBZVO.js +283 -0
  71. package/dist/validate-V2CW7J4B.js +201 -0
  72. package/package.json +3 -3
@@ -0,0 +1,1081 @@
1
+ import {
2
+ InputLine,
3
+ ScrollRegion,
4
+ renderMcLive,
5
+ runMcTuiLoop
6
+ } from "./chunk-PCCQUWE5.js";
7
+ import {
8
+ extractHandoffNamedPlans,
9
+ namedPlanCandidatePaths,
10
+ parsePlanFrontmatter
11
+ } from "./chunk-KD2MYBB6.js";
12
+ import {
13
+ LineSplitter,
14
+ RedactingStreamBuffer,
15
+ claudeRedactions,
16
+ formatHitlSummary,
17
+ operatorQuitStop,
18
+ parseStreamLine,
19
+ redactSecrets,
20
+ renderStreamEvent,
21
+ whichBinary
22
+ } from "./chunk-OUQZC744.js";
23
+ import {
24
+ TtySpinner,
25
+ shouldUseVisualMotion,
26
+ shouldUseWelcomeColor,
27
+ withCliProgress
28
+ } from "./chunk-QTON2GQX.js";
29
+ import {
30
+ logger
31
+ } from "./chunk-ZB43YMHW.js";
32
+ import {
33
+ HANDOFF_REL
34
+ } from "./chunk-5VCOWTTP.js";
35
+ import {
36
+ fileExists,
37
+ readJson
38
+ } from "./chunk-77V75A7O.js";
39
+
40
+ // src/mission-control/live-run.ts
41
+ import { readFile as readFile2 } from "fs/promises";
42
+ import path2 from "path";
43
+
44
+ // src/plan-loop/plan-state.ts
45
+ import { readFile, readdir } from "fs/promises";
46
+ import path from "path";
47
+ function countPendingTodos(raw) {
48
+ return parsePlanFrontmatter(raw).todos.filter(
49
+ (t) => (t.status === "pending" || t.status === "in_progress") && t.id !== "dogfood-poc"
50
+ ).length;
51
+ }
52
+ async function findActivePlanFile(plansDir) {
53
+ if (!await fileExists(plansDir)) return null;
54
+ const files = (await readdir(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
55
+ return files[0] ? path.join(plansDir, files[0]) : null;
56
+ }
57
+ async function readPlan(planPath) {
58
+ return readFile(planPath, "utf8");
59
+ }
60
+ var QUEUE_CURSOR_RE = /^- \*\*Queue cursor:\*\*\s*(.+)$/m;
61
+ var QUEUE_STATUS_RE = /^- \*\*Queue status:\*\*\s*(.+)$/m;
62
+ var QUEUE_DONE_STATUSES = /* @__PURE__ */ new Set(["exhausted", "completed", "done"]);
63
+ function readRunQueueState(handoff) {
64
+ const queue = extractHandoffNamedPlans(handoff).runQueue;
65
+ let cursorIndex = null;
66
+ let cursorPlan = null;
67
+ const cursorRaw = QUEUE_CURSOR_RE.exec(handoff)?.[1]?.trim() ?? "";
68
+ const indexMatch = /^(\d+)\b/.exec(cursorRaw);
69
+ if (indexMatch?.[1]) cursorIndex = Number(indexMatch[1]);
70
+ const currentMatch = /current:\s*`?([^`()]+?)`?\s*\)/i.exec(cursorRaw);
71
+ if (currentMatch?.[1]) {
72
+ const base = currentMatch[1].trim().split("/").pop() ?? "";
73
+ if (/\.plan\.md$/i.test(base)) cursorPlan = base;
74
+ }
75
+ const status = QUEUE_STATUS_RE.exec(handoff)?.[1]?.trim().split(/\s+/)[0]?.replace(/[`*]/g, "");
76
+ const queueStatus = status ? status : null;
77
+ const finished = queueStatus !== null && QUEUE_DONE_STATUSES.has(queueStatus.toLowerCase());
78
+ let cursor = null;
79
+ if (cursorIndex !== null && cursorIndex >= 0 && cursorIndex < queue.length) {
80
+ cursor = cursorIndex;
81
+ } else if (cursorPlan && queue.includes(cursorPlan)) {
82
+ cursor = queue.indexOf(cursorPlan);
83
+ }
84
+ const current = cursor !== null ? queue[cursor] ?? null : cursorPlan;
85
+ return { queue, cursor, current, status: queueStatus, finished };
86
+ }
87
+ function defaultPlanStateIo() {
88
+ return {
89
+ readFile: async (file) => {
90
+ try {
91
+ return await readFile(file, "utf8");
92
+ } catch {
93
+ return null;
94
+ }
95
+ },
96
+ findActivePlan: findActivePlanFile
97
+ };
98
+ }
99
+ async function resolveActivePlanFromHandoff(root, handoff, io = defaultPlanStateIo()) {
100
+ const queue = readRunQueueState(handoff);
101
+ const base = (queue.queue.length > 0 && !queue.finished ? queue.current : null) ?? extractHandoffNamedPlans(handoff).active;
102
+ if (!base) return io.findActivePlan(path.join(root, ".cursor", "plans"));
103
+ for (const candidate of namedPlanCandidatePaths(root, base)) {
104
+ if (await io.readFile(candidate) !== null) return candidate;
105
+ }
106
+ return null;
107
+ }
108
+ async function resolveActivePlanPath(root, io = defaultPlanStateIo()) {
109
+ const handoff = await io.readFile(path.join(root, HANDOFF_REL)) ?? "";
110
+ return resolveActivePlanFromHandoff(root, handoff, io);
111
+ }
112
+
113
+ // src/mission-control/live-feed.ts
114
+ import { StringDecoder } from "string_decoder";
115
+ var LIVE_LOG_CAPACITY = 500;
116
+ var LiveRunFeed = class {
117
+ log;
118
+ crew;
119
+ gate = null;
120
+ hitlReplies = 0;
121
+ lines = new LineSplitter();
122
+ decoder = new StringDecoder("utf8");
123
+ columns;
124
+ version = 0;
125
+ constructor(opts = {}) {
126
+ this.columns = Math.max(40, opts.columns ?? 80);
127
+ this.log = new ScrollRegion(opts.capacity ?? LIVE_LOG_CAPACITY);
128
+ this.crew = {
129
+ backend: opts.backend ?? null,
130
+ pid: void 0,
131
+ phase: "starting",
132
+ startedAt: (opts.now ?? Date.now)(),
133
+ toolCalls: 0,
134
+ currentTool: null,
135
+ lastResult: null,
136
+ rateLimit: null,
137
+ logPath: opts.logPath ?? null
138
+ };
139
+ }
140
+ /** Bumps on every change; a paint loop can skip a frame when nothing moved. */
141
+ get revision() {
142
+ return this.version;
143
+ }
144
+ setProcess(info) {
145
+ this.crew.backend = info.backend;
146
+ this.crew.pid = info.pid;
147
+ if (this.crew.phase === "starting") this.crew.phase = "running";
148
+ this.version += 1;
149
+ }
150
+ setPhase(phase) {
151
+ this.crew.phase = phase;
152
+ this.version += 1;
153
+ }
154
+ openGate(gate) {
155
+ this.gate = gate;
156
+ this.crew.phase = "gate";
157
+ this.version += 1;
158
+ }
159
+ closeGate() {
160
+ this.gate = null;
161
+ if (this.crew.phase === "gate") this.crew.phase = "running";
162
+ this.version += 1;
163
+ }
164
+ /** Append a status line the run lane produced itself (never from the stream). */
165
+ note(text, kind = "note") {
166
+ for (const line of text.split(/\r?\n/)) {
167
+ const trimmed = line.trimEnd();
168
+ if (trimmed) this.log.push({ kind, text: trimmed });
169
+ }
170
+ this.version += 1;
171
+ }
172
+ feed(chunk) {
173
+ try {
174
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
175
+ if (!text) return;
176
+ this.lines.feed(text, (line) => this.takeLine(line));
177
+ } catch {
178
+ }
179
+ }
180
+ end() {
181
+ try {
182
+ this.lines.feed(this.decoder.end(), (line) => this.takeLine(line));
183
+ const rest = this.lines.end();
184
+ if (rest) this.takeLine(rest);
185
+ this.crew.currentTool = null;
186
+ if (this.crew.phase !== "done") this.crew.phase = "done";
187
+ this.version += 1;
188
+ } catch {
189
+ }
190
+ }
191
+ takeLine(line) {
192
+ for (const event of parseStreamLine(line)) {
193
+ const rendered = renderStreamEvent(event, { color: false, columns: this.columns });
194
+ if (rendered === null) continue;
195
+ switch (event.kind) {
196
+ case "text":
197
+ this.note(rendered, "text");
198
+ break;
199
+ case "tool_use":
200
+ this.crew.toolCalls += 1;
201
+ this.crew.currentTool = { name: event.name, summary: event.summary };
202
+ this.note(rendered, "tool");
203
+ break;
204
+ case "tool_result":
205
+ this.crew.currentTool = null;
206
+ this.note(rendered, event.ok ? "tool" : "error");
207
+ break;
208
+ case "result":
209
+ this.crew.currentTool = null;
210
+ this.crew.lastResult = { ok: event.ok, status: event.status };
211
+ if (event.costUsd !== void 0) this.crew.costUsd = event.costUsd;
212
+ if (event.turns !== void 0) this.crew.turns = event.turns;
213
+ this.note(rendered, event.ok ? "result" : "error");
214
+ break;
215
+ case "hook":
216
+ this.note(rendered, "hook");
217
+ break;
218
+ case "rate_limit":
219
+ this.crew.rateLimit = { line: event.line, severe: event.severe };
220
+ this.note(rendered, "rate_limit");
221
+ break;
222
+ case "prompt":
223
+ this.note(rendered, "prompt");
224
+ break;
225
+ case "hitl_reply":
226
+ this.hitlReplies += 1;
227
+ this.note(rendered, "hitl");
228
+ break;
229
+ default:
230
+ break;
231
+ }
232
+ }
233
+ }
234
+ };
235
+ function emptyLiveMission() {
236
+ return {
237
+ planFile: null,
238
+ mode: null,
239
+ todos: { done: 0, total: 0 },
240
+ currentTodo: null,
241
+ nextTodo: null,
242
+ queue: null
243
+ };
244
+ }
245
+
246
+ // src/mission-control/live-run.ts
247
+ var LIVE_FRAME_MS = 120;
248
+ var LIVE_PROGRESS_TTL_MS = 2e3;
249
+ function shouldUseLiveTui(opts = {}) {
250
+ if (opts.plain) return false;
251
+ const env = opts.env ?? process.env;
252
+ const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
253
+ const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
254
+ if (!stdoutIsTTY || !stdinIsTTY) return false;
255
+ return shouldUseWelcomeColor({ stdoutIsTTY, env });
256
+ }
257
+ function defaultMissionIo() {
258
+ return {
259
+ readFile: async (file) => {
260
+ try {
261
+ return await readFile2(file, "utf8");
262
+ } catch {
263
+ return null;
264
+ }
265
+ },
266
+ findActivePlan: findActivePlanFile
267
+ };
268
+ }
269
+ var MODE_RE = /^- \*\*Mode:\*\*\s*(.+)$/m;
270
+ function firstWord(raw) {
271
+ const word = raw?.trim().split(/\s+/)[0]?.replace(/[`*]/g, "");
272
+ return word ? word : null;
273
+ }
274
+ async function readLiveMission(root, io = defaultMissionIo()) {
275
+ const mission = emptyLiveMission();
276
+ const handoff = await io.readFile(path2.join(root, HANDOFF_REL)) ?? "";
277
+ mission.mode = firstWord(MODE_RE.exec(handoff)?.[1]);
278
+ const { queue, cursor, current, status: queueStatus, finished } = readRunQueueState(handoff);
279
+ let checklist = [];
280
+ if (queue.length > 0) {
281
+ const done = finished ? queue.length : cursor ?? 0;
282
+ mission.queue = {
283
+ done,
284
+ total: queue.length,
285
+ current,
286
+ status: queueStatus
287
+ };
288
+ checklist = queue.map((file, i) => ({
289
+ label: file,
290
+ state: finished || cursor !== null && i < cursor ? "done" : i === cursor ? "current" : "pending"
291
+ }));
292
+ }
293
+ const planPath = await resolveActivePlanFromHandoff(root, handoff, io);
294
+ if (planPath) {
295
+ const raw = await io.readFile(planPath);
296
+ if (raw !== null) {
297
+ mission.planFile = path2.basename(planPath);
298
+ const { todos } = parsePlanFrontmatter(raw);
299
+ const open = todos.filter((t) => t.status !== "cancelled");
300
+ mission.todos = {
301
+ done: open.filter((t) => t.status === "completed").length,
302
+ total: open.length
303
+ };
304
+ mission.currentTodo = todos.find((t) => t.status === "in_progress")?.id ?? null;
305
+ mission.nextTodo = todos.find((t) => t.status === "pending")?.id ?? null;
306
+ if (checklist.length === 0) {
307
+ checklist = open.map((t) => ({
308
+ label: t.id,
309
+ state: t.status === "completed" ? "done" : t.status === "in_progress" ? "current" : "pending"
310
+ }));
311
+ const current2 = checklist.findIndex((row) => row.state === "current");
312
+ if (current2 > 1) checklist = checklist.slice(current2 - 1);
313
+ }
314
+ }
315
+ }
316
+ return { mission, checklist };
317
+ }
318
+ function createLiveMissionLoader(root, opts = {}) {
319
+ const ttl = opts.ttlMs ?? LIVE_PROGRESS_TTL_MS;
320
+ const now = opts.now ?? Date.now;
321
+ let cached = null;
322
+ let readAt = Number.NEGATIVE_INFINITY;
323
+ let inFlight = null;
324
+ return async () => {
325
+ if (cached && now() - readAt < ttl) return cached;
326
+ if (inFlight) return inFlight;
327
+ inFlight = readLiveMission(root, opts.io).then((state) => {
328
+ cached = state;
329
+ readAt = now();
330
+ return state;
331
+ }).catch(() => {
332
+ cached = cached ?? { mission: emptyLiveMission(), checklist: [] };
333
+ readAt = now();
334
+ return cached;
335
+ }).finally(() => {
336
+ inFlight = null;
337
+ });
338
+ return cached ?? inFlight;
339
+ };
340
+ }
341
+ async function withLiveTui(opts, run) {
342
+ const write = opts.write ?? ((chunk) => process.stdout.write(chunk));
343
+ const stdin = opts.stdin ?? process.stdin;
344
+ const env = opts.env ?? process.env;
345
+ const now = opts.now ?? Date.now;
346
+ const columns = opts.columns ?? process.stdout.columns ?? 80;
347
+ const rows = opts.rows ?? process.stdout.rows ?? 24;
348
+ const feed = new LiveRunFeed({
349
+ columns,
350
+ now,
351
+ backend: opts.backend,
352
+ logPath: path2.relative(opts.root, opts.logPath) || opts.logPath
353
+ });
354
+ opts.onFeed?.(feed);
355
+ const missionLoader = opts.missionLoader ?? createLiveMissionLoader(opts.root, { now });
356
+ const input = new InputLine();
357
+ let pending = null;
358
+ let spawnInfo = null;
359
+ let quitRequested = null;
360
+ let handle = null;
361
+ const repaint = () => {
362
+ void handle?.repaint();
363
+ };
364
+ const settleGate = (answer) => {
365
+ const open = pending;
366
+ if (!open) return;
367
+ pending = null;
368
+ feed.closeGate();
369
+ open.settle(answer);
370
+ };
371
+ const readAnswer = (gate, readOpts) => new Promise((resolve) => {
372
+ const onAbort = () => settleGate({ kind: "eof" });
373
+ if (readOpts.signal?.aborted) {
374
+ resolve({ kind: "eof" });
375
+ return;
376
+ }
377
+ input.reset();
378
+ pending = {
379
+ gate,
380
+ settle: (answer) => {
381
+ readOpts.signal?.removeEventListener("abort", onAbort);
382
+ resolve(answer);
383
+ }
384
+ };
385
+ readOpts.signal?.addEventListener("abort", onAbort, { once: true });
386
+ feed.openGate(gate);
387
+ repaint();
388
+ });
389
+ const stopRun = (stop) => {
390
+ if (pending) {
391
+ settleGate({ kind: "sigint" });
392
+ return;
393
+ }
394
+ if (quitRequested) return;
395
+ quitRequested = stop;
396
+ feed.setPhase("stopping");
397
+ feed.note(stop.message);
398
+ spawnInfo?.stop(stop);
399
+ };
400
+ const hooks = {
401
+ write,
402
+ stdin,
403
+ now,
404
+ setIntervalFn: opts.setIntervalFn,
405
+ clearIntervalFn: opts.clearIntervalFn,
406
+ onKey: (chunk) => {
407
+ if (!pending) return false;
408
+ const result = input.handleKey(chunk);
409
+ if (result === "submit") settleGate({ kind: "line", text: input.value });
410
+ else if (result === "sigint") settleGate({ kind: "sigint" });
411
+ else if (result === "eof") settleGate({ kind: "eof" });
412
+ else if (result === "edit") repaint();
413
+ return true;
414
+ },
415
+ onQuit: (key) => {
416
+ write("\n");
417
+ stopRun(operatorQuitStop(key));
418
+ }
419
+ };
420
+ const loadView = async () => {
421
+ const state = await missionLoader();
422
+ return {
423
+ mission: state.mission,
424
+ checklist: state.checklist,
425
+ gate: pending ? {
426
+ askId: pending.gate.askId,
427
+ labels: pending.gate.labels,
428
+ detection: pending.gate.detection,
429
+ input
430
+ } : null,
431
+ flightLog: feed.log,
432
+ crew: feed.crew,
433
+ error: null
434
+ };
435
+ };
436
+ const seams = {
437
+ render: feed,
438
+ log: (line) => feed.note(line),
439
+ onSpawn: (info) => {
440
+ spawnInfo = info;
441
+ feed.setProcess({ backend: info.backend, pid: info.pid });
442
+ if (quitRequested) info.stop(quitRequested);
443
+ },
444
+ hitl: {
445
+ policy: opts.noHitl ? "off" : "prompt",
446
+ isTTY: true,
447
+ readAnswer,
448
+ write: (text) => feed.note(text, "hitl")
449
+ }
450
+ };
451
+ const onSignal = (signal) => {
452
+ handle?.stop();
453
+ stopRun(operatorQuitStop(signal));
454
+ };
455
+ const onSigint = () => onSignal("SIGINT");
456
+ const onSigterm = () => onSignal("SIGTERM");
457
+ const signals = opts.signals ?? true;
458
+ if (signals) {
459
+ process.prependListener("SIGINT", onSigint);
460
+ process.prependListener("SIGTERM", onSigterm);
461
+ }
462
+ try {
463
+ handle = await runMcTuiLoop({
464
+ loadView,
465
+ render: (view, renderOpts) => renderMcLive(view, { ...renderOpts, columns, rows, now: now() }),
466
+ stdoutIsTTY: true,
467
+ env,
468
+ intervalMs: opts.intervalMs ?? LIVE_FRAME_MS,
469
+ renderOpts: { stdoutIsTTY: true, env, columns },
470
+ hooks
471
+ });
472
+ const result = await run(seams);
473
+ feed.setPhase("done");
474
+ if (handle) await handle.repaint();
475
+ return result;
476
+ } finally {
477
+ if (pending) settleGate({ kind: "eof" });
478
+ handle?.stop();
479
+ if (signals) {
480
+ process.off("SIGINT", onSigint);
481
+ process.off("SIGTERM", onSigterm);
482
+ }
483
+ write("\n");
484
+ }
485
+ }
486
+
487
+ // src/plan-loop/detect.ts
488
+ var DETECT_ORDER = ["cursor-agent", "claude"];
489
+ var INSTALL_HINTS = {
490
+ "cursor-agent": "Install the Cursor Agent CLI and ensure `cursor-agent` is on PATH (https://cursor.com/docs/cli).",
491
+ claude: "Install Claude Code and ensure `claude` is on PATH (https://code.claude.com/docs/en/quickstart)."
492
+ };
493
+ function isBackendId(id) {
494
+ return id === "cursor-agent" || id === "claude";
495
+ }
496
+ function isRequestedBackend(id) {
497
+ return id === "auto" || isBackendId(id);
498
+ }
499
+ function listDetectBackendIds() {
500
+ return ["auto", ...DETECT_ORDER];
501
+ }
502
+ async function detectAgentBackend(requested, whichFn = whichBinary) {
503
+ const id = requested.trim().toLowerCase();
504
+ if (id === "glm" || id.startsWith("glm-") || id === "glm4") {
505
+ return {
506
+ ok: false,
507
+ message: "No documented GLM CLI binary is registered. Refusing a stub that would claim to work. Use --backend auto, cursor-agent, or claude."
508
+ };
509
+ }
510
+ if (!isRequestedBackend(id)) {
511
+ return {
512
+ ok: false,
513
+ message: `Unknown backend '${requested}'. Supported: ${listDetectBackendIds().join(", ")}.`
514
+ };
515
+ }
516
+ if (id === "auto") {
517
+ for (const candidate of DETECT_ORDER) {
518
+ const bin2 = await whichFn(candidate);
519
+ if (bin2) return { ok: true, id: candidate, bin: bin2 };
520
+ }
521
+ return {
522
+ ok: false,
523
+ message: [
524
+ "No agent CLI found on PATH (tried: cursor-agent, claude).",
525
+ INSTALL_HINTS["cursor-agent"],
526
+ INSTALL_HINTS.claude
527
+ ].join(" ")
528
+ };
529
+ }
530
+ const bin = await whichFn(id);
531
+ if (!bin) {
532
+ return { ok: false, message: `${id} not found on PATH. ${INSTALL_HINTS[id]}` };
533
+ }
534
+ return { ok: true, id, bin };
535
+ }
536
+
537
+ // src/plan-loop/run-loop.ts
538
+ import { mkdir, readFile as readFile4, rm, unlink } from "fs/promises";
539
+ import path5 from "path";
540
+
541
+ // src/plan-loop/external-review.ts
542
+ import { spawn } from "child_process";
543
+ import path3 from "path";
544
+ var CANONICAL_REL = path3.join(".cursor", "scripts", "plan-external-review.sh");
545
+ var FALLBACK_REL = path3.join("scripts", "plan-external-review.sh");
546
+ function isPlanExhaustedReason(reason) {
547
+ const r = reason.trim().toLowerCase();
548
+ if (!r) return false;
549
+ if (r.includes("plan exhausted")) return true;
550
+ if (r.includes("nothing to do")) return true;
551
+ if (r.includes("no implementable")) return true;
552
+ if (/\ball\s+(implementable\s+)?to-?dos?\b/.test(r) && (r.includes("completed") || r.includes("done") || r.includes("exhausted"))) {
553
+ return true;
554
+ }
555
+ return false;
556
+ }
557
+ function shouldArmExternalPlanReview(input) {
558
+ if (input.pending === 0) return true;
559
+ if (input.stopReason && isPlanExhaustedReason(input.stopReason)) return true;
560
+ return false;
561
+ }
562
+ async function armExternalPlanReview(root, options = {}) {
563
+ const spawnFn = options.spawnFn ?? spawn;
564
+ const existsFn = options.existsFn ?? fileExists;
565
+ const log = options.log ?? ((line) => console.log(line));
566
+ const force = options.force === true;
567
+ const canonicalPath = path3.join(root, CANONICAL_REL);
568
+ const fallbackPath = path3.join(root, FALLBACK_REL);
569
+ let scriptPath = null;
570
+ let scriptRel = CANONICAL_REL;
571
+ if (await existsFn(canonicalPath)) {
572
+ scriptPath = canonicalPath;
573
+ scriptRel = CANONICAL_REL;
574
+ } else if (await existsFn(fallbackPath)) {
575
+ scriptPath = fallbackPath;
576
+ scriptRel = FALLBACK_REL;
577
+ }
578
+ if (!scriptPath) {
579
+ log(
580
+ `tip: ${CANONICAL_REL} missing (optional wrapper: ${FALLBACK_REL}). Manual: /plan-external-review (enable externalPlanReview in .cursor/context/config.json).`
581
+ );
582
+ return { invoked: false, exitCode: null, output: "" };
583
+ }
584
+ log("Plan exhausted: arming optional external plan review (opt-in handled by script)...");
585
+ const args = force ? [scriptPath, "--force", "--print"] : [scriptPath, "--print"];
586
+ return new Promise((resolve) => {
587
+ const child = spawnFn("bash", args, {
588
+ cwd: root,
589
+ env: { ...process.env, AGENT_KIT_HEADLESS: "1" }
590
+ });
591
+ const redact = claudeRedactions(process.env);
592
+ const outBuffer = new RedactingStreamBuffer(redact);
593
+ const errBuffer = new RedactingStreamBuffer(redact);
594
+ let output = "";
595
+ const emitOut = (s) => {
596
+ if (!s) return;
597
+ output += s;
598
+ process.stdout.write(s);
599
+ };
600
+ const emitErr = (s) => {
601
+ if (!s) return;
602
+ output += s;
603
+ process.stderr.write(s);
604
+ };
605
+ let flushed = false;
606
+ const flush = () => {
607
+ if (flushed) return;
608
+ flushed = true;
609
+ emitOut(outBuffer.flush());
610
+ emitErr(errBuffer.flush());
611
+ };
612
+ child.stdout?.on("data", (chunk) => emitOut(outBuffer.push(chunk)));
613
+ child.stderr?.on("data", (chunk) => emitErr(errBuffer.push(chunk)));
614
+ child.on("error", (err) => {
615
+ flush();
616
+ log(
617
+ `tip: external plan review launcher failed (${redactSecrets(err.message, redact)}). Manual: ${scriptRel} or /plan-external-review`
618
+ );
619
+ resolve({ invoked: true, exitCode: null, output });
620
+ });
621
+ child.on("close", (code) => {
622
+ flush();
623
+ if (code !== 0 && code !== null) {
624
+ log(
625
+ `tip: external plan review exited ${code} (ignored; does not fail the loop). Manual: ${scriptRel} or /plan-external-review`
626
+ );
627
+ }
628
+ resolve({ invoked: true, exitCode: code, output });
629
+ });
630
+ });
631
+ }
632
+
633
+ // src/plan-loop/persona-banners.ts
634
+ import path4 from "path";
635
+ import {
636
+ blue,
637
+ cyan,
638
+ gray,
639
+ green,
640
+ lightGray,
641
+ lightGreen,
642
+ magenta,
643
+ red,
644
+ white,
645
+ yellow
646
+ } from "kolorist";
647
+ var CLI_RUN_PLAN_MODE = "cli-run-plan";
648
+ var DEFAULT_CLI_PERSONA_ID = "ghost-runner";
649
+ var COLORS = {
650
+ white,
651
+ gray,
652
+ green,
653
+ cyan,
654
+ magenta,
655
+ yellow,
656
+ red,
657
+ blue,
658
+ "light-green": lightGreen,
659
+ lightgreen: lightGreen,
660
+ "light-gray": lightGray,
661
+ lightgray: lightGray
662
+ };
663
+ function resolveColor(name, fallback) {
664
+ if (!name) return fallback;
665
+ return COLORS[name.toLowerCase()] ?? fallback;
666
+ }
667
+ async function resolveCliPersonaId(root) {
668
+ try {
669
+ const cfg = await readJson(
670
+ path4.join(root, ".cursor", "context", "config.json")
671
+ );
672
+ const modes = cfg?.agentPersona?.modes ?? cfg?.workspaceSkin?.modes;
673
+ const id = modes?.[CLI_RUN_PLAN_MODE];
674
+ if (typeof id === "string" && id.trim()) return id.trim();
675
+ } catch {
676
+ }
677
+ return DEFAULT_CLI_PERSONA_ID;
678
+ }
679
+ async function loadPersonaPack(root, personaId) {
680
+ try {
681
+ const personaPath = path4.join(root, "registry", "personas", "core", personaId, "persona.json");
682
+ const pack = await readJson(personaPath);
683
+ if (!pack || typeof pack.id !== "string") return null;
684
+ return pack;
685
+ } catch {
686
+ return null;
687
+ }
688
+ }
689
+ async function loadCliRunPlanPersona(root) {
690
+ const id = await resolveCliPersonaId(root);
691
+ return loadPersonaPack(root, id);
692
+ }
693
+ function createPersonaBannerPrinter(persona) {
694
+ if (!persona?.cliBanners) return null;
695
+ const banners = persona.cliBanners;
696
+ if (!banners.tickStart && !banners.tickEnd && !banners.phaseComplete) return null;
697
+ const primary = resolveColor(persona.ansiPalette?.primary, white);
698
+ const secondary = resolveColor(persona.ansiPalette?.secondary, gray);
699
+ const accent = resolveColor(persona.ansiPalette?.accent, green);
700
+ return {
701
+ tickStart(detail) {
702
+ if (banners.tickStart) {
703
+ console.log(`${accent(banners.tickStart)} ${primary(detail)}`);
704
+ } else {
705
+ console.log(primary(detail));
706
+ }
707
+ },
708
+ tickEnd(detail) {
709
+ if (!banners.tickEnd) return;
710
+ const line = detail ? `${banners.tickEnd} ${detail}` : banners.tickEnd;
711
+ console.log(secondary(line));
712
+ },
713
+ phaseComplete(detail) {
714
+ if (!banners.phaseComplete) return;
715
+ const line = detail ? `${banners.phaseComplete} ${detail}` : banners.phaseComplete;
716
+ console.log(accent(line));
717
+ },
718
+ sleep(seconds) {
719
+ console.log(secondary(`sleep ${seconds}s`));
720
+ },
721
+ stop(reason) {
722
+ if (banners.tickEnd) {
723
+ console.log(secondary(`${banners.tickEnd} ${reason}`));
724
+ }
725
+ }
726
+ };
727
+ }
728
+
729
+ // src/plan-loop/sentinel.ts
730
+ import { readFile as readFile3 } from "fs/promises";
731
+ var SENTINEL_RE = /LOOP_TICK_RESULT:\s*(continue|stop(?:\s*[—\-].*)?)/i;
732
+ function takeFromText(text) {
733
+ if (!text) return null;
734
+ const m = SENTINEL_RE.exec(text);
735
+ if (!m?.[1]) return null;
736
+ const raw = m[1].trim();
737
+ if (raw.toLowerCase().startsWith("stop")) {
738
+ const reason = raw.slice(4).replace(/^[—\-\s]+/, "").trim() || "requested by agent";
739
+ return { kind: "stop", reason };
740
+ }
741
+ return { kind: "continue" };
742
+ }
743
+ function parseSentinelFromLog(content) {
744
+ let lastJson = null;
745
+ let lastText = null;
746
+ let sawJson = false;
747
+ for (const line of content.split(/\r?\n/)) {
748
+ const trimmed = line.trim();
749
+ if (!trimmed) continue;
750
+ if (!trimmed.startsWith("{")) {
751
+ const hit = takeFromText(trimmed);
752
+ if (hit) lastText = hit;
753
+ continue;
754
+ }
755
+ try {
756
+ const ev = JSON.parse(trimmed);
757
+ sawJson = true;
758
+ if (ev.type === "result" && typeof ev.result === "string") {
759
+ const hit = takeFromText(ev.result);
760
+ if (hit) lastJson = hit;
761
+ }
762
+ const parts = ev.type === "assistant" ? ev.message?.content : void 0;
763
+ if (Array.isArray(parts)) {
764
+ for (const part of parts) {
765
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
766
+ const hit = takeFromText(part.text);
767
+ if (hit) lastJson = hit;
768
+ }
769
+ }
770
+ }
771
+ } catch {
772
+ const hit = takeFromText(trimmed);
773
+ if (hit) lastText = hit;
774
+ }
775
+ }
776
+ return (sawJson ? lastJson : lastText) ?? { kind: "missing" };
777
+ }
778
+ function parseTickResultStatus(content) {
779
+ let last = null;
780
+ for (const line of content.split(/\r?\n/)) {
781
+ const trimmed = line.trim();
782
+ if (!trimmed.startsWith("{")) continue;
783
+ try {
784
+ const ev = JSON.parse(trimmed);
785
+ if (ev.type !== "result") continue;
786
+ const errors = Array.isArray(ev.errors) ? ev.errors.filter((e) => typeof e === "string") : [];
787
+ last = {
788
+ isError: ev.is_error === true,
789
+ ...typeof ev.subtype === "string" ? { subtype: ev.subtype } : {},
790
+ errors
791
+ };
792
+ } catch {
793
+ }
794
+ }
795
+ return last;
796
+ }
797
+ async function parseTickResultStatusFromLogFile(logPath) {
798
+ try {
799
+ return parseTickResultStatus(await readFile3(logPath, "utf8"));
800
+ } catch {
801
+ return null;
802
+ }
803
+ }
804
+ async function parseSentinelFromLogFile(logPath) {
805
+ try {
806
+ const content = await readFile3(logPath, "utf8");
807
+ return parseSentinelFromLog(content);
808
+ } catch {
809
+ return { kind: "missing" };
810
+ }
811
+ }
812
+ function formatSentinelLine(sentinel) {
813
+ if (sentinel.kind === "continue") return "LOOP_TICK_RESULT: continue";
814
+ if (sentinel.kind === "stop") return `LOOP_TICK_RESULT: stop - ${sentinel.reason}`;
815
+ return "";
816
+ }
817
+ function sentinelFromTurn(turn) {
818
+ const fromResult = takeFromText(turn.resultText);
819
+ if (fromResult) return fromResult;
820
+ for (let i = turn.assistantTexts.length - 1; i >= 0; i--) {
821
+ const hit = takeFromText(turn.assistantTexts[i] ?? "");
822
+ if (hit) return hit;
823
+ }
824
+ return null;
825
+ }
826
+ function tickResultStatusFromTurn(turn) {
827
+ return {
828
+ isError: turn.isError,
829
+ ...turn.subtype !== null ? { subtype: turn.subtype } : {},
830
+ errors: turn.errors
831
+ };
832
+ }
833
+ async function resolveTickResultStatus(lastResult, logPath, fromFile = parseTickResultStatusFromLogFile) {
834
+ if (lastResult) return tickResultStatusFromTurn(lastResult);
835
+ return fromFile(logPath);
836
+ }
837
+ async function resolveTickSentinel(lastResult, logPath, fromFile = parseSentinelFromLogFile) {
838
+ const fromTurn = lastResult ? sentinelFromTurn(lastResult) : null;
839
+ return fromTurn ?? fromFile(logPath);
840
+ }
841
+
842
+ // src/plan-loop/run-loop.ts
843
+ var TICK_PROMPT = '/run-plan - single tick from the headless runner (agent-kit run-plan). Read .cursor/HANDOFF.md and the active plan in .cursor/plans/. Mark the next to-do as in_progress in the frontmatter, execute ONLY that to-do, mark completed and update HANDOFF. If there is a commitable diff: run /git-staging without asking for confirmation. NEVER /git-prod. Do NOT re-arm an internal Loop skill - the external runner starts the next agent. End the response with exactly one line: "LOOP_TICK_RESULT: continue" if implementable to-dos remain, or "LOOP_TICK_RESULT: stop - <reason>" (plan exhausted, external blocker, or human decision needed).';
844
+ async function runTickWithSpinner(label, backend, opts, plain) {
845
+ if (shouldUseLiveTui({ plain })) {
846
+ return withLiveTui(
847
+ {
848
+ root: opts.workspace,
849
+ backend: backend.id,
850
+ logPath: opts.logPath,
851
+ noHitl: opts.hitl?.policy === "off"
852
+ },
853
+ (seams) => backend.run({
854
+ ...opts,
855
+ hitl: seams.hitl,
856
+ render: seams.render,
857
+ onSpawn: seams.onSpawn,
858
+ log: seams.log
859
+ })
860
+ );
861
+ }
862
+ if (!shouldUseVisualMotion()) return backend.run(opts);
863
+ const spinner = new TtySpinner({ motion: true });
864
+ spinner.start(label);
865
+ try {
866
+ return await backend.run({
867
+ ...opts,
868
+ hitl: {
869
+ ...opts.hitl,
870
+ onPromptStart: () => spinner.stop(),
871
+ onPromptEnd: () => spinner.start(label)
872
+ }
873
+ });
874
+ } finally {
875
+ spinner.stop();
876
+ }
877
+ }
878
+ function stamp() {
879
+ const d = /* @__PURE__ */ new Date();
880
+ const p = (n, w = 2) => String(n).padStart(w, "0");
881
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${p(d.getMilliseconds(), 3)}`;
882
+ }
883
+ function sleep(ms) {
884
+ return new Promise((r) => setTimeout(r, ms));
885
+ }
886
+ async function runPlanLoop(opts) {
887
+ const stopFile = path5.join(opts.root, ".cursor", "loop.stop");
888
+ const logDir = path5.join(opts.root, ".cursor", "loop-logs");
889
+ const planPath = await resolveActivePlanPath(opts.root);
890
+ if (!planPath) {
891
+ logger.error("No active plan in .cursor/plans/");
892
+ return 1;
893
+ }
894
+ const pending = async () => countPendingTodos(await readPlan(planPath));
895
+ await mkdir(logDir, { recursive: true });
896
+ try {
897
+ await unlink(stopFile);
898
+ } catch {
899
+ }
900
+ const onSigInt = () => {
901
+ console.log("");
902
+ console.log("Loop interrupted (Ctrl+C). State saved in HANDOFF by the last tick.");
903
+ process.exit(130);
904
+ };
905
+ process.on("SIGINT", onSigInt);
906
+ try {
907
+ const persona = await loadCliRunPlanPersona(opts.root);
908
+ const banners = createPersonaBannerPrinter(persona);
909
+ console.log(`Active plan: ${path5.basename(planPath)}`);
910
+ console.log(`Pending to-dos: ${await pending()} | max ticks: ${opts.maxTicks}`);
911
+ console.log(`Backend: ${opts.backend.id}`);
912
+ if (persona) {
913
+ console.log(`CLI persona: ${persona.displayName ?? persona.id}`);
914
+ }
915
+ if (opts.dryRun) {
916
+ console.log("--dry-run: no agent will be started. Tick prompt:");
917
+ console.log(TICK_PROMPT);
918
+ return 0;
919
+ }
920
+ const binary = await opts.backend.resolve();
921
+ if (!binary) {
922
+ logger.error(`${opts.backend.id} not found on PATH`);
923
+ return 1;
924
+ }
925
+ let tick = 0;
926
+ let stopReason;
927
+ let planExhausted = false;
928
+ let exitCode = 0;
929
+ while (true) {
930
+ tick += 1;
931
+ if (tick > opts.maxTicks) {
932
+ const msg = `Budget of ${opts.maxTicks} ticks reached - stopping. Run again to continue.`;
933
+ if (banners) banners.stop(msg);
934
+ else console.log(msg);
935
+ break;
936
+ }
937
+ if (await fileExists(stopFile)) {
938
+ const msg = `Stop file found (${stopFile}) - stopping.`;
939
+ if (banners) banners.stop(msg);
940
+ else console.log(msg);
941
+ try {
942
+ await rm(stopFile, { force: true });
943
+ } catch {
944
+ }
945
+ break;
946
+ }
947
+ const before = await pending();
948
+ if (before === 0) {
949
+ console.log("Plan exhausted (0 pending to-dos) - nothing to do.");
950
+ planExhausted = true;
951
+ break;
952
+ }
953
+ const logPath = path5.join(logDir, `tick-${stamp()}.log`);
954
+ const relLog = path5.relative(opts.root, logPath);
955
+ console.log("");
956
+ const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
957
+ if (banners) banners.tickStart(tickLine);
958
+ else console.log(tickLine);
959
+ let agentExit = 0;
960
+ let hitlStop;
961
+ let lastResult;
962
+ try {
963
+ const result = await runTickWithSpinner(
964
+ `tick ${tick}`,
965
+ opts.backend,
966
+ {
967
+ workspace: opts.root,
968
+ prompt: TICK_PROMPT,
969
+ model: opts.model,
970
+ logPath,
971
+ hitl: { policy: opts.noHitl ? "off" : "prompt" }
972
+ },
973
+ opts.plain === true
974
+ );
975
+ agentExit = result.exitCode;
976
+ for (const line of formatHitlSummary(result.hitl)) console.log(line);
977
+ hitlStop = result.hitl?.stop;
978
+ lastResult = result.lastResult;
979
+ } catch (err) {
980
+ logger.error(String(err));
981
+ return 1;
982
+ }
983
+ if (hitlStop) {
984
+ const msg = `${opts.backend.id} tick ended at a HITL gate without a reply - stopping. See log: ${relLog}`;
985
+ if (banners) {
986
+ banners.tickEnd("hitl stop");
987
+ banners.stop(msg);
988
+ } else {
989
+ console.log(msg);
990
+ }
991
+ exitCode = hitlStop.exitCode;
992
+ break;
993
+ }
994
+ try {
995
+ const logText = await readFile4(logPath, "utf8");
996
+ if (logText.includes("Too many MCP tools")) {
997
+ const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
998
+ if (banners) banners.stop(msg);
999
+ else console.log(msg);
1000
+ break;
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ const status = await resolveTickResultStatus(lastResult, logPath);
1005
+ if (status?.isError) {
1006
+ const detail = status.errors[0] ? `: ${status.errors[0].split("\n")[0]}` : "";
1007
+ const msg = `${opts.backend.id} tick failed (${status.subtype ?? "is_error"}${detail}) - stopping. See log: ${relLog}`;
1008
+ if (banners) {
1009
+ banners.tickEnd(status.subtype ?? "is_error");
1010
+ banners.stop(msg);
1011
+ } else {
1012
+ console.log(msg);
1013
+ }
1014
+ break;
1015
+ }
1016
+ if (agentExit !== 0) {
1017
+ const msg = `${opts.backend.id} exited with code ${agentExit} - stopping. See log: ${relLog}`;
1018
+ if (banners) {
1019
+ banners.tickEnd(`exit ${agentExit}`);
1020
+ banners.stop(msg);
1021
+ } else {
1022
+ console.log(msg);
1023
+ }
1024
+ break;
1025
+ }
1026
+ const sentinel = await resolveTickSentinel(lastResult, logPath);
1027
+ const after = await pending();
1028
+ if (banners) {
1029
+ banners.tickEnd(`pending: ${after}`);
1030
+ }
1031
+ if (sentinel.kind === "continue") {
1032
+ } else if (sentinel.kind === "stop") {
1033
+ console.log(`Agent requested stop: ${formatSentinelLine(sentinel)}`);
1034
+ stopReason = sentinel.reason;
1035
+ if (after === 0 || isPlanExhaustedReason(sentinel.reason)) {
1036
+ planExhausted = true;
1037
+ }
1038
+ break;
1039
+ } else if (after < before) {
1040
+ console.log(
1041
+ `warn: tick without sentinel, but progress (${before} -> ${after}) - continuing.`
1042
+ );
1043
+ } else {
1044
+ const msg = `Tick without sentinel and no progress (${before} -> ${after}) - stopping for safety.`;
1045
+ if (banners) banners.stop(msg);
1046
+ else console.log(msg);
1047
+ break;
1048
+ }
1049
+ if (after === 0) {
1050
+ console.log("All to-dos completed. Suggesting /git-prod stays with the human (HITL).");
1051
+ planExhausted = true;
1052
+ break;
1053
+ }
1054
+ if (opts.sleepSeconds > 0) {
1055
+ if (banners) banners.sleep(opts.sleepSeconds);
1056
+ await withCliProgress(`sleep ${opts.sleepSeconds}s`, () => sleep(opts.sleepSeconds * 1e3));
1057
+ }
1058
+ }
1059
+ const pendingNow = await pending();
1060
+ console.log("");
1061
+ const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
1062
+ if (banners) banners.phaseComplete(finishDetail);
1063
+ console.log(
1064
+ `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path5.relative(opts.root, logDir)}/`
1065
+ );
1066
+ if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
1067
+ await armExternalPlanReview(opts.root);
1068
+ }
1069
+ return exitCode;
1070
+ } finally {
1071
+ process.off("SIGINT", onSigInt);
1072
+ }
1073
+ }
1074
+
1075
+ export {
1076
+ shouldUseLiveTui,
1077
+ withLiveTui,
1078
+ listDetectBackendIds,
1079
+ detectAgentBackend,
1080
+ runPlanLoop
1081
+ };