@adhdev/daemon-core 0.9.82-rc.260 → 0.9.82-rc.261

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.
package/dist/index.mjs CHANGED
@@ -28408,8 +28408,12 @@ var TerminalAdapter = class {
28408
28408
  this.rows = opts.rows ?? DEFAULT_SESSION_HOST_ROWS5;
28409
28409
  this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
28410
28410
  this.tickIntervalMs = opts.tickIntervalMs ?? 0;
28411
- const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
28412
- this.factory = opts.transportFactory ?? new NodePtyTransportFactory2();
28411
+ if (opts.transportFactory) {
28412
+ this.factory = opts.transportFactory;
28413
+ } else {
28414
+ const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS(pty_transport_exports));
28415
+ this.factory = new NodePtyTransportFactory2();
28416
+ }
28413
28417
  this.screen = new TerminalScreen(this.rows, this.cols);
28414
28418
  }
28415
28419
  rows;
@@ -28566,6 +28570,10 @@ var FsmDriver = class {
28566
28570
  specWatcher = null;
28567
28571
  /** Last full FSM evaluation, kept for the debugger. */
28568
28572
  lastFsmEval = null;
28573
+ /** Ring buffer (max 20) of the full FSM evaluation captured at each
28574
+ * transition — the rich pre-transition table that lastFsmEval only keeps
28575
+ * for the single most recent evaluation. Separate from stateHistory. */
28576
+ fsmSnapshotHistory = [];
28569
28577
  subscribe(listener) {
28570
28578
  this.listeners.add(listener);
28571
28579
  return () => {
@@ -28656,6 +28664,9 @@ var FsmDriver = class {
28656
28664
  getStateHistory() {
28657
28665
  return this.stateHistory;
28658
28666
  }
28667
+ getFsmSnapshotHistory() {
28668
+ return this.fsmSnapshotHistory;
28669
+ }
28659
28670
  getSections() {
28660
28671
  try {
28661
28672
  const screen = this.adapter.snapshot();
@@ -28751,7 +28762,7 @@ var FsmDriver = class {
28751
28762
  this.lastFsmEval = ev;
28752
28763
  this.prevScreenLines = currentLines;
28753
28764
  if (ev.fired) {
28754
- this.commitTransition(ev.fired, now);
28765
+ this.commitTransition(ev.fired, now, ev);
28755
28766
  this.emitStateChanged(forceEmit);
28756
28767
  this.scheduleWakeForState();
28757
28768
  return;
@@ -28760,8 +28771,9 @@ var FsmDriver = class {
28760
28771
  this.scheduleWakeForState();
28761
28772
  this.maybeMarkReady();
28762
28773
  }
28763
- commitTransition(fired, now) {
28774
+ commitTransition(fired, now, ev) {
28764
28775
  const from = this.currentStateId;
28776
+ this.pushFsmSnapshot(from, fired, now, ev);
28765
28777
  this.currentStateId = fired.to;
28766
28778
  this.stateEnteredAt = now;
28767
28779
  this.regionLastChangedAt.clear();
@@ -28772,6 +28784,22 @@ var FsmDriver = class {
28772
28784
  });
28773
28785
  LOG.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
28774
28786
  }
28787
+ /** Snapshot the full FSM evaluation that produced a transition into the
28788
+ * separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
28789
+ * table is captured by reference — it is freshly built per evaluation in
28790
+ * evaluateFsm and never mutated after, so no clone is needed. */
28791
+ pushFsmSnapshot(from, fired, now, ev) {
28792
+ this.fsmSnapshotHistory.push({
28793
+ stateFrom: from,
28794
+ stateTo: fired.to,
28795
+ at: now,
28796
+ firedTo: fired.to,
28797
+ firedLabel: fired.label,
28798
+ reason: summarizeTransition(fired),
28799
+ transitions: ev.transitions
28800
+ });
28801
+ if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
28802
+ }
28775
28803
  /** Re-derive the visible modal + controls for the current state and emit a
28776
28804
  * state_changed if anything differs from the last emit. */
28777
28805
  emitStateChanged(forceEmit) {
@@ -29804,7 +29832,7 @@ import * as fs12 from "fs";
29804
29832
  function stripAnsi3(text) {
29805
29833
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
29806
29834
  }
29807
- var SpecCliAdapter = class {
29835
+ var SpecCliAdapter = class _SpecCliAdapter {
29808
29836
  cliType;
29809
29837
  cliName;
29810
29838
  workingDir;
@@ -29829,6 +29857,23 @@ var SpecCliAdapter = class {
29829
29857
  activeInteractivePrompt = null;
29830
29858
  interactivePromptTransport = null;
29831
29859
  claudeTuiPromptCaptureInFlight = false;
29860
+ /**
29861
+ * Wall clock of the first frame on which a held interactive prompt was
29862
+ * observed to have left the screen. Mirrors the approval FSM's
29863
+ * `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
29864
+ * repaints the choice picker as several PTY chunks, so a single frame
29865
+ * with no "Enter to select" footer is not proof the prompt is gone — it
29866
+ * may just be mid-repaint. We only clear the held prompt once it has
29867
+ * been absent across a short grace window. Reset to null the moment the
29868
+ * prompt footer reappears.
29869
+ *
29870
+ * Without this, a choice prompt resolved *directly in the terminal* (the
29871
+ * user picked an option without going through ADHDev's
29872
+ * setInteractivePromptResponse) was never cleared from
29873
+ * `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
29874
+ * forever — the choice-resolve-stuck bug.
29875
+ */
29876
+ interactivePromptLostAt = null;
29832
29877
  jsonLineTail = "";
29833
29878
  exited = false;
29834
29879
  spawned = false;
@@ -30089,6 +30134,11 @@ var SpecCliAdapter = class {
30089
30134
  // transition from the current state with its per-condition match
30090
30135
  // result + countdown — the canonical "why isn't it moving" answer.
30091
30136
  fsm: this.driver.getFsmDebug?.() ?? null,
30137
+ // v4 FSM transition snapshot history (null for v3 specs). The full
30138
+ // pre-transition evaluation table captured at each transition —
30139
+ // answers "why did this rule fire" after the fact, unlike the live
30140
+ // `fsm` field which only reflects the current instant.
30141
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
30092
30142
  // Extended fields
30093
30143
  name: this.cliName,
30094
30144
  status: this.getStatus().status,
@@ -30126,11 +30176,13 @@ var SpecCliAdapter = class {
30126
30176
  if (ev.state.title) {
30127
30177
  LOG.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
30128
30178
  }
30179
+ this.maybeClearResolvedClaudeTuiPrompt();
30129
30180
  this.maybeCaptureClaudeTuiPrompt();
30130
30181
  this.statusCallback?.();
30131
30182
  return;
30132
30183
  case "pty_data":
30133
30184
  this.detectInteractivePromptFromPtyChunk(ev.chunk);
30185
+ this.maybeClearResolvedClaudeTuiPrompt();
30134
30186
  this.maybeCaptureClaudeTuiPrompt();
30135
30187
  try {
30136
30188
  this.ptyDataCallback?.(ev.chunk);
@@ -30163,6 +30215,7 @@ var SpecCliAdapter = class {
30163
30215
  if (!prompt) continue;
30164
30216
  this.activeInteractivePrompt = prompt;
30165
30217
  this.interactivePromptTransport = "stream-json";
30218
+ this.interactivePromptLostAt = null;
30166
30219
  this.statusCallback?.();
30167
30220
  } catch {
30168
30221
  }
@@ -30216,6 +30269,47 @@ var SpecCliAdapter = class {
30216
30269
  }
30217
30270
  return messages;
30218
30271
  }
30272
+ /**
30273
+ * Grace window a held interactive prompt must be absent from the screen
30274
+ * before we treat it as resolved-in-terminal and clear it. claude-cli
30275
+ * repaints the picker across multiple PTY chunks, so a single
30276
+ * footer-less frame is not proof the prompt is gone. Sized in the same
30277
+ * spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
30278
+ */
30279
+ static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
30280
+ /**
30281
+ * Clear a held interactive prompt once the user has resolved it directly
30282
+ * in the terminal (the choice picker leaves the screen without going
30283
+ * through setInteractivePromptResponse). The approval path already does
30284
+ * this via the FSM's modal-lost hysteresis; the interactive-prompt path
30285
+ * had no equivalent, so a terminal-side answer left activeInteractivePrompt
30286
+ * set and getStatus() re-emitted the same choice modal forever.
30287
+ *
30288
+ * Detection mirrors capture: the claude TUI picker is on-screen exactly
30289
+ * while its "Enter to select" footer is rendered. When the footer is gone
30290
+ * for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
30291
+ */
30292
+ maybeClearResolvedClaudeTuiPrompt() {
30293
+ if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
30294
+ let screenText = "";
30295
+ try {
30296
+ screenText = this.driver.snapshot();
30297
+ } catch {
30298
+ return;
30299
+ }
30300
+ const stillOnScreen = screenText.includes("Enter to select");
30301
+ if (stillOnScreen) {
30302
+ this.interactivePromptLostAt = null;
30303
+ return;
30304
+ }
30305
+ const lostAt = this.interactivePromptLostAt ?? Date.now();
30306
+ if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
30307
+ if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
30308
+ this.activeInteractivePrompt = null;
30309
+ this.interactivePromptTransport = null;
30310
+ this.interactivePromptLostAt = null;
30311
+ this.statusCallback?.();
30312
+ }
30219
30313
  maybeCaptureClaudeTuiPrompt() {
30220
30314
  if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
30221
30315
  const screenText = this.driver.snapshot();
@@ -30229,6 +30323,7 @@ var SpecCliAdapter = class {
30229
30323
  if (!prompt) return;
30230
30324
  this.activeInteractivePrompt = prompt;
30231
30325
  this.interactivePromptTransport = "tui";
30326
+ this.interactivePromptLostAt = null;
30232
30327
  this.statusCallback?.();
30233
30328
  return;
30234
30329
  }
@@ -30265,6 +30360,7 @@ var SpecCliAdapter = class {
30265
30360
  if (!prompt) return;
30266
30361
  this.activeInteractivePrompt = prompt;
30267
30362
  this.interactivePromptTransport = "tui";
30363
+ this.interactivePromptLostAt = null;
30268
30364
  this.statusCallback?.();
30269
30365
  }
30270
30366
  getDebugState() {
@@ -30319,6 +30415,9 @@ var SpecCliAdapter = class {
30319
30415
  // and countdown. This is the canonical "why isn't it transitioning"
30320
30416
  // answer — no screenshots needed.
30321
30417
  fsm: this.driver.getFsmDebug?.() ?? null,
30418
+ // v4 FSM transition snapshot history — the captured pre-transition
30419
+ // evaluation table at each transition (null for v3 specs).
30420
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
30322
30421
  messages,
30323
30422
  committedMessages: messages
30324
30423
  };