@adhdev/daemon-core 0.9.82-rc.553 → 0.9.82-rc.554

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
@@ -423,10 +423,10 @@ function readInjected(value) {
423
423
  }
424
424
  function getDaemonBuildInfo() {
425
425
  if (cached) return cached;
426
- const commit = readInjected(true ? "3fc4f272513b6f628ec1732ef57d9ed94e08b8ff" : void 0) ?? "unknown";
427
- const commitShort = readInjected(true ? "3fc4f272" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
428
- const version = readInjected(true ? "0.9.82-rc.553" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
429
- const builtAt = readInjected(true ? "2026-07-17T06:04:22.090Z" : void 0);
426
+ const commit = readInjected(true ? "23529650799df69f9a14f16f6c2b34db153f1b27" : void 0) ?? "unknown";
427
+ const commitShort = readInjected(true ? "23529650" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
428
+ const version = readInjected(true ? "0.9.82-rc.554" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
429
+ const builtAt = readInjected(true ? "2026-07-17T07:57:39.144Z" : void 0);
430
430
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
431
431
  return cached;
432
432
  }
@@ -43289,6 +43289,12 @@ var TerminalAdapter = class {
43289
43289
  const pos = this.screen.getCursorPosition();
43290
43290
  return { row: pos.row, col: pos.col };
43291
43291
  }
43292
+ /** Current terminal geometry (columns × rows). Tracked here rather than
43293
+ * read off the screen buffer so a resize is reflected immediately, before
43294
+ * the next repaint. Consumed by the mesh_read_terminal viewport read. */
43295
+ getScreenSize() {
43296
+ return { cols: this.cols, rows: this.rows };
43297
+ }
43292
43298
  send_keys(text) {
43293
43299
  this.recordEvent("input", capPreview(escapeControl(text)), text.length);
43294
43300
  this.pty?.write(text);
@@ -43627,6 +43633,9 @@ var FsmDriver = class {
43627
43633
  getScreen() {
43628
43634
  return this.adapter.snapshot();
43629
43635
  }
43636
+ getScreenSize() {
43637
+ return this.adapter.getScreenSize();
43638
+ }
43630
43639
  /** Scrollback-inclusive screen as line array — used only for modal/button
43631
43640
  * content extraction so a tall prompt's off-screen anchors stay matchable.
43632
43641
  * Falls back to the viewport snapshot if scrollback read is unavailable. */
@@ -45664,8 +45673,10 @@ function readTailJsonlLines(filePath, maxBytes) {
45664
45673
  }
45665
45674
 
45666
45675
  // src/providers/spec/cli-adapter.ts
45676
+ init_provider_cli_shared();
45667
45677
  init_logger();
45668
45678
  import * as fs20 from "fs";
45679
+ import { createHash as createHash5 } from "crypto";
45669
45680
  function stripAnsi3(text) {
45670
45681
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
45671
45682
  }
@@ -45843,6 +45854,105 @@ var SpecCliAdapter = class _SpecCliAdapter {
45843
45854
  isReady() {
45844
45855
  return this.spawned && !this.exited;
45845
45856
  }
45857
+ // Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
45858
+ // The spec path drives the child through the transport/driver rather than a
45859
+ // directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
45860
+ // lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
45861
+ // session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
45862
+ isAlive() {
45863
+ return this.spawned && !this.exited;
45864
+ }
45865
+ // MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
45866
+ // ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
45867
+ // not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
45868
+ // the char count still looks safe.
45869
+ static TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
45870
+ static TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
45871
+ /**
45872
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
45873
+ * of the CURRENT rendered viewport for mesh_read_terminal on the spec path
45874
+ * (claude-cli / antigravity / codex-cli — the native-source providers that
45875
+ * route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
45876
+ * - returns ONLY the driver's current viewport snapshot, the cursor
45877
+ * position and the terminal geometry — NO scrollback, NO parser/FSM
45878
+ * state, NO debug buffers;
45879
+ * - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
45880
+ * screen of multi-byte glyphs can never exceed the MCP payload cap;
45881
+ * - `hash` is over the FULL untruncated viewport so a caller can detect a
45882
+ * screen change across polls even when the returned text was truncated.
45883
+ *
45884
+ * SECURITY: the raw viewport can carry tokens / command args / env / user
45885
+ * data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
45886
+ */
45887
+ getTerminalScreenSnapshot(maxBytes = _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES) {
45888
+ const cap = Math.min(
45889
+ _SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
45890
+ Math.max(1024, Math.floor(maxBytes) || _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES)
45891
+ );
45892
+ let rawViewport = "";
45893
+ try {
45894
+ rawViewport = this.driver.snapshot() || "";
45895
+ } catch {
45896
+ rawViewport = "";
45897
+ }
45898
+ let cursor = { row: 0, col: 0 };
45899
+ try {
45900
+ cursor = this.driver.getCursorPosition();
45901
+ } catch {
45902
+ }
45903
+ let size = { cols: 0, rows: 0 };
45904
+ try {
45905
+ size = this.driver.getScreenSize?.() ?? size;
45906
+ } catch {
45907
+ }
45908
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
45909
+ const hash = createHash5("sha256").update(rawViewport, "utf8").digest("hex").slice(0, 16);
45910
+ return {
45911
+ text: truncation.text,
45912
+ cursor: { col: cursor.col, row: cursor.row },
45913
+ cols: size.cols,
45914
+ rows: size.rows,
45915
+ truncated: truncation.truncated,
45916
+ originalBytes: truncation.originalBytes,
45917
+ returnedBytes: truncation.returnedBytes,
45918
+ hash
45919
+ };
45920
+ }
45921
+ /**
45922
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
45923
+ * sequence into the spec-driven PTY for mesh_send_keys. Mirrors
45924
+ * ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
45925
+ * whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
45926
+ * contiguous string, so a submit key can never be separated from the text
45927
+ * it submits).
45928
+ *
45929
+ * The spec path drives the child through the FsmDriver, not a directly-held
45930
+ * ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
45931
+ * against here (the driver serializes its own writes), so the only guard is
45932
+ * the modal fail-closed: a NON-destructive injection into an actionable
45933
+ * approval modal is refused (use mesh_approve) unless explicitly overridden.
45934
+ * A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
45935
+ * past this gate (the tool layer owns the destructive double-gate + audit).
45936
+ * This method NEVER logs the literal text — only key enums / byte length.
45937
+ */
45938
+ async injectKeys(items, opts = {}) {
45939
+ if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
45940
+ const encoded = encodeMeshSendKeys(items);
45941
+ const modalActive = this.latestState?.status === "approval";
45942
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
45943
+ LOG.warn("SpecAdapter", `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(",")} \u2014 use mesh_approve`);
45944
+ return { ok: false, refused: "actionable_modal", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
45945
+ }
45946
+ this.driver.dispatch({ kind: "pty_write", data: encoded.sequence });
45947
+ LOG.info("SpecAdapter", `[${this.cliType}] send_keys injected keys=${encoded.keys.join(",") || "(text-only)"} bytes=${Buffer.byteLength(encoded.sequence, "utf8")} destructive=${encoded.hasDestructive}`);
45948
+ return {
45949
+ ok: true,
45950
+ keys: encoded.keys,
45951
+ hasDestructive: encoded.hasDestructive,
45952
+ submits: encoded.submits,
45953
+ bytes: Buffer.byteLength(encoded.sequence, "utf8")
45954
+ };
45955
+ }
45846
45956
  setOnStatusChange(cb) {
45847
45957
  this.statusCallback = cb;
45848
45958
  }
@@ -48110,6 +48220,25 @@ var CliProviderInstance = class _CliProviderInstance {
48110
48220
  }
48111
48221
  return "";
48112
48222
  }
48223
+ /**
48224
+ * NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
48225
+ * cached for the current turn (lastCompletionSummary), if any. The evidence
48226
+ * probe (completionFinalAssistantEvidence) is a POINT-SAMPLE: on a native-source
48227
+ * provider (antigravity) the parsed screen and the native transcript can both
48228
+ * momentarily yield no in-turn final assistant at the exact instant the
48229
+ * completion gate fires — source='unavailable', missingEvidence=true — even
48230
+ * though a prior poll already read the real answer off native-history and cached
48231
+ * it here (the same value mesh_read_chat.summary shows). Consulting the cache at
48232
+ * emit time lets that already-secured summary count as evidence, so the completion
48233
+ * notification carries the answer instead of completion_diagnostic=missing_final_assistant
48234
+ * with an empty summary. Returns '' when the cache is empty or was reset by the
48235
+ * next turn (see lastCompletionSummary = null on onTurnStarted).
48236
+ */
48237
+ cachedCompletionSummaryContent() {
48238
+ const cached3 = this.lastCompletionSummary;
48239
+ const content = typeof cached3?.content === "string" ? cached3.content.trim() : "";
48240
+ return content;
48241
+ }
48113
48242
  completionFinalAssistantEvidence(parsedMessages, turnStartedAt) {
48114
48243
  const turnClosed = !this.hasAdapterPendingResponse();
48115
48244
  if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
@@ -48173,12 +48302,19 @@ var CliProviderInstance = class _CliProviderInstance {
48173
48302
  const lastVisibleRole = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : null;
48174
48303
  const lastVisibleKind = typeof lastVisible?.kind === "string" ? lastVisible.kind : null;
48175
48304
  const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
48305
+ const cachedSummary = evidence.present ? "" : this.cachedCompletionSummaryContent();
48306
+ const creditedFromCache = !evidence.present && cachedSummary.length > 0;
48307
+ const finalAssistantPresent = evidence.present || creditedFromCache;
48308
+ const finalAssistantEvidenceSource = evidence.present ? evidence.source : creditedFromCache ? "cached-summary" : evidence.source;
48309
+ const clearMissingBlock = creditedFromCache && args.blockReason === "missing_final_assistant";
48310
+ const effectiveBlockReason = clearMissingBlock ? void 0 : args.blockReason;
48176
48311
  return {
48177
48312
  providerType: this.type,
48178
48313
  sessionId: this.instanceId,
48179
48314
  providerSessionId: this.providerSessionId || null,
48180
48315
  workspace: this.workingDir,
48181
- blockReason: args.blockReason,
48316
+ ...effectiveBlockReason ? { blockReason: effectiveBlockReason } : {},
48317
+ ...clearMissingBlock ? { originalBlockReason: args.blockReason } : {},
48182
48318
  emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
48183
48319
  waitedMs: args.waitedMs,
48184
48320
  maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
@@ -48186,8 +48322,9 @@ var CliProviderInstance = class _CliProviderInstance {
48186
48322
  latestVisibleStatus: args.latestVisibleStatus,
48187
48323
  parsedStatus: typeof parsed?.status === "string" ? parsed.status : parseError ? "parse_error" : "unknown",
48188
48324
  parseError: parseError || void 0,
48189
- finalAssistantPresent: evidence.present,
48190
- finalAssistantEvidenceSource: evidence.source,
48325
+ finalAssistantPresent,
48326
+ finalAssistantFromCachedSummary: !evidence.present && cachedSummary.length > 0,
48327
+ finalAssistantEvidenceSource,
48191
48328
  visibleMessageCount: visibleMessages.length,
48192
48329
  lastVisibleRole,
48193
48330
  lastVisibleKind,
@@ -48381,6 +48518,7 @@ var CliProviderInstance = class _CliProviderInstance {
48381
48518
  */
48382
48519
  getTerminalScreenSnapshot(maxBytes) {
48383
48520
  if (!this.isMeshWorkerSession()) return null;
48521
+ if (typeof this.adapter.getTerminalScreenSnapshot !== "function") return null;
48384
48522
  return this.adapter.getTerminalScreenSnapshot(maxBytes);
48385
48523
  }
48386
48524
  /**
@@ -48399,6 +48537,9 @@ var CliProviderInstance = class _CliProviderInstance {
48399
48537
  if (!this.isMeshWorkerSession()) {
48400
48538
  return { ok: false, refused: "not_mesh_worker", keys: [], hasDestructive: false };
48401
48539
  }
48540
+ if (typeof this.adapter.injectKeys !== "function") {
48541
+ return { ok: false, refused: "unsupported", keys: [], hasDestructive: false };
48542
+ }
48402
48543
  return this.adapter.injectKeys(items, opts);
48403
48544
  }
48404
48545
  /**
@@ -48431,7 +48572,7 @@ var CliProviderInstance = class _CliProviderInstance {
48431
48572
  this.meshStallEmittedForAnchor = false;
48432
48573
  return;
48433
48574
  }
48434
- if (!this.adapter.isAlive()) {
48575
+ if (typeof this.adapter.isAlive === "function" && !this.adapter.isAlive()) {
48435
48576
  this.meshStallAnchorAt = -1;
48436
48577
  this.meshStallEmittedForAnchor = false;
48437
48578
  return;
@@ -48788,7 +48929,13 @@ var CliProviderInstance = class _CliProviderInstance {
48788
48929
  // delegated session's inbox preview blank — or, for a LOCAL worktree session,
48789
48930
  // stuck on the dispatched user task. If the parser DID surface assistant text,
48790
48931
  // prefer it; only fall back to '' when no assistant summary can be derived.
48791
- finalSummary: blockReason.startsWith("parsed_status:") ? this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) ?? "" : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
48932
+ // NOTIF Defect-B: completionFinalSummary is a point-sample of native-history/
48933
+ // screen at THIS instant; on a native-source provider (antigravity) it can be
48934
+ // empty at the forced-emit instant even though a prior poll already cached the
48935
+ // real answer (lastCompletionSummary). Fall back to the cache so the notification
48936
+ // carries the summary that mesh_read_chat.summary already shows — consistent with
48937
+ // completionDiagnostic.finalAssistantPresent being credited from the same cache.
48938
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) || this.cachedCompletionSummaryContent() || (blockReason.startsWith("parsed_status:") ? "" : void 0),
48792
48939
  completionDiagnostic
48793
48940
  });
48794
48941
  this.completedDebouncePending = null;
@@ -60489,6 +60636,150 @@ ${e?.stderr || ""}`
60489
60636
  };
60490
60637
  }
60491
60638
  }
60639
+ async function classifyPatchEquivalenceFailure(repoRoot, baseHead, branchHead, summary, options = {}) {
60640
+ const targetBaseRef = options.targetBaseRef || baseHead;
60641
+ const autoPublish = options.autoPublishSubmoduleMainCommits;
60642
+ const evidence = {
60643
+ baseHead,
60644
+ branchHead,
60645
+ mergeBase: summary.mergeBase,
60646
+ expectedPatchId: summary.expectedPatchId,
60647
+ actualPatchId: summary.actualPatchId,
60648
+ patchIdEqual: !!summary.expectedPatchId && summary.expectedPatchId === summary.actualPatchId,
60649
+ ...autoPublish !== void 0 ? { autoPublishSubmoduleMainCommits: autoPublish } : {}
60650
+ };
60651
+ try {
60652
+ const git = (args) => execFileSync7(GIT2, args, {
60653
+ cwd: repoRoot,
60654
+ encoding: "utf8",
60655
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
60656
+ windowsHide: true
60657
+ });
60658
+ const gitOk = (args) => {
60659
+ try {
60660
+ git(args);
60661
+ return true;
60662
+ } catch {
60663
+ return false;
60664
+ }
60665
+ };
60666
+ let ahead = 0;
60667
+ let behind = 0;
60668
+ try {
60669
+ const out = git(["rev-list", "--left-right", "--count", `${targetBaseRef}...${branchHead}`]).trim();
60670
+ const [left, right] = out.split(/\s+/).map((n) => Number.parseInt(n, 10));
60671
+ behind = Number.isFinite(left) ? left : 0;
60672
+ ahead = Number.isFinite(right) ? right : 0;
60673
+ } catch {
60674
+ }
60675
+ evidence.ahead = ahead;
60676
+ evidence.behind = behind;
60677
+ const baseIsAncestor = gitOk(["merge-base", "--is-ancestor", targetBaseRef, branchHead]);
60678
+ evidence.baseDiverged = !baseIsAncestor;
60679
+ let diffStat = "";
60680
+ try {
60681
+ if (summary.mergedTree) {
60682
+ diffStat = git(["diff", "--stat", baseHead, summary.mergedTree]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
60683
+ } else {
60684
+ diffStat = git(["diff", "--stat", baseHead, branchHead]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
60685
+ }
60686
+ } catch {
60687
+ }
60688
+ if (diffStat) evidence.diffStat = diffStat;
60689
+ const submoduleGitlinks = [];
60690
+ try {
60691
+ const nameStatus = git(["diff", "--name-only", "--diff-filter=d", baseHead, branchHead]).trim();
60692
+ const changedPaths = nameStatus ? nameStatus.split("\n").map((p) => p.trim()).filter(Boolean) : [];
60693
+ for (const p of changedPaths) {
60694
+ let baseCommit;
60695
+ let branchCommit;
60696
+ try {
60697
+ const baseLs = git(["ls-tree", baseHead, "--", p]).trim();
60698
+ const branchLs = git(["ls-tree", branchHead, "--", p]).trim();
60699
+ const isGitlink = /(^|\s)160000\s/.test(baseLs) || /(^|\s)160000\s/.test(branchLs);
60700
+ if (!isGitlink) continue;
60701
+ baseCommit = baseLs.split(/\s+/)[2];
60702
+ branchCommit = branchLs.split(/\s+/)[2];
60703
+ } catch {
60704
+ continue;
60705
+ }
60706
+ const submoduleRepo = pathJoin2(repoRoot, p);
60707
+ let fastForward;
60708
+ let reachableFromOriginMain;
60709
+ if (branchCommit) {
60710
+ if (baseCommit) {
60711
+ fastForward = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", baseCommit, branchCommit]);
60712
+ }
60713
+ reachableFromOriginMain = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", branchCommit, "refs/remotes/origin/main"]);
60714
+ }
60715
+ submoduleGitlinks.push({ path: p, baseCommit, branchCommit, fastForward, reachableFromOriginMain });
60716
+ }
60717
+ } catch {
60718
+ }
60719
+ if (submoduleGitlinks.length) evidence.submoduleGitlinks = submoduleGitlinks;
60720
+ const gitlinkFf = summary.gitlinkTrivialFastForward;
60721
+ const noResidualDiff = !evidence.diffStat && (!summary.actualPatchId || summary.actualPatchId === "");
60722
+ if (ahead === 0 && behind === 0 && noResidualDiff) {
60723
+ return {
60724
+ detailedReason: "already_converged",
60725
+ detailedReasonDescription: "Branch is already identical to the target base (ahead 0, behind 0, no residual diff); the merge would be a no-op.",
60726
+ recommendedAction: "Treat as already converged \u2014 no merge needed. Verify with `git range-diff` / patch-id, then mark the branch merged (or clean up the worktree).",
60727
+ evidence
60728
+ };
60729
+ }
60730
+ const unreachable = submoduleGitlinks.filter((g) => g.reachableFromOriginMain === false);
60731
+ if (unreachable.length > 0) {
60732
+ const paths = unreachable.map((g) => g.path).join(", ");
60733
+ return {
60734
+ detailedReason: "submodule_unreachable",
60735
+ detailedReasonDescription: `Submodule gitlink commit(s) not reachable from submodule origin/main (publish needed): ${paths}.`,
60736
+ recommendedAction: `Publish the submodule commit(s) to submodule origin/main, then retry mesh_refine_node (policy allowAutoPublishSubmoduleMainCommits=${autoPublish === void 0 ? "unknown" : autoPublish}).`,
60737
+ evidence
60738
+ };
60739
+ }
60740
+ const changedGitlinks = submoduleGitlinks.length > 0;
60741
+ const allGitlinksFf = changedGitlinks && submoduleGitlinks.every((g) => g.fastForward === true);
60742
+ const gateSawUnresolvedGitlinkFf = gitlinkFf?.resolved === false && Array.isArray(gitlinkFf.gitlinks) && gitlinkFf.gitlinks.some((g) => g.fastForward);
60743
+ if (baseIsAncestor && (evidence.patchIdEqual || allGitlinksFf || gateSawUnresolvedGitlinkFf)) {
60744
+ return {
60745
+ detailedReason: "trivial_ff_misjudgment",
60746
+ detailedReasonDescription: "HEAD descends the target base and the patch content matches; the block is a submodule gitlink trivial fast-forward that merge-tree refused, not a real divergence.",
60747
+ recommendedAction: "Converge via the strict fast-forward-only bypass (verify HEAD descends origin/main and patch-id equality, then merge --ff-only) instead of the refine gate.",
60748
+ evidence
60749
+ };
60750
+ }
60751
+ if (!baseIsAncestor) {
60752
+ return {
60753
+ detailedReason: "base_divergence",
60754
+ detailedReasonDescription: `Worktree base has diverged from ${targetBaseRef} (HEAD is not a descendant; ahead ${ahead}, behind ${behind}).`,
60755
+ recommendedAction: `Rebase the branch onto ${targetBaseRef}, then retry mesh_refine_node.`,
60756
+ evidence
60757
+ };
60758
+ }
60759
+ return {
60760
+ detailedReason: "actual_patch_diff",
60761
+ detailedReasonDescription: "The merge introduces content not equivalent to the branch's cumulative patch (expected tree vs actual merge diff differ).",
60762
+ recommendedAction: "Manual review required \u2014 inspect the residual diff; the branch content is not patch-equivalent to a clean merge onto the base.",
60763
+ evidence
60764
+ };
60765
+ } catch (e) {
60766
+ evidence.classifierError = e?.message || String(e);
60767
+ return {
60768
+ detailedReason: "unclassified",
60769
+ detailedReasonDescription: "Patch-equivalence sub-cause could not be classified (git inspection failed); see classifierError.",
60770
+ recommendedAction: "Inspect the refineStages and patchEquivalence summary manually to determine the cause.",
60771
+ evidence
60772
+ };
60773
+ }
60774
+ }
60775
+ function execGitOk(cwd, args) {
60776
+ try {
60777
+ execFileSync7(GIT2, args, { cwd, encoding: "utf8", maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES, windowsHide: true });
60778
+ return true;
60779
+ } catch {
60780
+ return false;
60781
+ }
60782
+ }
60492
60783
  async function checkWorktreeChangesPatchEquivalentInRef(repoRoot, ref, worktreeHead) {
60493
60784
  const startedAt = Date.now();
60494
60785
  try {
@@ -61808,9 +62099,23 @@ async function refineSyncBaseStage(self, ctx) {
61808
62099
  error: submoduleHintPatchEquivalence.error,
61809
62100
  actionableHint: submoduleHintPatchEquivalence.actionableHint
61810
62101
  });
62102
+ const classification = await classifyPatchEquivalenceFailure(
62103
+ repoRoot,
62104
+ baseHead,
62105
+ ctx.branchHead,
62106
+ submoduleHintPatchEquivalence,
62107
+ {
62108
+ targetBaseRef: baseHead,
62109
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(ctx.mesh, node.workspace).enabled
62110
+ }
62111
+ );
61811
62112
  return { kind: "terminal", result: {
61812
62113
  success: false,
61813
62114
  code: "patch_equivalence_failed",
62115
+ detailedReason: classification.detailedReason,
62116
+ detailedReasonDescription: classification.detailedReasonDescription,
62117
+ recommendedAction: classification.recommendedAction,
62118
+ evidence: classification.evidence,
61814
62119
  convergenceStatus: "blocked_review",
61815
62120
  error: "Refinery patch-equivalence preflight failed (submodule gitlink conflict); merge/refine was not attempted.",
61816
62121
  branch,
@@ -61950,7 +62255,7 @@ ${tail}` : ""
61950
62255
  return { kind: "continue", ctx };
61951
62256
  }
61952
62257
  async function refinePatchEquivalenceStage(self, ctx) {
61953
- const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, validationSummary, refineStages } = ctx;
62258
+ const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, mesh, node, validationSummary, refineStages } = ctx;
61954
62259
  const branchHead = ctx.branchHead;
61955
62260
  const patchEquivalenceStarted = Date.now();
61956
62261
  const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
@@ -61964,9 +62269,27 @@ async function refinePatchEquivalenceStage(self, ctx) {
61964
62269
  if (!patchEquivalence.equivalent) {
61965
62270
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
61966
62271
  if (!alreadyMergedViaOtherPath) {
62272
+ const classification = await classifyPatchEquivalenceFailure(
62273
+ repoRoot,
62274
+ baseHead,
62275
+ branchHead,
62276
+ patchEquivalence,
62277
+ {
62278
+ targetBaseRef: baseHead,
62279
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace).enabled
62280
+ }
62281
+ );
62282
+ recordMeshRefineStage(refineStages, "patch_equivalence_classification", "failed", patchEquivalenceStarted, {
62283
+ detailedReason: classification.detailedReason,
62284
+ recommendedAction: classification.recommendedAction
62285
+ });
61967
62286
  return { kind: "terminal", result: {
61968
62287
  success: false,
61969
62288
  code: "patch_equivalence_failed",
62289
+ detailedReason: classification.detailedReason,
62290
+ detailedReasonDescription: classification.detailedReasonDescription,
62291
+ recommendedAction: classification.recommendedAction,
62292
+ evidence: classification.evidence,
61970
62293
  convergenceStatus: "blocked_review",
61971
62294
  error: "Refinery patch-equivalence preflight failed; merge/refine was not attempted.",
61972
62295
  branch,
@@ -63073,6 +63396,10 @@ async function finishMeshRefineJob(self, handle, args) {
63073
63396
  };
63074
63397
  if (typeof result.error === "string") ctx.error = result.error;
63075
63398
  if (typeof result.blockedReason === "string") ctx.blockedReason = result.blockedReason;
63399
+ if (typeof result.detailedReason === "string") ctx.detailedReason = result.detailedReason;
63400
+ if (typeof result.detailedReasonDescription === "string") ctx.detailedReasonDescription = result.detailedReasonDescription;
63401
+ if (typeof result.recommendedAction === "string") ctx.recommendedAction = result.recommendedAction;
63402
+ if (result.evidence && typeof result.evidence === "object") ctx.evidence = result.evidence;
63076
63403
  if (stage === "patch_equivalence" && result.patchEquivalence) {
63077
63404
  const pe = result.patchEquivalence;
63078
63405
  ctx.details = {
@@ -63080,7 +63407,10 @@ async function finishMeshRefineJob(self, handle, args) {
63080
63407
  actualPatchId: pe.actualPatchId,
63081
63408
  status: pe.status,
63082
63409
  actionableHint: pe.actionableHint,
63083
- error: pe.error
63410
+ error: pe.error,
63411
+ ...typeof result.detailedReason === "string" ? { detailedReason: result.detailedReason } : {},
63412
+ ...typeof result.recommendedAction === "string" ? { recommendedAction: result.recommendedAction } : {},
63413
+ ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {}
63084
63414
  };
63085
63415
  }
63086
63416
  if (stage === "submodule_reachability" && Array.isArray(result.unreachableSubmoduleCommits)) {