@adhdev/daemon-standalone 0.9.82-rc.260 → 0.9.82-rc.262

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.js CHANGED
@@ -58358,8 +58358,12 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58358
58358
  this.rows = opts.rows ?? import_session_host_core6.DEFAULT_SESSION_HOST_ROWS;
58359
58359
  this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
58360
58360
  this.tickIntervalMs = opts.tickIntervalMs ?? 0;
58361
- const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS2(pty_transport_exports));
58362
- this.factory = opts.transportFactory ?? new NodePtyTransportFactory2();
58361
+ if (opts.transportFactory) {
58362
+ this.factory = opts.transportFactory;
58363
+ } else {
58364
+ const { NodePtyTransportFactory: NodePtyTransportFactory2 } = (init_pty_transport(), __toCommonJS2(pty_transport_exports));
58365
+ this.factory = new NodePtyTransportFactory2();
58366
+ }
58363
58367
  this.screen = new TerminalScreen(this.rows, this.cols);
58364
58368
  }
58365
58369
  rows;
@@ -58514,6 +58518,10 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58514
58518
  specWatcher = null;
58515
58519
  /** Last full FSM evaluation, kept for the debugger. */
58516
58520
  lastFsmEval = null;
58521
+ /** Ring buffer (max 20) of the full FSM evaluation captured at each
58522
+ * transition — the rich pre-transition table that lastFsmEval only keeps
58523
+ * for the single most recent evaluation. Separate from stateHistory. */
58524
+ fsmSnapshotHistory = [];
58517
58525
  subscribe(listener) {
58518
58526
  this.listeners.add(listener);
58519
58527
  return () => {
@@ -58604,6 +58612,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58604
58612
  getStateHistory() {
58605
58613
  return this.stateHistory;
58606
58614
  }
58615
+ getFsmSnapshotHistory() {
58616
+ return this.fsmSnapshotHistory;
58617
+ }
58607
58618
  getSections() {
58608
58619
  try {
58609
58620
  const screen = this.adapter.snapshot();
@@ -58699,7 +58710,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58699
58710
  this.lastFsmEval = ev;
58700
58711
  this.prevScreenLines = currentLines;
58701
58712
  if (ev.fired) {
58702
- this.commitTransition(ev.fired, now);
58713
+ this.commitTransition(ev.fired, now, ev);
58703
58714
  this.emitStateChanged(forceEmit);
58704
58715
  this.scheduleWakeForState();
58705
58716
  return;
@@ -58708,8 +58719,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58708
58719
  this.scheduleWakeForState();
58709
58720
  this.maybeMarkReady();
58710
58721
  }
58711
- commitTransition(fired, now) {
58722
+ commitTransition(fired, now, ev) {
58712
58723
  const from = this.currentStateId;
58724
+ this.pushFsmSnapshot(from, fired, now, ev);
58713
58725
  this.currentStateId = fired.to;
58714
58726
  this.stateEnteredAt = now;
58715
58727
  this.regionLastChangedAt.clear();
@@ -58720,6 +58732,22 @@ ${formatManifestValidationIssues2(validation.issues)}`,
58720
58732
  });
58721
58733
  LOG2.info("FsmDriver", `[${this.specTag()}] ${from} \u2192 ${fired.to} (${fired.label})`);
58722
58734
  }
58735
+ /** Snapshot the full FSM evaluation that produced a transition into the
58736
+ * separate fsmSnapshotHistory ring buffer (max 20). The transitions[]
58737
+ * table is captured by reference — it is freshly built per evaluation in
58738
+ * evaluateFsm and never mutated after, so no clone is needed. */
58739
+ pushFsmSnapshot(from, fired, now, ev) {
58740
+ this.fsmSnapshotHistory.push({
58741
+ stateFrom: from,
58742
+ stateTo: fired.to,
58743
+ at: now,
58744
+ firedTo: fired.to,
58745
+ firedLabel: fired.label,
58746
+ reason: summarizeTransition(fired),
58747
+ transitions: ev.transitions
58748
+ });
58749
+ if (this.fsmSnapshotHistory.length > 20) this.fsmSnapshotHistory.shift();
58750
+ }
58723
58751
  /** Re-derive the visible modal + controls for the current state and emit a
58724
58752
  * state_changed if anything differs from the last emit. */
58725
58753
  emitStateChanged(forceEmit) {
@@ -59748,7 +59776,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
59748
59776
  function stripAnsi3(text) {
59749
59777
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
59750
59778
  }
59751
- var SpecCliAdapter = class {
59779
+ var SpecCliAdapter = class _SpecCliAdapter {
59752
59780
  cliType;
59753
59781
  cliName;
59754
59782
  workingDir;
@@ -59773,6 +59801,23 @@ ${formatManifestValidationIssues2(validation.issues)}`,
59773
59801
  activeInteractivePrompt = null;
59774
59802
  interactivePromptTransport = null;
59775
59803
  claudeTuiPromptCaptureInFlight = false;
59804
+ /**
59805
+ * Wall clock of the first frame on which a held interactive prompt was
59806
+ * observed to have left the screen. Mirrors the approval FSM's
59807
+ * `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
59808
+ * repaints the choice picker as several PTY chunks, so a single frame
59809
+ * with no "Enter to select" footer is not proof the prompt is gone — it
59810
+ * may just be mid-repaint. We only clear the held prompt once it has
59811
+ * been absent across a short grace window. Reset to null the moment the
59812
+ * prompt footer reappears.
59813
+ *
59814
+ * Without this, a choice prompt resolved *directly in the terminal* (the
59815
+ * user picked an option without going through ADHDev's
59816
+ * setInteractivePromptResponse) was never cleared from
59817
+ * `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
59818
+ * forever — the choice-resolve-stuck bug.
59819
+ */
59820
+ interactivePromptLostAt = null;
59776
59821
  jsonLineTail = "";
59777
59822
  exited = false;
59778
59823
  spawned = false;
@@ -60033,6 +60078,11 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60033
60078
  // transition from the current state with its per-condition match
60034
60079
  // result + countdown — the canonical "why isn't it moving" answer.
60035
60080
  fsm: this.driver.getFsmDebug?.() ?? null,
60081
+ // v4 FSM transition snapshot history (null for v3 specs). The full
60082
+ // pre-transition evaluation table captured at each transition —
60083
+ // answers "why did this rule fire" after the fact, unlike the live
60084
+ // `fsm` field which only reflects the current instant.
60085
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
60036
60086
  // Extended fields
60037
60087
  name: this.cliName,
60038
60088
  status: this.getStatus().status,
@@ -60070,11 +60120,13 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60070
60120
  if (ev.state.title) {
60071
60121
  LOG2.debug("SpecAdapter", `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
60072
60122
  }
60123
+ this.maybeClearResolvedClaudeTuiPrompt();
60073
60124
  this.maybeCaptureClaudeTuiPrompt();
60074
60125
  this.statusCallback?.();
60075
60126
  return;
60076
60127
  case "pty_data":
60077
60128
  this.detectInteractivePromptFromPtyChunk(ev.chunk);
60129
+ this.maybeClearResolvedClaudeTuiPrompt();
60078
60130
  this.maybeCaptureClaudeTuiPrompt();
60079
60131
  try {
60080
60132
  this.ptyDataCallback?.(ev.chunk);
@@ -60107,6 +60159,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60107
60159
  if (!prompt) continue;
60108
60160
  this.activeInteractivePrompt = prompt;
60109
60161
  this.interactivePromptTransport = "stream-json";
60162
+ this.interactivePromptLostAt = null;
60110
60163
  this.statusCallback?.();
60111
60164
  } catch {
60112
60165
  }
@@ -60160,6 +60213,47 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60160
60213
  }
60161
60214
  return messages;
60162
60215
  }
60216
+ /**
60217
+ * Grace window a held interactive prompt must be absent from the screen
60218
+ * before we treat it as resolved-in-terminal and clear it. claude-cli
60219
+ * repaints the picker across multiple PTY chunks, so a single
60220
+ * footer-less frame is not proof the prompt is gone. Sized in the same
60221
+ * spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
60222
+ */
60223
+ static INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
60224
+ /**
60225
+ * Clear a held interactive prompt once the user has resolved it directly
60226
+ * in the terminal (the choice picker leaves the screen without going
60227
+ * through setInteractivePromptResponse). The approval path already does
60228
+ * this via the FSM's modal-lost hysteresis; the interactive-prompt path
60229
+ * had no equivalent, so a terminal-side answer left activeInteractivePrompt
60230
+ * set and getStatus() re-emitted the same choice modal forever.
60231
+ *
60232
+ * Detection mirrors capture: the claude TUI picker is on-screen exactly
60233
+ * while its "Enter to select" footer is rendered. When the footer is gone
60234
+ * for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
60235
+ */
60236
+ maybeClearResolvedClaudeTuiPrompt() {
60237
+ if (this.cliType !== "claude-cli" || !this.activeInteractivePrompt) return;
60238
+ let screenText = "";
60239
+ try {
60240
+ screenText = this.driver.snapshot();
60241
+ } catch {
60242
+ return;
60243
+ }
60244
+ const stillOnScreen = screenText.includes("Enter to select");
60245
+ if (stillOnScreen) {
60246
+ this.interactivePromptLostAt = null;
60247
+ return;
60248
+ }
60249
+ const lostAt = this.interactivePromptLostAt ?? Date.now();
60250
+ if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
60251
+ if (Date.now() - lostAt < _SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
60252
+ this.activeInteractivePrompt = null;
60253
+ this.interactivePromptTransport = null;
60254
+ this.interactivePromptLostAt = null;
60255
+ this.statusCallback?.();
60256
+ }
60163
60257
  maybeCaptureClaudeTuiPrompt() {
60164
60258
  if (this.cliType !== "claude-cli" || this.activeInteractivePrompt || this.claudeTuiPromptCaptureInFlight) return;
60165
60259
  const screenText = this.driver.snapshot();
@@ -60173,6 +60267,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60173
60267
  if (!prompt) return;
60174
60268
  this.activeInteractivePrompt = prompt;
60175
60269
  this.interactivePromptTransport = "tui";
60270
+ this.interactivePromptLostAt = null;
60176
60271
  this.statusCallback?.();
60177
60272
  return;
60178
60273
  }
@@ -60209,6 +60304,7 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60209
60304
  if (!prompt) return;
60210
60305
  this.activeInteractivePrompt = prompt;
60211
60306
  this.interactivePromptTransport = "tui";
60307
+ this.interactivePromptLostAt = null;
60212
60308
  this.statusCallback?.();
60213
60309
  }
60214
60310
  getDebugState() {
@@ -60263,6 +60359,9 @@ ${formatManifestValidationIssues2(validation.issues)}`,
60263
60359
  // and countdown. This is the canonical "why isn't it transitioning"
60264
60360
  // answer — no screenshots needed.
60265
60361
  fsm: this.driver.getFsmDebug?.() ?? null,
60362
+ // v4 FSM transition snapshot history — the captured pre-transition
60363
+ // evaluation table at each transition (null for v3 specs).
60364
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
60266
60365
  messages,
60267
60366
  committedMessages: messages
60268
60367
  };
@@ -70198,8 +70297,37 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
70198
70297
  maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
70199
70298
  });
70200
70299
  const mergeBase = git(["merge-base", baseHead, branchHead]).trim();
70201
- const mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
70202
- const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
70300
+ let mergedTree = "";
70301
+ let mergeTreeStdout = "";
70302
+ let gitlinkTrivialFastForward;
70303
+ try {
70304
+ mergeTreeStdout = git(["merge-tree", "--write-tree", baseHead, branchHead]);
70305
+ mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || "";
70306
+ } catch (mergeTreeErr) {
70307
+ const output = `${mergeTreeErr?.message || ""}
70308
+ ${mergeTreeErr?.stdout || ""}
70309
+ ${mergeTreeErr?.stderr || ""}`;
70310
+ const isSubmoduleConflict = /(submodule|160000)/i.test(output) || /Recursive merging with submodules/i.test(output);
70311
+ if (!isSubmoduleConflict) throw mergeTreeErr;
70312
+ const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
70313
+ if (!evaluation.trivial) {
70314
+ return {
70315
+ status: "failed",
70316
+ equivalent: false,
70317
+ baseHead,
70318
+ branchHead,
70319
+ mergeBase: mergeBase || void 0,
70320
+ durationMs: Date.now() - startedAt,
70321
+ error: mergeTreeErr?.message || String(mergeTreeErr),
70322
+ stdout: truncateValidationOutput(mergeTreeErr?.stdout),
70323
+ stderr: truncateValidationOutput(mergeTreeErr?.stderr),
70324
+ gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
70325
+ actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output)
70326
+ };
70327
+ }
70328
+ mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || "";
70329
+ gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
70330
+ }
70203
70331
  if (!mergeBase || !mergedTree) {
70204
70332
  return {
70205
70333
  status: "failed",
@@ -70210,7 +70338,8 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
70210
70338
  mergedTree: mergedTree || void 0,
70211
70339
  durationMs: Date.now() - startedAt,
70212
70340
  error: "patch equivalence preflight could not resolve merge-base or synthetic merge tree",
70213
- stdout: truncateValidationOutput(mergeTreeStdout)
70341
+ stdout: truncateValidationOutput(mergeTreeStdout),
70342
+ gitlinkTrivialFastForward
70214
70343
  };
70215
70344
  }
70216
70345
  const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
@@ -70225,7 +70354,8 @@ ${formatManifestValidationIssues2(validation2.issues)}`);
70225
70354
  mergedTree,
70226
70355
  expectedPatchId,
70227
70356
  actualPatchId,
70228
- durationMs: Date.now() - startedAt
70357
+ durationMs: Date.now() - startedAt,
70358
+ gitlinkTrivialFastForward
70229
70359
  };
70230
70360
  } catch (e) {
70231
70361
  return {
@@ -70248,6 +70378,65 @@ ${e?.stderr || ""}`
70248
70378
  };
70249
70379
  }
70250
70380
  }
70381
+ async function runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead) {
70382
+ const startedAt = Date.now();
70383
+ try {
70384
+ const { execFileSync: execFileSync6 } = await import("child_process");
70385
+ const git = (args, opts) => execFileSync6("git", args, {
70386
+ cwd: opts?.cwd || repoRoot,
70387
+ encoding: "utf8",
70388
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
70389
+ });
70390
+ const rawDiff = git(["diff", "--raw", baseHead, branchHead]).trim();
70391
+ if (rawDiff) {
70392
+ const changedPaths = rawDiff.split("\n").map((line) => line.split(" ").slice(1).join(" ").trim()).filter(Boolean).slice(0, 50);
70393
+ return {
70394
+ status: "passed",
70395
+ hasEffectiveDiff: true,
70396
+ baseHead,
70397
+ branchHead,
70398
+ changedPaths,
70399
+ durationMs: Date.now() - startedAt
70400
+ };
70401
+ }
70402
+ const submoduleHints = [];
70403
+ try {
70404
+ const status = git(["submodule", "status"]);
70405
+ for (const line of status.split("\n")) {
70406
+ const trimmed = line.trimEnd();
70407
+ if (!trimmed) continue;
70408
+ if (trimmed.startsWith("+")) {
70409
+ const parts = trimmed.slice(1).trim().split(/\s+/);
70410
+ const path39 = parts[1] || parts[0] || "(unknown)";
70411
+ submoduleHints.push({
70412
+ path: path39,
70413
+ reason: "submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)"
70414
+ });
70415
+ }
70416
+ }
70417
+ } catch {
70418
+ }
70419
+ return {
70420
+ status: "failed",
70421
+ hasEffectiveDiff: false,
70422
+ baseHead,
70423
+ branchHead,
70424
+ ...submoduleHints.length ? { submoduleHints } : {},
70425
+ durationMs: Date.now() - startedAt
70426
+ };
70427
+ } catch (e) {
70428
+ return {
70429
+ status: "skipped",
70430
+ hasEffectiveDiff: true,
70431
+ baseHead,
70432
+ branchHead,
70433
+ durationMs: Date.now() - startedAt,
70434
+ error: e?.message || String(e),
70435
+ stdout: truncateValidationOutput(e?.stdout),
70436
+ stderr: truncateValidationOutput(e?.stderr)
70437
+ };
70438
+ }
70439
+ }
70251
70440
  function buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output) {
70252
70441
  if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return void 0;
70253
70442
  const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => ({
@@ -70304,6 +70493,135 @@ ${e?.stderr || ""}`
70304
70493
  return void 0;
70305
70494
  }
70306
70495
  }
70496
+ function resolveGitDir(repoRoot) {
70497
+ const out = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "--absolute-git-dir"], {
70498
+ cwd: repoRoot,
70499
+ encoding: "utf8",
70500
+ maxBuffer: 1024 * 1024
70501
+ }).trim();
70502
+ return out;
70503
+ }
70504
+ function isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit) {
70505
+ if (!baseCommit || !branchCommit) return false;
70506
+ if (baseCommit === branchCommit) return true;
70507
+ try {
70508
+ if (!fs23.existsSync(submoduleRepoPath)) return false;
70509
+ (0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
70510
+ (0, import_node_child_process6.execFileSync)("git", ["cat-file", "-e", `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: "ignore" });
70511
+ (0, import_node_child_process6.execFileSync)("git", ["merge-base", "--is-ancestor", baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: "ignore" });
70512
+ return true;
70513
+ } catch {
70514
+ return false;
70515
+ }
70516
+ }
70517
+ function readChangedPathKinds(repoRoot, fromRef, toRef) {
70518
+ try {
70519
+ const output = (0, import_node_child_process6.execFileSync)("git", ["diff", "--raw", "--no-abbrev", fromRef, toRef], {
70520
+ cwd: repoRoot,
70521
+ encoding: "utf8",
70522
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES
70523
+ });
70524
+ const result = [];
70525
+ const seen = /* @__PURE__ */ new Set();
70526
+ for (const line of output.split("\n")) {
70527
+ if (!line.trim()) continue;
70528
+ const metaAndPath = line.split(" ");
70529
+ const meta3 = metaAndPath[0] || "";
70530
+ const path39 = metaAndPath[metaAndPath.length - 1]?.trim();
70531
+ if (!path39 || seen.has(path39)) continue;
70532
+ seen.add(path39);
70533
+ const parts = meta3.split(/\s+/);
70534
+ const isGitlink = !!(parts[0]?.includes("160000") || parts[1]?.includes("160000"));
70535
+ result.push({ path: path39, isGitlink });
70536
+ }
70537
+ return result;
70538
+ } catch {
70539
+ return [];
70540
+ }
70541
+ }
70542
+ function evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead) {
70543
+ const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map((path39) => {
70544
+ const baseCommit = readTreeObject(repoRoot, baseHead, path39);
70545
+ const branchCommit = readTreeObject(repoRoot, branchHead, path39);
70546
+ const submoduleRepoPath = (0, import_path10.resolve)(repoRoot, path39);
70547
+ const fastForward = !!baseCommit && !!branchCommit && isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
70548
+ return { path: path39, baseCommit, branchCommit, fastForward };
70549
+ });
70550
+ if (changedGitlinks.length === 0) {
70551
+ return { trivial: false, reason: "no_changed_gitlinks", gitlinks: changedGitlinks };
70552
+ }
70553
+ const nonFastForward = changedGitlinks.filter((entry) => !entry.fastForward);
70554
+ if (nonFastForward.length > 0) {
70555
+ return {
70556
+ trivial: false,
70557
+ reason: `diverged_gitlinks:${nonFastForward.map((entry) => entry.path).join(",")}`,
70558
+ gitlinks: changedGitlinks
70559
+ };
70560
+ }
70561
+ let mergeBase = "";
70562
+ try {
70563
+ mergeBase = (0, import_node_child_process6.execFileSync)("git", ["merge-base", baseHead, branchHead], {
70564
+ cwd: repoRoot,
70565
+ encoding: "utf8",
70566
+ maxBuffer: 1024 * 1024
70567
+ }).trim();
70568
+ } catch {
70569
+ return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
70570
+ }
70571
+ if (!mergeBase) {
70572
+ return { trivial: false, reason: "merge_base_unresolved", gitlinks: changedGitlinks };
70573
+ }
70574
+ const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
70575
+ const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
70576
+ const baseChangedPaths = new Map(baseSideChanges.map((entry) => [entry.path, entry]));
70577
+ const overlapping = branchSideChanges.filter((entry) => baseChangedPaths.has(entry.path));
70578
+ const nonGitlinkOverlap = overlapping.filter((entry) => {
70579
+ const baseEntry = baseChangedPaths.get(entry.path);
70580
+ return !(entry.isGitlink && baseEntry?.isGitlink);
70581
+ });
70582
+ if (nonGitlinkOverlap.length > 0) {
70583
+ return {
70584
+ trivial: false,
70585
+ reason: `non_gitlink_overlap:${nonGitlinkOverlap.map((entry) => entry.path).join(",")}`,
70586
+ gitlinks: changedGitlinks
70587
+ };
70588
+ }
70589
+ return { trivial: true, gitlinks: changedGitlinks };
70590
+ }
70591
+ function synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, gitlinks) {
70592
+ try {
70593
+ const baseTree = (0, import_node_child_process6.execFileSync)("git", ["rev-parse", `${baseHead}^{tree}`], {
70594
+ cwd: repoRoot,
70595
+ encoding: "utf8",
70596
+ maxBuffer: 1024 * 1024
70597
+ }).trim();
70598
+ if (!baseTree) return void 0;
70599
+ const updates = gitlinks.filter((entry) => entry.branchCommit).map((entry) => `160000 commit ${entry.branchCommit} ${entry.path}`).join("\n");
70600
+ if (!updates) return baseTree;
70601
+ const tmpIndex = (0, import_path10.join)(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
70602
+ const env2 = { ...process.env, GIT_INDEX_FILE: tmpIndex };
70603
+ try {
70604
+ (0, import_node_child_process6.execFileSync)("git", ["read-tree", baseTree], { cwd: repoRoot, env: env2, stdio: "ignore" });
70605
+ (0, import_node_child_process6.execFileSync)("git", ["update-index", "--index-info"], {
70606
+ cwd: repoRoot,
70607
+ env: env2,
70608
+ input: `${updates}
70609
+ `,
70610
+ encoding: "utf8",
70611
+ stdio: ["pipe", "ignore", "ignore"]
70612
+ });
70613
+ const newTree = (0, import_node_child_process6.execFileSync)("git", ["write-tree"], { cwd: repoRoot, env: env2, encoding: "utf8" }).trim();
70614
+ return newTree || void 0;
70615
+ } finally {
70616
+ try {
70617
+ fs23.rmSync(tmpIndex, { force: true });
70618
+ } catch {
70619
+ }
70620
+ }
70621
+ } catch {
70622
+ return void 0;
70623
+ }
70624
+ }
70307
70625
  async function alignRefinerySubmodulesAfterMerge(repoRoot, previousBaseHead, currentHead, options = {}) {
70308
70626
  const startedAt = Date.now();
70309
70627
  const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead).filter((path39) => !(options.submoduleIgnorePaths || []).includes(path39));
@@ -70971,6 +71289,10 @@ ${e?.stderr || ""}`
70971
71289
  runningRefineJobs = /* @__PURE__ */ new Map();
70972
71290
  /** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
70973
71291
  terminalRefineJobs = /* @__PURE__ */ new Map();
71292
+ /** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
71293
+ runningRefineBatchJobs = /* @__PURE__ */ new Map();
71294
+ /** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
71295
+ terminalRefineBatchJobs = /* @__PURE__ */ new Map();
70974
71296
  constructor(deps) {
70975
71297
  this.deps = deps;
70976
71298
  }
@@ -72200,6 +72522,48 @@ ${tail}` : ""
72200
72522
  }
72201
72523
  };
72202
72524
  }
72525
+ const effectiveDiffStarted = Date.now();
72526
+ const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
72527
+ recordMeshRefineStage(refineStages, "effective_diff", effectiveDiff.status, effectiveDiffStarted, {
72528
+ hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
72529
+ changedPaths: effectiveDiff.changedPaths,
72530
+ submoduleHints: effectiveDiff.submoduleHints,
72531
+ ...effectiveDiff.error ? { error: effectiveDiff.error } : {}
72532
+ });
72533
+ if (effectiveDiff.status === "failed" && !effectiveDiff.hasEffectiveDiff) {
72534
+ const hintLines = (effectiveDiff.submoduleHints || []).map((h) => ` - ${h.path}: ${h.reason}`);
72535
+ const message = [
72536
+ `Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
72537
+ "This usually means a submodule (e.g. oss) has commits but the root branch never committed the gitlink (pointer) bump, so the merge would be a silent no-op while the real change never reaches main.",
72538
+ hintLines.length ? `Submodules with uncommitted pointer bumps:
72539
+ ${hintLines.join("\n")}` : "",
72540
+ `Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`
72541
+ ].filter(Boolean).join("\n");
72542
+ return {
72543
+ success: false,
72544
+ code: "no_effective_diff",
72545
+ convergenceStatus: "blocked_review",
72546
+ error: message,
72547
+ branch,
72548
+ into: baseBranch,
72549
+ validationSummary,
72550
+ patchEquivalence,
72551
+ effectiveDiff,
72552
+ refineStages,
72553
+ finalBranchConvergenceState: {
72554
+ branch,
72555
+ baseBranch,
72556
+ merged: false,
72557
+ removed: false,
72558
+ validation: "passed",
72559
+ patchEquivalence: "passed",
72560
+ effectiveDiff: "no_effective_diff",
72561
+ status: "blocked_review",
72562
+ reason: "no_effective_diff",
72563
+ ...effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}
72564
+ }
72565
+ };
72566
+ }
72203
72567
  let mergeResult;
72204
72568
  const mergeStarted = Date.now();
72205
72569
  try {
@@ -72541,6 +72905,17 @@ ${tail}` : ""
72541
72905
  note: "Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order."
72542
72906
  };
72543
72907
  }
72908
+ return this.runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args);
72909
+ }
72910
+ /**
72911
+ * Convergence core shared by the synchronous batch entry and the async batch job.
72912
+ * Refines each node in order: the per-node refine pipeline fetches origin/<base>
72913
+ * fresh, so each merged sibling advances the base before the next node's auto-rebase
72914
+ * + patch-equivalence re-check. A blocked/failed node is isolated; the batch
72915
+ * continues with the remaining nodes. Does NOT touch the per-node merge logic — it
72916
+ * only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
72917
+ */
72918
+ async runMeshRefineBatchConvergence(meshId, orderedNodes, ordering, args) {
72544
72919
  const results = [];
72545
72920
  for (const node of orderedNodes) {
72546
72921
  let result;
@@ -72595,6 +72970,204 @@ ${tail}` : ""
72595
72970
  }
72596
72971
  };
72597
72972
  }
72973
+ buildRefineBatchJobKey(meshId) {
72974
+ return `${meshId}::batch`;
72975
+ }
72976
+ buildRefineBatchJobHandle(args) {
72977
+ return {
72978
+ success: true,
72979
+ async: true,
72980
+ batch: true,
72981
+ status: args.status || "accepted",
72982
+ jobId: args.jobId || `refine_batch_${createInteractionId()}`,
72983
+ interactionId: args.interactionId || createInteractionId(),
72984
+ meshId: args.meshId,
72985
+ batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? "" : "s"}`,
72986
+ nodeIds: args.nodeIds,
72987
+ nodeCount: args.nodeIds.length,
72988
+ order: args.order,
72989
+ startedAt: args.startedAt || (/* @__PURE__ */ new Date()).toISOString(),
72990
+ ...args.completedAt ? { completedAt: args.completedAt } : {},
72991
+ ...args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {},
72992
+ eventDelivery: { pendingEvents: true, ledger: true },
72993
+ evidence: {
72994
+ pendingEventsCommand: "get_pending_mesh_events",
72995
+ ledgerCommand: "get_mesh_ledger_slice",
72996
+ taskHistoryKind: args.status === "completed" ? "task_completed" : args.status === "failed" ? "task_failed" : "task_dispatched"
72997
+ }
72998
+ };
72999
+ }
73000
+ /**
73001
+ * Emit a batch Refinery terminal/accepted event through the SAME pending-event +
73002
+ * forward mechanism single-node refine uses (queueRefineJobEvent), so the
73003
+ * coordinator's existing refine:accepted/completed/failed handling and message
73004
+ * renderer apply unchanged. The aggregate per-node results ride along in `result`.
73005
+ */
73006
+ queueRefineBatchJobEvent(event, handle, result) {
73007
+ const metadataEvent = {
73008
+ source: "refine_mesh_node_async_job",
73009
+ batch: true,
73010
+ jobId: handle.jobId,
73011
+ interactionId: handle.interactionId,
73012
+ meshId: handle.meshId,
73013
+ nodeId: handle.batchLabel,
73014
+ nodeIds: handle.nodeIds,
73015
+ workspace: void 0,
73016
+ status: handle.status,
73017
+ startedAt: handle.startedAt,
73018
+ completedAt: handle.completedAt,
73019
+ order: handle.order,
73020
+ ...result ? { result } : {}
73021
+ };
73022
+ const eventPayload = {
73023
+ event,
73024
+ meshId: handle.meshId,
73025
+ nodeLabel: handle.batchLabel,
73026
+ nodeId: handle.batchLabel,
73027
+ metadataEvent,
73028
+ queuedAt: Date.now(),
73029
+ ...handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}
73030
+ };
73031
+ if (typeof this.deps.instanceManager?.getByCategory === "function") {
73032
+ const forwarded = handleMeshForwardEvent(
73033
+ { instanceManager: this.deps.instanceManager },
73034
+ {
73035
+ event,
73036
+ meshId: handle.meshId,
73037
+ nodeId: handle.batchLabel,
73038
+ jobId: handle.jobId,
73039
+ interactionId: handle.interactionId,
73040
+ status: handle.status,
73041
+ startedAt: handle.startedAt,
73042
+ completedAt: handle.completedAt,
73043
+ ...result ? { result } : {}
73044
+ }
73045
+ );
73046
+ if (forwarded?.success === true) return;
73047
+ LOG2.warn("Mesh", `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || "unknown error"}`);
73048
+ }
73049
+ queuePendingMeshCoordinatorEvent(eventPayload);
73050
+ }
73051
+ async appendRefineBatchJobLedger(kind, handle, result) {
73052
+ try {
73053
+ const { appendLedgerEntry: appendLedgerEntry2 } = await Promise.resolve().then(() => (init_mesh_ledger(), mesh_ledger_exports));
73054
+ appendLedgerEntry2(handle.meshId, {
73055
+ kind,
73056
+ nodeId: handle.batchLabel,
73057
+ payload: {
73058
+ source: "refine_mesh_node_async_job",
73059
+ refineJob: {
73060
+ batch: true,
73061
+ jobId: handle.jobId,
73062
+ interactionId: handle.interactionId,
73063
+ status: handle.status,
73064
+ meshId: handle.meshId,
73065
+ nodeIds: handle.nodeIds,
73066
+ order: handle.order,
73067
+ targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
73068
+ startedAt: handle.startedAt,
73069
+ completedAt: handle.completedAt
73070
+ },
73071
+ async: true,
73072
+ batch: true,
73073
+ ...result ? {
73074
+ success: result.success === true,
73075
+ result
73076
+ } : {}
73077
+ }
73078
+ });
73079
+ } catch (e) {
73080
+ LOG2.warn("Mesh", `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
73081
+ }
73082
+ }
73083
+ async finishMeshRefineBatchJob(handle, orderedNodes, ordering, args) {
73084
+ const key = this.buildRefineBatchJobKey(handle.meshId);
73085
+ let result;
73086
+ try {
73087
+ result = await this.runMeshRefineBatchConvergence(handle.meshId, orderedNodes, ordering, args);
73088
+ } catch (e) {
73089
+ result = { success: false, error: e?.message || String(e), batch: true };
73090
+ }
73091
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
73092
+ const summary = result.summary && typeof result.summary === "object" ? result.summary : void 0;
73093
+ const allConverged = result.allConverged === true;
73094
+ const isTerminalSuccess = result.success === true && allConverged;
73095
+ const nextStep = typeof result.nextStep === "string" && result.nextStep ? result.nextStep : isTerminalSuccess ? "All batched nodes converged onto base. Continue from the updated mesh state." : "Resolve blocked_review / not_mergeable nodes (see per-node code/stage/error in result.results), then re-run mesh_refine_batch for the remaining nodes.";
73096
+ const normalizedResult = {
73097
+ ...result,
73098
+ batch: true,
73099
+ nextStep,
73100
+ ...summary ? {
73101
+ convergenceStatus: allConverged ? "all_converged" : "partial"
73102
+ } : {}
73103
+ };
73104
+ const terminalHandle = this.buildRefineBatchJobHandle({
73105
+ meshId: handle.meshId,
73106
+ nodeIds: handle.nodeIds,
73107
+ order: handle.order,
73108
+ status: isTerminalSuccess ? "completed" : "failed",
73109
+ startedAt: handle.startedAt,
73110
+ completedAt,
73111
+ jobId: handle.jobId,
73112
+ interactionId: handle.interactionId,
73113
+ coordinatorDaemonId: handle.targetCoordinatorDaemonId
73114
+ });
73115
+ const terminal = { ...terminalHandle, result: normalizedResult };
73116
+ this.terminalRefineBatchJobs.set(key, terminal);
73117
+ this.runningRefineBatchJobs.delete(key);
73118
+ this.invalidateAggregateMeshStatus(handle.meshId);
73119
+ await this.appendRefineBatchJobLedger(isTerminalSuccess ? "task_completed" : "task_failed", terminalHandle, normalizedResult);
73120
+ this.queueRefineBatchJobEvent(isTerminalSuccess ? "refine:completed" : "refine:failed", terminalHandle, normalizedResult);
73121
+ }
73122
+ /**
73123
+ * Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
73124
+ * resolves the plan synchronously (so target/ordering errors and the dry-run shape
73125
+ * stay synchronous), then for execute=true registers an in-flight batch job, returns
73126
+ * {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
73127
+ * convergence loop in the background — emitting the same terminal refine event.
73128
+ * Idempotent: a batch already in flight for this mesh returns the running handle
73129
+ * with duplicate:true rather than spawning a second background job.
73130
+ */
73131
+ async startMeshRefineBatchJob(meshId, requestedNodeIds, args) {
73132
+ const plan = await this.batchRefineMeshNodes(meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
73133
+ const planRecord = plan;
73134
+ if (planRecord.success !== true) return plan;
73135
+ if (args?.dryRun === true && args?.execute !== true) return plan;
73136
+ const order = Array.isArray(planRecord.order) ? planRecord.order.filter((v) => typeof v === "string") : [];
73137
+ const nodeIds = order.slice();
73138
+ if (nodeIds.length === 0) {
73139
+ return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
73140
+ }
73141
+ const key = this.buildRefineBatchJobKey(meshId);
73142
+ const running = this.runningRefineBatchJobs.get(key);
73143
+ if (running) return { ...running, duplicate: true };
73144
+ const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
73145
+ const mesh = meshRecord?.mesh;
73146
+ const allNodes = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
73147
+ const orderedNodes = nodeIds.map((id) => allNodes.find((n) => n.id === id || n.nodeId === id)).filter((n) => !!n);
73148
+ if (orderedNodes.length === 0) {
73149
+ return { success: false, error: "Batch nodes no longer resolvable in mesh", batch: true };
73150
+ }
73151
+ const ordering = {
73152
+ order,
73153
+ rationale: planRecord.orderingRationale
73154
+ };
73155
+ const coordinatorDaemonId = typeof args?.coordinatorDaemonId === "string" && args.coordinatorDaemonId.trim() ? args.coordinatorDaemonId.trim() : this.deps.statusInstanceId || void 0;
73156
+ const handle = this.buildRefineBatchJobHandle({ meshId, nodeIds, order, coordinatorDaemonId });
73157
+ this.runningRefineBatchJobs.set(key, handle);
73158
+ await this.appendRefineBatchJobLedger("task_dispatched", handle);
73159
+ this.queueRefineBatchJobEvent("refine:accepted", handle);
73160
+ setImmediate(() => {
73161
+ void this.finishMeshRefineBatchJob(handle, orderedNodes, ordering, args);
73162
+ });
73163
+ return {
73164
+ ...handle,
73165
+ order,
73166
+ orderingRationale: planRecord.orderingRationale,
73167
+ plan: planRecord.plan,
73168
+ note: "Batch convergence accepted and running in the background. Completion/failure (with per-node results) will be delivered as a terminal refine event; do not poll repeatedly."
73169
+ };
73170
+ }
72598
73171
  async finishMeshRefineJob(handle, args) {
72599
73172
  const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
72600
73173
  let result;
@@ -74142,7 +74715,9 @@ ${tail}` : ""
74142
74715
  const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";
74143
74716
  if (!meshId) return { success: false, error: "meshId required" };
74144
74717
  const requestedNodeIds = Array.isArray(args?.nodeIds) ? args.nodeIds.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim()) : void 0;
74145
- return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
74718
+ const isDryRun = args?.dryRun !== false && args?.execute !== true;
74719
+ if (isDryRun) return this.batchRefineMeshNodes(meshId, requestedNodeIds, args);
74720
+ return this.startMeshRefineBatchJob(meshId, requestedNodeIds, args);
74146
74721
  }
74147
74722
  case "remove_mesh_node": {
74148
74723
  const meshId = typeof args?.meshId === "string" ? args.meshId.trim() : "";