@dadado/agent-kit-cli 5.16.0 → 5.18.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 (40) hide show
  1. package/dashboard/README.md +10 -1
  2. package/dashboard/dashboard-data.mjs +12 -2
  3. package/dashboard/dashboard.html +58 -0
  4. package/dashboard/lib/run-logs.d.mts +34 -0
  5. package/dashboard/lib/run-logs.mjs +218 -0
  6. package/dist/{add-7UMSGVWC.js → add-KQJE3ZTG.js} +3 -3
  7. package/dist/{chunk-KD2MYBB6.js → chunk-3J3I2W22.js} +1 -1
  8. package/dist/{chunk-YGBBHKBE.js → chunk-6FXQINND.js} +2 -2
  9. package/dist/{chunk-OUQZC744.js → chunk-B5CH47OS.js} +852 -74
  10. package/dist/chunk-C26I6HTO.js +39 -0
  11. package/dist/{chunk-6QV2BFGA.js → chunk-GSCXFJB7.js} +1 -1
  12. package/dist/{chunk-KI6ZHBPV.js → chunk-HRILMD5N.js} +312 -11
  13. package/dist/{chunk-OZ4QV6LM.js → chunk-KIXK7KOW.js} +42 -148
  14. package/dist/chunk-P6PMKYWU.js +50 -0
  15. package/dist/{chunk-WW3YZKHW.js → chunk-RZ5TNBHU.js} +25 -4
  16. package/dist/{chunk-TFZ2JUXH.js → chunk-TOWH3XCJ.js} +520 -70
  17. package/dist/{chunk-CGTE7DHN.js → chunk-TZYKLR5J.js} +135 -20
  18. package/dist/{chunk-5OXPNVCF.js → chunk-UEKWZ7BN.js} +2 -2
  19. package/dist/chunk-WGLPZXNU.js +45 -0
  20. package/dist/{chunk-VS2JD2XC.js → chunk-WSJ77EHO.js} +5 -2
  21. package/dist/{chunk-VGZBARQC.js → chunk-YJMZ6UM2.js} +2 -2
  22. package/dist/{chunk-XZZAVHH4.js → chunk-YMYLO5LG.js} +2 -2
  23. package/dist/{chunk-XWPO3VYQ.js → chunk-ZDCEAGD4.js} +1 -1
  24. package/dist/{contribute-YKK6YCSI.js → contribute-LFT762Y4.js} +4 -4
  25. package/dist/{diff-YTR7VOAN.js → diff-56KMRCER.js} +5 -5
  26. package/dist/doctor-AA64XVEY.js +237 -0
  27. package/dist/{handoff-3UI6VKVG.js → handoff-34SDU4FF.js} +2 -2
  28. package/dist/{hook-MUV6Y7UW.js → hook-KRIXBLNW.js} +7 -7
  29. package/dist/index.js +19 -17
  30. package/dist/{init-TTTKGC4C.js → init-XP5EAUSJ.js} +12 -11
  31. package/dist/{install-ZMFJBPOI.js → install-SHDEX54F.js} +12 -11
  32. package/dist/{plan-index-JMC7Z4DW.js → plan-index-O3545OA7.js} +2 -2
  33. package/dist/{run-I3BFGULT.js → run-FADX6QCP.js} +6 -5
  34. package/dist/{run-plan-FO7CL4M3.js → run-plan-MO4IFAKV.js} +36 -5
  35. package/dist/{setup-global-JUVMJXJ5.js → setup-global-CBWLXOQG.js} +6 -6
  36. package/dist/{status-MVCUNCWX.js → status-BSIXWZMY.js} +19 -7
  37. package/dist/{update-Q4EBIRUB.js → update-FJX3DA6X.js} +33 -10
  38. package/package.json +3 -3
  39. package/dist/chunk-5VCOWTTP.js +0 -6
  40. package/dist/doctor-TA2FWREU.js +0 -497
@@ -8,18 +8,19 @@ import {
8
8
  extractHandoffNamedPlans,
9
9
  namedPlanCandidatePaths,
10
10
  parsePlanFrontmatter
11
- } from "./chunk-KD2MYBB6.js";
11
+ } from "./chunk-3J3I2W22.js";
12
12
  import {
13
13
  LineSplitter,
14
14
  RedactingStreamBuffer,
15
15
  claudeRedactions,
16
+ formatHitlGateLine,
16
17
  formatHitlSummary,
17
18
  operatorQuitStop,
18
19
  parseStreamLine,
19
20
  redactSecrets,
20
21
  renderStreamEvent,
21
22
  whichBinary
22
- } from "./chunk-OUQZC744.js";
23
+ } from "./chunk-B5CH47OS.js";
23
24
  import {
24
25
  TtySpinner,
25
26
  shouldUseVisualMotion,
@@ -30,8 +31,11 @@ import {
30
31
  logger
31
32
  } from "./chunk-ZB43YMHW.js";
32
33
  import {
33
- HANDOFF_REL
34
- } from "./chunk-5VCOWTTP.js";
34
+ HANDOFF_REL,
35
+ STATE_ROOT_ENV,
36
+ ensureLoopLogsDir,
37
+ runnerStatePaths
38
+ } from "./chunk-C26I6HTO.js";
35
39
  import {
36
40
  fileExists,
37
41
  readJson
@@ -486,18 +490,21 @@ async function withLiveTui(opts, run) {
486
490
 
487
491
  // src/plan-loop/detect.ts
488
492
  var DETECT_ORDER = ["cursor-agent", "claude"];
493
+ var EXPLICIT_ONLY_BACKENDS = ["codex", "cursor-acp"];
489
494
  var INSTALL_HINTS = {
490
495
  "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)."
496
+ claude: "Install Claude Code and ensure `claude` is on PATH (https://code.claude.com/docs/en/quickstart).",
497
+ "cursor-acp": "Install the Cursor Agent CLI and ensure `cursor-agent` is on PATH (https://cursor.com/docs/cli/acp); sign in with `cursor-agent login`.",
498
+ codex: "Install the Codex CLI and ensure `codex` is on PATH (https://developers.openai.com/codex/cli)."
492
499
  };
493
500
  function isBackendId(id) {
494
- return id === "cursor-agent" || id === "claude";
501
+ return id === "cursor-agent" || id === "claude" || id === "codex" || id === "cursor-acp";
495
502
  }
496
503
  function isRequestedBackend(id) {
497
504
  return id === "auto" || isBackendId(id);
498
505
  }
499
506
  function listDetectBackendIds() {
500
- return ["auto", ...DETECT_ORDER];
507
+ return ["auto", ...DETECT_ORDER, ...EXPLICIT_ONLY_BACKENDS];
501
508
  }
502
509
  async function detectAgentBackend(requested, whichFn = whichBinary) {
503
510
  const id = requested.trim().toLowerCase();
@@ -534,8 +541,220 @@ async function detectAgentBackend(requested, whichFn = whichBinary) {
534
541
  return { ok: true, id, bin };
535
542
  }
536
543
 
544
+ // src/plan-loop/driver-events.ts
545
+ import { createInterface } from "readline";
546
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
547
+ var DRIVER_PROTOCOL_MAJOR = 1;
548
+ var DRIVER_PROTOCOL_VERSION = "1.1.0";
549
+ var DRIVER_EVENTS_FORMATS = ["ndjson"];
550
+ var INBOUND_MAX_CHARS = 64 * 1024;
551
+ var AGENT_PENDING_MAX_CHARS = 4 * 1024 * 1024;
552
+ function formatDriverEvent(event, now = () => /* @__PURE__ */ new Date()) {
553
+ return `${JSON.stringify({ v: DRIVER_PROTOCOL_MAJOR, ts: now().toISOString(), ...event })}
554
+ `;
555
+ }
556
+ function createDriverEmitter(write = (text) => {
557
+ process.stdout.write(text);
558
+ }, now) {
559
+ return {
560
+ emit(event) {
561
+ try {
562
+ write(formatDriverEvent(event, now));
563
+ } catch {
564
+ }
565
+ }
566
+ };
567
+ }
568
+ function parseDriverInbound(line) {
569
+ const text = line.trim();
570
+ if (text.length > INBOUND_MAX_CHARS) return { ok: false, error: "line too long" };
571
+ let value;
572
+ try {
573
+ value = JSON.parse(text);
574
+ } catch {
575
+ return { ok: false, error: "not JSON" };
576
+ }
577
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
578
+ return { ok: false, error: "not a JSON object" };
579
+ }
580
+ const msg = value;
581
+ if (msg.v !== void 0 && msg.v !== DRIVER_PROTOCOL_MAJOR) {
582
+ return { ok: false, error: `unsupported protocol v${String(msg.v)}` };
583
+ }
584
+ if (msg.type === "stop") {
585
+ return {
586
+ ok: true,
587
+ message: { type: "stop", ...typeof msg.reason === "string" ? { reason: msg.reason } : {} }
588
+ };
589
+ }
590
+ if (msg.type !== "hitl_reply")
591
+ return { ok: false, error: `unknown type ${JSON.stringify(msg.type)}` };
592
+ if (typeof msg.askId !== "string" || !/^[a-z0-9-]+$/.test(msg.askId)) {
593
+ return { ok: false, error: "hitl_reply.askId must be a kebab-case string" };
594
+ }
595
+ if (typeof msg.reply === "number") {
596
+ if (!Number.isInteger(msg.reply) || msg.reply < 1) {
597
+ return { ok: false, error: "hitl_reply.reply number must be a positive integer" };
598
+ }
599
+ return { ok: true, message: { type: "hitl_reply", askId: msg.askId, reply: msg.reply } };
600
+ }
601
+ if (typeof msg.reply === "string" && msg.reply.trim() && !/[\r\n]/.test(msg.reply)) {
602
+ return { ok: true, message: { type: "hitl_reply", askId: msg.askId, reply: msg.reply.trim() } };
603
+ }
604
+ return { ok: false, error: "hitl_reply.reply must be a number or a one-line non-empty string" };
605
+ }
606
+ function inboundToRawAnswer(gate, parsed) {
607
+ if (!parsed.ok) return { kind: "stop", cause: "protocol error", detail: parsed.error };
608
+ const message = parsed.message;
609
+ if (message.type === "stop") return { kind: "stop", cause: "driver stop" };
610
+ if (message.askId !== gate.askId) {
611
+ return {
612
+ kind: "stop",
613
+ cause: "protocol error",
614
+ detail: `reply for ${message.askId} while gate ${gate.askId} is open`
615
+ };
616
+ }
617
+ return { kind: "line", text: String(message.reply) };
618
+ }
619
+ function createDriverAnswerReader(opts) {
620
+ const input = opts.input ?? process.stdin;
621
+ const queue = [];
622
+ let ended = false;
623
+ let waiter = null;
624
+ let rl = null;
625
+ const ensure = () => {
626
+ if (rl) return;
627
+ rl = createInterface({ input, terminal: false, crlfDelay: Number.POSITIVE_INFINITY });
628
+ rl.on("line", (line) => {
629
+ if (!line.trim()) return;
630
+ if (waiter) {
631
+ const w = waiter;
632
+ waiter = null;
633
+ w(line);
634
+ } else queue.push(line);
635
+ });
636
+ rl.on("close", () => {
637
+ ended = true;
638
+ if (waiter) {
639
+ const w = waiter;
640
+ waiter = null;
641
+ w(null);
642
+ }
643
+ });
644
+ };
645
+ const nextLine = (signal) => new Promise((resolve) => {
646
+ const queued = queue.shift();
647
+ if (queued !== void 0) {
648
+ resolve(queued);
649
+ return;
650
+ }
651
+ if (ended) {
652
+ resolve(null);
653
+ return;
654
+ }
655
+ const onAbort = () => {
656
+ if (waiter === settle) waiter = null;
657
+ resolve(null);
658
+ };
659
+ const settle = (line) => {
660
+ signal?.removeEventListener("abort", onAbort);
661
+ resolve(line);
662
+ };
663
+ if (signal?.aborted) {
664
+ resolve(null);
665
+ return;
666
+ }
667
+ signal?.addEventListener("abort", onAbort, { once: true });
668
+ waiter = settle;
669
+ });
670
+ return {
671
+ read: async (gate, readOpts) => {
672
+ ensure();
673
+ opts.emitter.emit({
674
+ type: "hitl_gate",
675
+ tick: opts.tick(),
676
+ askId: gate.askId,
677
+ labels: [...gate.labels],
678
+ detection: gate.detection,
679
+ line: formatHitlGateLine(gate)
680
+ });
681
+ const line = await nextLine(readOpts.signal);
682
+ if (line === null) return { kind: "eof" };
683
+ return inboundToRawAnswer(gate, parseDriverInbound(line));
684
+ },
685
+ close: () => {
686
+ const r = rl;
687
+ rl = null;
688
+ r?.close();
689
+ if (typeof input.pause === "function") {
690
+ input.pause();
691
+ }
692
+ }
693
+ };
694
+ }
695
+ var DriverAgentSink = class {
696
+ constructor(emitter, tick) {
697
+ this.emitter = emitter;
698
+ this.tick = tick;
699
+ }
700
+ emitter;
701
+ tick;
702
+ pending = "";
703
+ dropped = false;
704
+ decoder = new StringDecoder2("utf8");
705
+ feed(chunk) {
706
+ try {
707
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
708
+ if (!text) return;
709
+ const lines = (this.pending + text).split("\n");
710
+ this.pending = lines.pop() ?? "";
711
+ for (const line of lines) {
712
+ if (this.dropped) {
713
+ this.dropped = false;
714
+ continue;
715
+ }
716
+ this.take(line);
717
+ }
718
+ if (this.pending.length > AGENT_PENDING_MAX_CHARS) {
719
+ this.pending = "";
720
+ this.dropped = true;
721
+ }
722
+ } catch {
723
+ }
724
+ }
725
+ end() {
726
+ try {
727
+ const rest = this.pending + this.decoder.end();
728
+ this.pending = "";
729
+ if (rest && !this.dropped) this.take(rest);
730
+ this.dropped = false;
731
+ } catch {
732
+ }
733
+ }
734
+ take(raw) {
735
+ const line = raw.replace(/\r$/, "");
736
+ if (!line.trim()) return;
737
+ const trimmed = line.trim();
738
+ if (trimmed.startsWith("{")) {
739
+ try {
740
+ const event = JSON.parse(trimmed);
741
+ if (typeof event === "object" && event !== null && !Array.isArray(event)) {
742
+ this.emitter.emit({
743
+ type: "agent_event",
744
+ tick: this.tick,
745
+ event
746
+ });
747
+ return;
748
+ }
749
+ } catch {
750
+ }
751
+ }
752
+ this.emitter.emit({ type: "agent_text", tick: this.tick, text: line });
753
+ }
754
+ };
755
+
537
756
  // src/plan-loop/run-loop.ts
538
- import { mkdir, readFile as readFile4, rm, unlink } from "fs/promises";
757
+ import { readFile as readFile4, rm, unlink } from "fs/promises";
539
758
  import path5 from "path";
540
759
 
541
760
  // src/plan-loop/external-review.ts
@@ -595,7 +814,8 @@ async function armExternalPlanReview(root, options = {}) {
595
814
  const emitOut = (s) => {
596
815
  if (!s) return;
597
816
  output += s;
598
- process.stdout.write(s);
817
+ if (options.writeOut) options.writeOut(s);
818
+ else process.stdout.write(s);
599
819
  };
600
820
  const emitErr = (s) => {
601
821
  if (!s) return;
@@ -839,8 +1059,87 @@ async function resolveTickSentinel(lastResult, logPath, fromFile = parseSentinel
839
1059
  return fromTurn ?? fromFile(logPath);
840
1060
  }
841
1061
 
1062
+ // src/plan-loop/usage-limit.ts
1063
+ var LIMIT_TEXT_RE = /\b(?:usage|rate)[ _-]?limit|\blimit (?:reached|exceeded)|\bquota (?:exceeded|reached)|\btoo many requests\b|\bout of (?:extra )?usage\b/i;
1064
+ var NON_LIMIT_STATUSES = /* @__PURE__ */ new Set(["allowed", "allowed_warning"]);
1065
+ function isRecord(v) {
1066
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1067
+ }
1068
+ function oneLine(text, max = 200) {
1069
+ const line = text.replace(/\s+/g, " ").trim();
1070
+ return line.length > max ? `${line.slice(0, max - 1)}\u2026` : line;
1071
+ }
1072
+ function fromRateLimitEvent(event) {
1073
+ const info = isRecord(event.rate_limit_info) ? event.rate_limit_info : event;
1074
+ const status = typeof info.status === "string" ? info.status : null;
1075
+ if (!status || NON_LIMIT_STATUSES.has(status)) return null;
1076
+ const parts = [status];
1077
+ if (typeof info.rateLimitType === "string") parts.unshift(info.rateLimitType);
1078
+ if (typeof info.resetsAt === "number" && Number.isFinite(info.resetsAt)) {
1079
+ const ms = info.resetsAt < 1e12 ? info.resetsAt * 1e3 : info.resetsAt;
1080
+ parts.push(`resets ${new Date(ms).toISOString()}`);
1081
+ }
1082
+ return { source: "rate_limit_event", detail: parts.join(" ") };
1083
+ }
1084
+ function fromErrorResult(event) {
1085
+ if (event.is_error !== true) return null;
1086
+ const texts = [];
1087
+ if (typeof event.result === "string") texts.push(event.result);
1088
+ if (typeof event.subtype === "string") texts.push(event.subtype);
1089
+ if (Array.isArray(event.errors)) {
1090
+ for (const e of event.errors) if (typeof e === "string") texts.push(e);
1091
+ }
1092
+ if (isRecord(event.error) && typeof event.error.message === "string") {
1093
+ texts.push(event.error.message);
1094
+ }
1095
+ const hit = texts.find((t) => LIMIT_TEXT_RE.test(t));
1096
+ return hit ? { source: "result", detail: oneLine(hit) } : null;
1097
+ }
1098
+ function fromCodexError(event) {
1099
+ const message = isRecord(event.error) && typeof event.error.message === "string" ? event.error.message : typeof event.message === "string" ? event.message : "";
1100
+ return LIMIT_TEXT_RE.test(message) ? { source: "result", detail: oneLine(message) } : null;
1101
+ }
1102
+ var PRIORITY = {
1103
+ rate_limit_event: 3,
1104
+ result: 2,
1105
+ stderr: 1
1106
+ };
1107
+ function detectUsageLimit(logText) {
1108
+ let last = null;
1109
+ const take = (hit) => {
1110
+ if (!last || PRIORITY[hit.source] >= PRIORITY[last.source]) last = hit;
1111
+ };
1112
+ for (const raw of logText.split("\n")) {
1113
+ const line = raw.trim();
1114
+ if (!line) continue;
1115
+ if (!line.startsWith("{")) {
1116
+ if (LIMIT_TEXT_RE.test(line)) take({ source: "stderr", detail: oneLine(line) });
1117
+ continue;
1118
+ }
1119
+ let event;
1120
+ try {
1121
+ event = JSON.parse(line);
1122
+ } catch {
1123
+ continue;
1124
+ }
1125
+ if (!isRecord(event)) continue;
1126
+ const hit = event.type === "rate_limit_event" ? fromRateLimitEvent(event) : event.type === "result" ? fromErrorResult(event) : event.type === "turn.failed" || event.type === "error" || event.jsonrpc === "2.0" && isRecord(event.error) ? fromCodexError(event) : null;
1127
+ if (hit) take(hit);
1128
+ }
1129
+ return last;
1130
+ }
1131
+
842
1132
  // src/plan-loop/run-loop.ts
843
1133
  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).';
1134
+ function driverSentinel(sentinel) {
1135
+ if (!sentinel) return null;
1136
+ const line = formatSentinelLine(sentinel);
1137
+ return {
1138
+ kind: sentinel.kind,
1139
+ ...sentinel.kind === "stop" ? { reason: sentinel.reason } : {},
1140
+ line: line || null
1141
+ };
1142
+ }
844
1143
  async function runTickWithSpinner(label, backend, opts, plain) {
845
1144
  if (shouldUseLiveTui({ plain })) {
846
1145
  return withLiveTui(
@@ -884,60 +1183,119 @@ function sleep(ms) {
884
1183
  return new Promise((r) => setTimeout(r, ms));
885
1184
  }
886
1185
  async function runPlanLoop(opts) {
887
- const stopFile = path5.join(opts.root, ".cursor", "loop.stop");
888
- const logDir = path5.join(opts.root, ".cursor", "loop-logs");
1186
+ const statePaths = runnerStatePaths(opts.root);
1187
+ const stopFile = statePaths.stopFile;
1188
+ const logDir = statePaths.loopLogsDir;
1189
+ let tick = 0;
1190
+ let ticksRun = 0;
1191
+ const driver = opts.events ? (() => {
1192
+ const emitter = createDriverEmitter(opts.events.write, opts.events.now);
1193
+ return {
1194
+ emitter,
1195
+ reader: createDriverAnswerReader({
1196
+ input: opts.events.input,
1197
+ emitter,
1198
+ tick: () => tick
1199
+ }),
1200
+ writeErr: opts.events.writeErr ?? ((text) => {
1201
+ process.stderr.write(text);
1202
+ })
1203
+ };
1204
+ })() : null;
1205
+ const say = (message, level = "info") => {
1206
+ if (driver) driver.emitter.emit({ type: "log", level, message });
1207
+ else if (level === "error") logger.error(message);
1208
+ else console.log(message);
1209
+ };
1210
+ const emit = (event) => driver?.emitter.emit(event);
1211
+ const finishRun = (exitCode, reason, pending2, planExhausted = false) => {
1212
+ emit({ type: "run_end", exitCode, ticks: ticksRun, pending: pending2, reason, planExhausted });
1213
+ driver?.reader.close();
1214
+ return exitCode;
1215
+ };
889
1216
  const planPath = await resolveActivePlanPath(opts.root);
890
1217
  if (!planPath) {
891
- logger.error("No active plan in .cursor/plans/");
892
- return 1;
1218
+ say("No active plan in .cursor/plans/", "error");
1219
+ return finishRun(1, "no active plan", null);
893
1220
  }
894
1221
  const pending = async () => countPendingTodos(await readPlan(planPath));
895
- await mkdir(logDir, { recursive: true });
1222
+ if (statePaths.invalid !== void 0) {
1223
+ say(
1224
+ `${STATE_ROOT_ENV}=${statePaths.invalid} is not supported (use .cursor or .agent-kit); using .cursor.`,
1225
+ "warn"
1226
+ );
1227
+ }
1228
+ await ensureLoopLogsDir(statePaths);
896
1229
  try {
897
1230
  await unlink(stopFile);
898
1231
  } catch {
899
1232
  }
900
1233
  const onSigInt = () => {
901
- console.log("");
902
- console.log("Loop interrupted (Ctrl+C). State saved in HANDOFF by the last tick.");
1234
+ if (driver) {
1235
+ emit({
1236
+ type: "run_end",
1237
+ exitCode: 130,
1238
+ ticks: ticksRun,
1239
+ pending: null,
1240
+ reason: "interrupted (SIGINT)",
1241
+ planExhausted: false
1242
+ });
1243
+ } else {
1244
+ console.log("");
1245
+ console.log("Loop interrupted (Ctrl+C). State saved in HANDOFF by the last tick.");
1246
+ }
903
1247
  process.exit(130);
904
1248
  };
905
1249
  process.on("SIGINT", onSigInt);
906
1250
  try {
907
- const persona = await loadCliRunPlanPersona(opts.root);
1251
+ const persona = driver ? null : await loadCliRunPlanPersona(opts.root);
908
1252
  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}`);
1253
+ const pendingAtStart = await pending();
1254
+ emit({
1255
+ type: "run_start",
1256
+ protocol: DRIVER_PROTOCOL_VERSION,
1257
+ backend: opts.backend.id,
1258
+ plan: path5.basename(planPath),
1259
+ pending: pendingAtStart,
1260
+ maxTicks: opts.maxTicks,
1261
+ hitl: opts.noHitl ? "off" : "driver"
1262
+ });
1263
+ say(`Active plan: ${path5.basename(planPath)}`);
1264
+ say(`Pending to-dos: ${pendingAtStart} | max ticks: ${opts.maxTicks}`);
1265
+ say(`Backend: ${opts.backend.id}`);
912
1266
  if (persona) {
913
- console.log(`CLI persona: ${persona.displayName ?? persona.id}`);
1267
+ say(`CLI persona: ${persona.displayName ?? persona.id}`);
914
1268
  }
915
1269
  if (opts.dryRun) {
916
- console.log("--dry-run: no agent will be started. Tick prompt:");
917
- console.log(TICK_PROMPT);
918
- return 0;
1270
+ say("--dry-run: no agent will be started. Tick prompt:");
1271
+ say(TICK_PROMPT);
1272
+ return finishRun(0, "dry-run", pendingAtStart);
919
1273
  }
920
1274
  const binary = await opts.backend.resolve();
921
1275
  if (!binary) {
922
- logger.error(`${opts.backend.id} not found on PATH`);
923
- return 1;
1276
+ say(`${opts.backend.id} not found on PATH`, "error");
1277
+ return finishRun(1, `${opts.backend.id} not found on PATH`, pendingAtStart);
924
1278
  }
925
- let tick = 0;
926
1279
  let stopReason;
927
1280
  let planExhausted = false;
928
1281
  let exitCode = 0;
1282
+ let endReason = "";
1283
+ let backend = opts.backend;
1284
+ let limitSwitched = false;
929
1285
  while (true) {
930
1286
  tick += 1;
931
1287
  if (tick > opts.maxTicks) {
932
1288
  const msg = `Budget of ${opts.maxTicks} ticks reached - stopping. Run again to continue.`;
933
1289
  if (banners) banners.stop(msg);
934
- else console.log(msg);
1290
+ else say(msg);
1291
+ endReason = `budget of ${opts.maxTicks} ticks reached`;
935
1292
  break;
936
1293
  }
937
1294
  if (await fileExists(stopFile)) {
938
1295
  const msg = `Stop file found (${stopFile}) - stopping.`;
939
1296
  if (banners) banners.stop(msg);
940
- else console.log(msg);
1297
+ else say(msg);
1298
+ endReason = "stop file found";
941
1299
  try {
942
1300
  await rm(stopFile, { force: true });
943
1301
  } catch {
@@ -946,81 +1304,159 @@ async function runPlanLoop(opts) {
946
1304
  }
947
1305
  const before = await pending();
948
1306
  if (before === 0) {
949
- console.log("Plan exhausted (0 pending to-dos) - nothing to do.");
1307
+ say("Plan exhausted (0 pending to-dos) - nothing to do.");
950
1308
  planExhausted = true;
1309
+ endReason = "plan exhausted";
951
1310
  break;
952
1311
  }
953
1312
  const logPath = path5.join(logDir, `tick-${stamp()}.log`);
954
1313
  const relLog = path5.relative(opts.root, logPath);
955
- console.log("");
1314
+ if (!driver) console.log("");
956
1315
  const tickLine = `=== tick ${tick}/${opts.maxTicks} - pending: ${before} - log: ${relLog} ===`;
957
1316
  if (banners) banners.tickStart(tickLine);
958
- else console.log(tickLine);
1317
+ else say(tickLine);
1318
+ ticksRun = tick;
1319
+ emit({
1320
+ type: "tick_start",
1321
+ tick,
1322
+ maxTicks: opts.maxTicks,
1323
+ pending: before,
1324
+ log: relLog,
1325
+ prompt: TICK_PROMPT,
1326
+ backend: backend.id
1327
+ });
959
1328
  let agentExit = 0;
960
1329
  let hitlStop;
961
1330
  let lastResult;
1331
+ let replies = [];
1332
+ let limit = null;
1333
+ const endTick = (pendingAfter, sentinel2) => emit({
1334
+ type: "tick_end",
1335
+ tick,
1336
+ backend: backend.id,
1337
+ limit,
1338
+ exitCode: agentExit,
1339
+ pendingBefore: before,
1340
+ pendingAfter,
1341
+ result: driverSentinel(sentinel2),
1342
+ replies: replies.map(({ askId, reply, label, line, at }) => ({
1343
+ askId,
1344
+ reply,
1345
+ label,
1346
+ line,
1347
+ at
1348
+ })),
1349
+ stop: hitlStop ? {
1350
+ askId: hitlStop.askId,
1351
+ cause: hitlStop.cause,
1352
+ message: hitlStop.message,
1353
+ exitCode: hitlStop.exitCode
1354
+ } : null
1355
+ });
962
1356
  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
- );
1357
+ const runOpts = {
1358
+ workspace: opts.root,
1359
+ prompt: TICK_PROMPT,
1360
+ model: opts.model,
1361
+ logPath,
1362
+ hitl: { policy: opts.noHitl ? "off" : "prompt" }
1363
+ };
1364
+ const result = driver ? await backend.run({
1365
+ ...runOpts,
1366
+ log: (line) => say(line),
1367
+ render: new DriverAgentSink(driver.emitter, tick),
1368
+ hitl: {
1369
+ policy: opts.noHitl ? "off" : "driver",
1370
+ readAnswer: driver.reader.read,
1371
+ write: driver.writeErr
1372
+ }
1373
+ }) : await runTickWithSpinner(`tick ${tick}`, backend, runOpts, opts.plain === true);
975
1374
  agentExit = result.exitCode;
976
- for (const line of formatHitlSummary(result.hitl)) console.log(line);
1375
+ for (const line of formatHitlSummary(result.hitl)) say(line);
977
1376
  hitlStop = result.hitl?.stop;
1377
+ replies = result.hitl?.replies ?? [];
978
1378
  lastResult = result.lastResult;
979
1379
  } catch (err) {
980
- logger.error(String(err));
981
- return 1;
1380
+ say(String(err), "error");
1381
+ agentExit = 1;
1382
+ endTick(null, null);
1383
+ return finishRun(1, `tick ${tick} failed to start: ${String(err)}`, await pending());
982
1384
  }
983
1385
  if (hitlStop) {
984
- const msg = `${opts.backend.id} tick ended at a HITL gate without a reply - stopping. See log: ${relLog}`;
1386
+ const msg = `${backend.id} tick ended at a HITL gate without a reply - stopping. See log: ${relLog}`;
985
1387
  if (banners) {
986
1388
  banners.tickEnd("hitl stop");
987
1389
  banners.stop(msg);
988
1390
  } else {
989
- console.log(msg);
1391
+ say(msg);
990
1392
  }
1393
+ endTick(await pending(), null);
991
1394
  exitCode = hitlStop.exitCode;
1395
+ endReason = hitlStop.message;
992
1396
  break;
993
1397
  }
1398
+ let logText = "";
994
1399
  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
- }
1400
+ logText = await readFile4(logPath, "utf8");
1002
1401
  } catch {
1003
1402
  }
1403
+ limit = detectUsageLimit(logText);
1404
+ if (limit) {
1405
+ const hit = limit;
1406
+ const fallback = opts.onLimit && !limitSwitched && opts.onLimit.id !== backend.id ? opts.onLimit : null;
1407
+ const fallbackBin = fallback ? await fallback.resolve() : null;
1408
+ endTick(await pending(), null);
1409
+ if (fallback && fallbackBin) {
1410
+ const msg2 = `${backend.id} stopped on a usage limit (${hit.detail}); next tick continues the plan on ${fallback.id} (--on-limit). See log: ${relLog}`;
1411
+ if (banners) banners.tickEnd("usage limit");
1412
+ say(msg2, "warn");
1413
+ backend = fallback;
1414
+ limitSwitched = true;
1415
+ continue;
1416
+ }
1417
+ const why = fallback ? ` (--on-limit ${fallback.id} not found on PATH)` : "";
1418
+ const msg = `${backend.id} stopped on a usage limit (${hit.detail})${why} - stopping. The plan is unchanged: run again after the reset, or with --backend <other>. See log: ${relLog}`;
1419
+ if (banners) {
1420
+ banners.tickEnd("usage limit");
1421
+ banners.stop(msg);
1422
+ } else {
1423
+ say(msg);
1424
+ }
1425
+ endReason = `usage-limit: ${backend.id}`;
1426
+ break;
1427
+ }
1428
+ if (logText.includes("Too many MCP tools")) {
1429
+ const msg = "Too many MCP tools for the headless model - disable servers (cursor-agent mcp disable <id>) and run again.";
1430
+ if (banners) banners.stop(msg);
1431
+ else say(msg);
1432
+ endTick(await pending(), null);
1433
+ endReason = "too many MCP tools";
1434
+ break;
1435
+ }
1004
1436
  const status = await resolveTickResultStatus(lastResult, logPath);
1005
1437
  if (status?.isError) {
1006
1438
  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}`;
1439
+ const msg = `${backend.id} tick failed (${status.subtype ?? "is_error"}${detail}) - stopping. See log: ${relLog}`;
1008
1440
  if (banners) {
1009
1441
  banners.tickEnd(status.subtype ?? "is_error");
1010
1442
  banners.stop(msg);
1011
1443
  } else {
1012
- console.log(msg);
1444
+ say(msg);
1013
1445
  }
1446
+ endTick(await pending(), null);
1447
+ endReason = `tick failed (${status.subtype ?? "is_error"})`;
1014
1448
  break;
1015
1449
  }
1016
1450
  if (agentExit !== 0) {
1017
- const msg = `${opts.backend.id} exited with code ${agentExit} - stopping. See log: ${relLog}`;
1451
+ const msg = `${backend.id} exited with code ${agentExit} - stopping. See log: ${relLog}`;
1018
1452
  if (banners) {
1019
1453
  banners.tickEnd(`exit ${agentExit}`);
1020
1454
  banners.stop(msg);
1021
1455
  } else {
1022
- console.log(msg);
1456
+ say(msg);
1023
1457
  }
1458
+ endTick(await pending(), null);
1459
+ endReason = `${backend.id} exited with code ${agentExit}`;
1024
1460
  break;
1025
1461
  }
1026
1462
  const sentinel = await resolveTickSentinel(lastResult, logPath);
@@ -1028,45 +1464,58 @@ async function runPlanLoop(opts) {
1028
1464
  if (banners) {
1029
1465
  banners.tickEnd(`pending: ${after}`);
1030
1466
  }
1467
+ endTick(after, sentinel);
1031
1468
  if (sentinel.kind === "continue") {
1032
1469
  } else if (sentinel.kind === "stop") {
1033
- console.log(`Agent requested stop: ${formatSentinelLine(sentinel)}`);
1470
+ say(`Agent requested stop: ${formatSentinelLine(sentinel)}`);
1034
1471
  stopReason = sentinel.reason;
1472
+ endReason = `agent requested stop: ${sentinel.reason}`;
1035
1473
  if (after === 0 || isPlanExhaustedReason(sentinel.reason)) {
1036
1474
  planExhausted = true;
1037
1475
  }
1038
1476
  break;
1039
1477
  } else if (after < before) {
1040
- console.log(
1041
- `warn: tick without sentinel, but progress (${before} -> ${after}) - continuing.`
1478
+ say(
1479
+ `warn: tick without sentinel, but progress (${before} -> ${after}) - continuing.`,
1480
+ "warn"
1042
1481
  );
1043
1482
  } else {
1044
1483
  const msg = `Tick without sentinel and no progress (${before} -> ${after}) - stopping for safety.`;
1045
1484
  if (banners) banners.stop(msg);
1046
- else console.log(msg);
1485
+ else say(msg);
1486
+ endReason = "tick without sentinel and no progress";
1047
1487
  break;
1048
1488
  }
1049
1489
  if (after === 0) {
1050
- console.log("All to-dos completed. Suggesting /git-prod stays with the human (HITL).");
1490
+ say("All to-dos completed. Suggesting /git-prod stays with the human (HITL).");
1051
1491
  planExhausted = true;
1492
+ endReason = "plan exhausted";
1052
1493
  break;
1053
1494
  }
1054
1495
  if (opts.sleepSeconds > 0) {
1055
1496
  if (banners) banners.sleep(opts.sleepSeconds);
1056
- await withCliProgress(`sleep ${opts.sleepSeconds}s`, () => sleep(opts.sleepSeconds * 1e3));
1497
+ if (driver) await sleep(opts.sleepSeconds * 1e3);
1498
+ else
1499
+ await withCliProgress(
1500
+ `sleep ${opts.sleepSeconds}s`,
1501
+ () => sleep(opts.sleepSeconds * 1e3)
1502
+ );
1057
1503
  }
1058
1504
  }
1059
1505
  const pendingNow = await pending();
1060
- console.log("");
1506
+ if (!driver) console.log("");
1061
1507
  const finishDetail = `after ${tick} tick(s); pending: ${pendingNow}`;
1062
1508
  if (banners) banners.phaseComplete(finishDetail);
1063
- console.log(
1509
+ say(
1064
1510
  `Loop finished after ${tick} tick(s). Pending now: ${pendingNow}. Logs in ${path5.relative(opts.root, logDir)}/`
1065
1511
  );
1066
1512
  if (planExhausted || shouldArmExternalPlanReview({ pending: pendingNow, stopReason })) {
1067
- await armExternalPlanReview(opts.root);
1513
+ await armExternalPlanReview(
1514
+ opts.root,
1515
+ driver ? { log: (line) => say(line), writeOut: driver.writeErr } : {}
1516
+ );
1068
1517
  }
1069
- return exitCode;
1518
+ return finishRun(exitCode, endReason || "stopped", pendingNow, planExhausted);
1070
1519
  } finally {
1071
1520
  process.off("SIGINT", onSigInt);
1072
1521
  }
@@ -1077,5 +1526,6 @@ export {
1077
1526
  withLiveTui,
1078
1527
  listDetectBackendIds,
1079
1528
  detectAgentBackend,
1529
+ DRIVER_EVENTS_FORMATS,
1080
1530
  runPlanLoop
1081
1531
  };