@adhdev/daemon-standalone 0.9.82-rc.553 → 0.9.82-rc.555

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
@@ -30217,10 +30217,10 @@ var require_dist3 = __commonJS({
30217
30217
  }
30218
30218
  function getDaemonBuildInfo() {
30219
30219
  if (cached2) return cached2;
30220
- const commit = readInjected(true ? "3fc4f272513b6f628ec1732ef57d9ed94e08b8ff" : void 0) ?? "unknown";
30221
- const commitShort = readInjected(true ? "3fc4f272" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
30222
- const version2 = readInjected(true ? "0.9.82-rc.553" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
30223
- const builtAt = readInjected(true ? "2026-07-17T06:05:28.497Z" : void 0);
30220
+ const commit = readInjected(true ? "45a19b2d93612559899332379b6953a02a4b4ccb" : void 0) ?? "unknown";
30221
+ const commitShort = readInjected(true ? "45a19b2d" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
30222
+ const version2 = readInjected(true ? "0.9.82-rc.555" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
30223
+ const builtAt = readInjected(true ? "2026-07-17T08:35:04.385Z" : void 0);
30224
30224
  cached2 = builtAt ? { commit, commitShort, version: version2, builtAt } : { commit, commitShort, version: version2 };
30225
30225
  return cached2;
30226
30226
  }
@@ -73511,6 +73511,12 @@ ${body}
73511
73511
  const pos = this.screen.getCursorPosition();
73512
73512
  return { row: pos.row, col: pos.col };
73513
73513
  }
73514
+ /** Current terminal geometry (columns × rows). Tracked here rather than
73515
+ * read off the screen buffer so a resize is reflected immediately, before
73516
+ * the next repaint. Consumed by the mesh_read_terminal viewport read. */
73517
+ getScreenSize() {
73518
+ return { cols: this.cols, rows: this.rows };
73519
+ }
73514
73520
  send_keys(text) {
73515
73521
  this.recordEvent("input", capPreview(escapeControl(text)), text.length);
73516
73522
  this.pty?.write(text);
@@ -73843,6 +73849,9 @@ ${body}
73843
73849
  getScreen() {
73844
73850
  return this.adapter.snapshot();
73845
73851
  }
73852
+ getScreenSize() {
73853
+ return this.adapter.getScreenSize();
73854
+ }
73846
73855
  /** Scrollback-inclusive screen as line array — used only for modal/button
73847
73856
  * content extraction so a tall prompt's off-screen anchors stay matchable.
73848
73857
  * Falls back to the viewport snapshot if scrollback read is unavailable. */
@@ -75869,6 +75878,8 @@ ${body}
75869
75878
  return out;
75870
75879
  }
75871
75880
  var fs20 = __toESM2(require("fs"));
75881
+ var import_node_crypto4 = require("crypto");
75882
+ init_provider_cli_shared();
75872
75883
  init_logger();
75873
75884
  function stripAnsi3(text) {
75874
75885
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
@@ -76047,6 +76058,105 @@ ${body}
76047
76058
  isReady() {
76048
76059
  return this.spawned && !this.exited;
76049
76060
  }
76061
+ // Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
76062
+ // The spec path drives the child through the transport/driver rather than a
76063
+ // directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
76064
+ // lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
76065
+ // session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
76066
+ isAlive() {
76067
+ return this.spawned && !this.exited;
76068
+ }
76069
+ // MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
76070
+ // ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
76071
+ // not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
76072
+ // the char count still looks safe.
76073
+ static TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
76074
+ static TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
76075
+ /**
76076
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
76077
+ * of the CURRENT rendered viewport for mesh_read_terminal on the spec path
76078
+ * (claude-cli / antigravity / codex-cli — the native-source providers that
76079
+ * route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
76080
+ * - returns ONLY the driver's current viewport snapshot, the cursor
76081
+ * position and the terminal geometry — NO scrollback, NO parser/FSM
76082
+ * state, NO debug buffers;
76083
+ * - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
76084
+ * screen of multi-byte glyphs can never exceed the MCP payload cap;
76085
+ * - `hash` is over the FULL untruncated viewport so a caller can detect a
76086
+ * screen change across polls even when the returned text was truncated.
76087
+ *
76088
+ * SECURITY: the raw viewport can carry tokens / command args / env / user
76089
+ * data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
76090
+ */
76091
+ getTerminalScreenSnapshot(maxBytes = _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES) {
76092
+ const cap = Math.min(
76093
+ _SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
76094
+ Math.max(1024, Math.floor(maxBytes) || _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES)
76095
+ );
76096
+ let rawViewport = "";
76097
+ try {
76098
+ rawViewport = this.driver.snapshot() || "";
76099
+ } catch {
76100
+ rawViewport = "";
76101
+ }
76102
+ let cursor = { row: 0, col: 0 };
76103
+ try {
76104
+ cursor = this.driver.getCursorPosition();
76105
+ } catch {
76106
+ }
76107
+ let size = { cols: 0, rows: 0 };
76108
+ try {
76109
+ size = this.driver.getScreenSize?.() ?? size;
76110
+ } catch {
76111
+ }
76112
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
76113
+ const hash2 = (0, import_node_crypto4.createHash)("sha256").update(rawViewport, "utf8").digest("hex").slice(0, 16);
76114
+ return {
76115
+ text: truncation.text,
76116
+ cursor: { col: cursor.col, row: cursor.row },
76117
+ cols: size.cols,
76118
+ rows: size.rows,
76119
+ truncated: truncation.truncated,
76120
+ originalBytes: truncation.originalBytes,
76121
+ returnedBytes: truncation.returnedBytes,
76122
+ hash: hash2
76123
+ };
76124
+ }
76125
+ /**
76126
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
76127
+ * sequence into the spec-driven PTY for mesh_send_keys. Mirrors
76128
+ * ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
76129
+ * whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
76130
+ * contiguous string, so a submit key can never be separated from the text
76131
+ * it submits).
76132
+ *
76133
+ * The spec path drives the child through the FsmDriver, not a directly-held
76134
+ * ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
76135
+ * against here (the driver serializes its own writes), so the only guard is
76136
+ * the modal fail-closed: a NON-destructive injection into an actionable
76137
+ * approval modal is refused (use mesh_approve) unless explicitly overridden.
76138
+ * A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
76139
+ * past this gate (the tool layer owns the destructive double-gate + audit).
76140
+ * This method NEVER logs the literal text — only key enums / byte length.
76141
+ */
76142
+ async injectKeys(items, opts = {}) {
76143
+ if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
76144
+ const encoded = encodeMeshSendKeys(items);
76145
+ const modalActive = this.latestState?.status === "approval";
76146
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
76147
+ LOG2.warn("SpecAdapter", `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(",")} \u2014 use mesh_approve`);
76148
+ return { ok: false, refused: "actionable_modal", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
76149
+ }
76150
+ this.driver.dispatch({ kind: "pty_write", data: encoded.sequence });
76151
+ LOG2.info("SpecAdapter", `[${this.cliType}] send_keys injected keys=${encoded.keys.join(",") || "(text-only)"} bytes=${Buffer.byteLength(encoded.sequence, "utf8")} destructive=${encoded.hasDestructive}`);
76152
+ return {
76153
+ ok: true,
76154
+ keys: encoded.keys,
76155
+ hasDestructive: encoded.hasDestructive,
76156
+ submits: encoded.submits,
76157
+ bytes: Buffer.byteLength(encoded.sequence, "utf8")
76158
+ };
76159
+ }
76050
76160
  setOnStatusChange(cb) {
76051
76161
  this.statusCallback = cb;
76052
76162
  }
@@ -78290,6 +78400,25 @@ ${body}
78290
78400
  }
78291
78401
  return "";
78292
78402
  }
78403
+ /**
78404
+ * NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
78405
+ * cached for the current turn (lastCompletionSummary), if any. The evidence
78406
+ * probe (completionFinalAssistantEvidence) is a POINT-SAMPLE: on a native-source
78407
+ * provider (antigravity) the parsed screen and the native transcript can both
78408
+ * momentarily yield no in-turn final assistant at the exact instant the
78409
+ * completion gate fires — source='unavailable', missingEvidence=true — even
78410
+ * though a prior poll already read the real answer off native-history and cached
78411
+ * it here (the same value mesh_read_chat.summary shows). Consulting the cache at
78412
+ * emit time lets that already-secured summary count as evidence, so the completion
78413
+ * notification carries the answer instead of completion_diagnostic=missing_final_assistant
78414
+ * with an empty summary. Returns '' when the cache is empty or was reset by the
78415
+ * next turn (see lastCompletionSummary = null on onTurnStarted).
78416
+ */
78417
+ cachedCompletionSummaryContent() {
78418
+ const cached3 = this.lastCompletionSummary;
78419
+ const content = typeof cached3?.content === "string" ? cached3.content.trim() : "";
78420
+ return content;
78421
+ }
78293
78422
  completionFinalAssistantEvidence(parsedMessages, turnStartedAt) {
78294
78423
  const turnClosed = !this.hasAdapterPendingResponse();
78295
78424
  if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
@@ -78353,12 +78482,19 @@ ${body}
78353
78482
  const lastVisibleRole = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : null;
78354
78483
  const lastVisibleKind = typeof lastVisible?.kind === "string" ? lastVisible.kind : null;
78355
78484
  const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
78485
+ const cachedSummary = evidence.present ? "" : this.cachedCompletionSummaryContent();
78486
+ const creditedFromCache = !evidence.present && cachedSummary.length > 0;
78487
+ const finalAssistantPresent = evidence.present || creditedFromCache;
78488
+ const finalAssistantEvidenceSource = evidence.present ? evidence.source : creditedFromCache ? "cached-summary" : evidence.source;
78489
+ const clearMissingBlock = creditedFromCache && args.blockReason === "missing_final_assistant";
78490
+ const effectiveBlockReason = clearMissingBlock ? void 0 : args.blockReason;
78356
78491
  return {
78357
78492
  providerType: this.type,
78358
78493
  sessionId: this.instanceId,
78359
78494
  providerSessionId: this.providerSessionId || null,
78360
78495
  workspace: this.workingDir,
78361
- blockReason: args.blockReason,
78496
+ ...effectiveBlockReason ? { blockReason: effectiveBlockReason } : {},
78497
+ ...clearMissingBlock ? { originalBlockReason: args.blockReason } : {},
78362
78498
  emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
78363
78499
  waitedMs: args.waitedMs,
78364
78500
  maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
@@ -78366,8 +78502,9 @@ ${body}
78366
78502
  latestVisibleStatus: args.latestVisibleStatus,
78367
78503
  parsedStatus: typeof parsed?.status === "string" ? parsed.status : parseError ? "parse_error" : "unknown",
78368
78504
  parseError: parseError || void 0,
78369
- finalAssistantPresent: evidence.present,
78370
- finalAssistantEvidenceSource: evidence.source,
78505
+ finalAssistantPresent,
78506
+ finalAssistantFromCachedSummary: !evidence.present && cachedSummary.length > 0,
78507
+ finalAssistantEvidenceSource,
78371
78508
  visibleMessageCount: visibleMessages.length,
78372
78509
  lastVisibleRole,
78373
78510
  lastVisibleKind,
@@ -78561,6 +78698,7 @@ ${body}
78561
78698
  */
78562
78699
  getTerminalScreenSnapshot(maxBytes) {
78563
78700
  if (!this.isMeshWorkerSession()) return null;
78701
+ if (typeof this.adapter.getTerminalScreenSnapshot !== "function") return null;
78564
78702
  return this.adapter.getTerminalScreenSnapshot(maxBytes);
78565
78703
  }
78566
78704
  /**
@@ -78579,6 +78717,9 @@ ${body}
78579
78717
  if (!this.isMeshWorkerSession()) {
78580
78718
  return { ok: false, refused: "not_mesh_worker", keys: [], hasDestructive: false };
78581
78719
  }
78720
+ if (typeof this.adapter.injectKeys !== "function") {
78721
+ return { ok: false, refused: "unsupported", keys: [], hasDestructive: false };
78722
+ }
78582
78723
  return this.adapter.injectKeys(items, opts);
78583
78724
  }
78584
78725
  /**
@@ -78611,7 +78752,7 @@ ${body}
78611
78752
  this.meshStallEmittedForAnchor = false;
78612
78753
  return;
78613
78754
  }
78614
- if (!this.adapter.isAlive()) {
78755
+ if (typeof this.adapter.isAlive === "function" && !this.adapter.isAlive()) {
78615
78756
  this.meshStallAnchorAt = -1;
78616
78757
  this.meshStallEmittedForAnchor = false;
78617
78758
  return;
@@ -78968,7 +79109,13 @@ ${body}
78968
79109
  // delegated session's inbox preview blank — or, for a LOCAL worktree session,
78969
79110
  // stuck on the dispatched user task. If the parser DID surface assistant text,
78970
79111
  // prefer it; only fall back to '' when no assistant summary can be derived.
78971
- finalSummary: blockReason.startsWith("parsed_status:") ? this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) ?? "" : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
79112
+ // NOTIF Defect-B: completionFinalSummary is a point-sample of native-history/
79113
+ // screen at THIS instant; on a native-source provider (antigravity) it can be
79114
+ // empty at the forced-emit instant even though a prior poll already cached the
79115
+ // real answer (lastCompletionSummary). Fall back to the cache so the notification
79116
+ // carries the summary that mesh_read_chat.summary already shows — consistent with
79117
+ // completionDiagnostic.finalAssistantPresent being credited from the same cache.
79118
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) || this.cachedCompletionSummaryContent() || (blockReason.startsWith("parsed_status:") ? "" : void 0),
78972
79119
  completionDiagnostic
78973
79120
  });
78974
79121
  this.completedDebouncePending = null;
@@ -90584,6 +90731,150 @@ ${e?.stderr || ""}`
90584
90731
  };
90585
90732
  }
90586
90733
  }
90734
+ async function classifyPatchEquivalenceFailure(repoRoot, baseHead, branchHead, summary, options = {}) {
90735
+ const targetBaseRef = options.targetBaseRef || baseHead;
90736
+ const autoPublish = options.autoPublishSubmoduleMainCommits;
90737
+ const evidence = {
90738
+ baseHead,
90739
+ branchHead,
90740
+ mergeBase: summary.mergeBase,
90741
+ expectedPatchId: summary.expectedPatchId,
90742
+ actualPatchId: summary.actualPatchId,
90743
+ patchIdEqual: !!summary.expectedPatchId && summary.expectedPatchId === summary.actualPatchId,
90744
+ ...autoPublish !== void 0 ? { autoPublishSubmoduleMainCommits: autoPublish } : {}
90745
+ };
90746
+ try {
90747
+ const git = (args) => (0, import_node_child_process6.execFileSync)(GIT2, args, {
90748
+ cwd: repoRoot,
90749
+ encoding: "utf8",
90750
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
90751
+ windowsHide: true
90752
+ });
90753
+ const gitOk = (args) => {
90754
+ try {
90755
+ git(args);
90756
+ return true;
90757
+ } catch {
90758
+ return false;
90759
+ }
90760
+ };
90761
+ let ahead = 0;
90762
+ let behind = 0;
90763
+ try {
90764
+ const out = git(["rev-list", "--left-right", "--count", `${targetBaseRef}...${branchHead}`]).trim();
90765
+ const [left, right] = out.split(/\s+/).map((n) => Number.parseInt(n, 10));
90766
+ behind = Number.isFinite(left) ? left : 0;
90767
+ ahead = Number.isFinite(right) ? right : 0;
90768
+ } catch {
90769
+ }
90770
+ evidence.ahead = ahead;
90771
+ evidence.behind = behind;
90772
+ const baseIsAncestor = gitOk(["merge-base", "--is-ancestor", targetBaseRef, branchHead]);
90773
+ evidence.baseDiverged = !baseIsAncestor;
90774
+ let diffStat = "";
90775
+ try {
90776
+ if (summary.mergedTree) {
90777
+ diffStat = git(["diff", "--stat", baseHead, summary.mergedTree]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
90778
+ } else {
90779
+ diffStat = git(["diff", "--stat", baseHead, branchHead]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
90780
+ }
90781
+ } catch {
90782
+ }
90783
+ if (diffStat) evidence.diffStat = diffStat;
90784
+ const submoduleGitlinks = [];
90785
+ try {
90786
+ const nameStatus = git(["diff", "--name-only", "--diff-filter=d", baseHead, branchHead]).trim();
90787
+ const changedPaths = nameStatus ? nameStatus.split("\n").map((p) => p.trim()).filter(Boolean) : [];
90788
+ for (const p of changedPaths) {
90789
+ let baseCommit;
90790
+ let branchCommit;
90791
+ try {
90792
+ const baseLs = git(["ls-tree", baseHead, "--", p]).trim();
90793
+ const branchLs = git(["ls-tree", branchHead, "--", p]).trim();
90794
+ const isGitlink = /(^|\s)160000\s/.test(baseLs) || /(^|\s)160000\s/.test(branchLs);
90795
+ if (!isGitlink) continue;
90796
+ baseCommit = baseLs.split(/\s+/)[2];
90797
+ branchCommit = branchLs.split(/\s+/)[2];
90798
+ } catch {
90799
+ continue;
90800
+ }
90801
+ const submoduleRepo = (0, import_path14.join)(repoRoot, p);
90802
+ let fastForward;
90803
+ let reachableFromOriginMain;
90804
+ if (branchCommit) {
90805
+ if (baseCommit) {
90806
+ fastForward = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", baseCommit, branchCommit]);
90807
+ }
90808
+ reachableFromOriginMain = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", branchCommit, "refs/remotes/origin/main"]);
90809
+ }
90810
+ submoduleGitlinks.push({ path: p, baseCommit, branchCommit, fastForward, reachableFromOriginMain });
90811
+ }
90812
+ } catch {
90813
+ }
90814
+ if (submoduleGitlinks.length) evidence.submoduleGitlinks = submoduleGitlinks;
90815
+ const gitlinkFf = summary.gitlinkTrivialFastForward;
90816
+ const noResidualDiff = !evidence.diffStat && (!summary.actualPatchId || summary.actualPatchId === "");
90817
+ if (ahead === 0 && behind === 0 && noResidualDiff) {
90818
+ return {
90819
+ detailedReason: "already_converged",
90820
+ detailedReasonDescription: "Branch is already identical to the target base (ahead 0, behind 0, no residual diff); the merge would be a no-op.",
90821
+ 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).",
90822
+ evidence
90823
+ };
90824
+ }
90825
+ const unreachable = submoduleGitlinks.filter((g) => g.reachableFromOriginMain === false);
90826
+ if (unreachable.length > 0) {
90827
+ const paths = unreachable.map((g) => g.path).join(", ");
90828
+ return {
90829
+ detailedReason: "submodule_unreachable",
90830
+ detailedReasonDescription: `Submodule gitlink commit(s) not reachable from submodule origin/main (publish needed): ${paths}.`,
90831
+ recommendedAction: `Publish the submodule commit(s) to submodule origin/main, then retry mesh_refine_node (policy allowAutoPublishSubmoduleMainCommits=${autoPublish === void 0 ? "unknown" : autoPublish}).`,
90832
+ evidence
90833
+ };
90834
+ }
90835
+ const changedGitlinks = submoduleGitlinks.length > 0;
90836
+ const allGitlinksFf = changedGitlinks && submoduleGitlinks.every((g) => g.fastForward === true);
90837
+ const gateSawUnresolvedGitlinkFf = gitlinkFf?.resolved === false && Array.isArray(gitlinkFf.gitlinks) && gitlinkFf.gitlinks.some((g) => g.fastForward);
90838
+ if (baseIsAncestor && (evidence.patchIdEqual || allGitlinksFf || gateSawUnresolvedGitlinkFf)) {
90839
+ return {
90840
+ detailedReason: "trivial_ff_misjudgment",
90841
+ 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.",
90842
+ 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.",
90843
+ evidence
90844
+ };
90845
+ }
90846
+ if (!baseIsAncestor) {
90847
+ return {
90848
+ detailedReason: "base_divergence",
90849
+ detailedReasonDescription: `Worktree base has diverged from ${targetBaseRef} (HEAD is not a descendant; ahead ${ahead}, behind ${behind}).`,
90850
+ recommendedAction: `Rebase the branch onto ${targetBaseRef}, then retry mesh_refine_node.`,
90851
+ evidence
90852
+ };
90853
+ }
90854
+ return {
90855
+ detailedReason: "actual_patch_diff",
90856
+ detailedReasonDescription: "The merge introduces content not equivalent to the branch's cumulative patch (expected tree vs actual merge diff differ).",
90857
+ recommendedAction: "Manual review required \u2014 inspect the residual diff; the branch content is not patch-equivalent to a clean merge onto the base.",
90858
+ evidence
90859
+ };
90860
+ } catch (e) {
90861
+ evidence.classifierError = e?.message || String(e);
90862
+ return {
90863
+ detailedReason: "unclassified",
90864
+ detailedReasonDescription: "Patch-equivalence sub-cause could not be classified (git inspection failed); see classifierError.",
90865
+ recommendedAction: "Inspect the refineStages and patchEquivalence summary manually to determine the cause.",
90866
+ evidence
90867
+ };
90868
+ }
90869
+ }
90870
+ function execGitOk(cwd, args) {
90871
+ try {
90872
+ (0, import_node_child_process6.execFileSync)(GIT2, args, { cwd, encoding: "utf8", maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES, windowsHide: true });
90873
+ return true;
90874
+ } catch {
90875
+ return false;
90876
+ }
90877
+ }
90587
90878
  async function checkWorktreeChangesPatchEquivalentInRef(repoRoot, ref, worktreeHead) {
90588
90879
  const startedAt = Date.now();
90589
90880
  try {
@@ -91901,9 +92192,23 @@ ${mergeTreeErr?.stderr || ""}`;
91901
92192
  error: submoduleHintPatchEquivalence.error,
91902
92193
  actionableHint: submoduleHintPatchEquivalence.actionableHint
91903
92194
  });
92195
+ const classification = await classifyPatchEquivalenceFailure(
92196
+ repoRoot,
92197
+ baseHead,
92198
+ ctx.branchHead,
92199
+ submoduleHintPatchEquivalence,
92200
+ {
92201
+ targetBaseRef: baseHead,
92202
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(ctx.mesh, node.workspace).enabled
92203
+ }
92204
+ );
91904
92205
  return { kind: "terminal", result: {
91905
92206
  success: false,
91906
92207
  code: "patch_equivalence_failed",
92208
+ detailedReason: classification.detailedReason,
92209
+ detailedReasonDescription: classification.detailedReasonDescription,
92210
+ recommendedAction: classification.recommendedAction,
92211
+ evidence: classification.evidence,
91907
92212
  convergenceStatus: "blocked_review",
91908
92213
  error: "Refinery patch-equivalence preflight failed (submodule gitlink conflict); merge/refine was not attempted.",
91909
92214
  branch,
@@ -92043,7 +92348,7 @@ ${tail}` : ""
92043
92348
  return { kind: "continue", ctx };
92044
92349
  }
92045
92350
  async function refinePatchEquivalenceStage(self, ctx) {
92046
- const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, validationSummary, refineStages } = ctx;
92351
+ const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, mesh, node, validationSummary, refineStages } = ctx;
92047
92352
  const branchHead = ctx.branchHead;
92048
92353
  const patchEquivalenceStarted = Date.now();
92049
92354
  const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
@@ -92057,9 +92362,27 @@ ${tail}` : ""
92057
92362
  if (!patchEquivalence.equivalent) {
92058
92363
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
92059
92364
  if (!alreadyMergedViaOtherPath) {
92365
+ const classification = await classifyPatchEquivalenceFailure(
92366
+ repoRoot,
92367
+ baseHead,
92368
+ branchHead,
92369
+ patchEquivalence,
92370
+ {
92371
+ targetBaseRef: baseHead,
92372
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace).enabled
92373
+ }
92374
+ );
92375
+ recordMeshRefineStage(refineStages, "patch_equivalence_classification", "failed", patchEquivalenceStarted, {
92376
+ detailedReason: classification.detailedReason,
92377
+ recommendedAction: classification.recommendedAction
92378
+ });
92060
92379
  return { kind: "terminal", result: {
92061
92380
  success: false,
92062
92381
  code: "patch_equivalence_failed",
92382
+ detailedReason: classification.detailedReason,
92383
+ detailedReasonDescription: classification.detailedReasonDescription,
92384
+ recommendedAction: classification.recommendedAction,
92385
+ evidence: classification.evidence,
92063
92386
  convergenceStatus: "blocked_review",
92064
92387
  error: "Refinery patch-equivalence preflight failed; merge/refine was not attempted.",
92065
92388
  branch,
@@ -93166,6 +93489,10 @@ ${e?.stderr || ""}`;
93166
93489
  };
93167
93490
  if (typeof result.error === "string") ctx.error = result.error;
93168
93491
  if (typeof result.blockedReason === "string") ctx.blockedReason = result.blockedReason;
93492
+ if (typeof result.detailedReason === "string") ctx.detailedReason = result.detailedReason;
93493
+ if (typeof result.detailedReasonDescription === "string") ctx.detailedReasonDescription = result.detailedReasonDescription;
93494
+ if (typeof result.recommendedAction === "string") ctx.recommendedAction = result.recommendedAction;
93495
+ if (result.evidence && typeof result.evidence === "object") ctx.evidence = result.evidence;
93169
93496
  if (stage === "patch_equivalence" && result.patchEquivalence) {
93170
93497
  const pe = result.patchEquivalence;
93171
93498
  ctx.details = {
@@ -93173,7 +93500,10 @@ ${e?.stderr || ""}`;
93173
93500
  actualPatchId: pe.actualPatchId,
93174
93501
  status: pe.status,
93175
93502
  actionableHint: pe.actionableHint,
93176
- error: pe.error
93503
+ error: pe.error,
93504
+ ...typeof result.detailedReason === "string" ? { detailedReason: result.detailedReason } : {},
93505
+ ...typeof result.recommendedAction === "string" ? { recommendedAction: result.recommendedAction } : {},
93506
+ ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {}
93177
93507
  };
93178
93508
  }
93179
93509
  if (stage === "submodule_reachability" && Array.isArray(result.unreachableSubmoduleCommits)) {