@adhdev/daemon-core 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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { ChatMessage } from './types.js';
7
7
  import type { InteractivePrompt, InteractivePromptResponse } from './providers/types/interactive-prompt.js';
8
+ import type { MeshSendKeyItem, MeshSendKeyName } from './cli-adapters/provider-cli-shared.js';
8
9
  export interface CliAdapterStatus {
9
10
  status?: string;
10
11
  parsedStatus?: string;
@@ -118,6 +119,34 @@ export interface CliAdapter {
118
119
  cancel(): void;
119
120
  isProcessing(): boolean;
120
121
  isReady(): boolean;
122
+ isAlive?(): boolean;
123
+ getTerminalScreenSnapshot?(maxBytes?: number): {
124
+ text: string;
125
+ cursor: {
126
+ col: number;
127
+ row: number;
128
+ };
129
+ cols: number;
130
+ rows: number;
131
+ truncated: boolean;
132
+ originalBytes: number;
133
+ returnedBytes: number;
134
+ hash: string;
135
+ };
136
+ injectKeys?(items: MeshSendKeyItem[], opts?: {
137
+ allowModalOverride?: boolean;
138
+ }): Promise<{
139
+ ok: true;
140
+ keys: MeshSendKeyName[];
141
+ hasDestructive: boolean;
142
+ submits: boolean;
143
+ bytes: number;
144
+ } | {
145
+ ok: false;
146
+ refused: 'submit_race' | 'actionable_modal';
147
+ keys: MeshSendKeyName[];
148
+ hasDestructive: boolean;
149
+ }>;
121
150
  setOnStatusChange(callback: () => void): void;
122
151
  updateRuntimeSettings?(settings: Record<string, unknown>): void;
123
152
  setCliScripts?(scripts: Record<string, unknown>): void;
package/dist/index.js CHANGED
@@ -428,10 +428,10 @@ function readInjected(value) {
428
428
  }
429
429
  function getDaemonBuildInfo() {
430
430
  if (cached) return cached;
431
- const commit = readInjected(true ? "3fc4f272513b6f628ec1732ef57d9ed94e08b8ff" : void 0) ?? "unknown";
432
- const commitShort = readInjected(true ? "3fc4f272" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
433
- const version = readInjected(true ? "0.9.82-rc.553" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
434
- const builtAt = readInjected(true ? "2026-07-17T06:04:22.090Z" : void 0);
431
+ const commit = readInjected(true ? "45a19b2d93612559899332379b6953a02a4b4ccb" : void 0) ?? "unknown";
432
+ const commitShort = readInjected(true ? "45a19b2d" : void 0) ?? (commit !== "unknown" ? commit.slice(0, 7) : "unknown");
433
+ const version = readInjected(true ? "0.9.82-rc.555" : void 0) ?? readInjected(typeof process !== "undefined" ? process.env?.ADHDEV_PKG_VERSION : void 0) ?? "unknown";
434
+ const builtAt = readInjected(true ? "2026-07-17T08:34:01.452Z" : void 0);
435
435
  cached = builtAt ? { commit, commitShort, version, builtAt } : { commit, commitShort, version };
436
436
  return cached;
437
437
  }
@@ -43722,6 +43722,12 @@ var TerminalAdapter = class {
43722
43722
  const pos = this.screen.getCursorPosition();
43723
43723
  return { row: pos.row, col: pos.col };
43724
43724
  }
43725
+ /** Current terminal geometry (columns × rows). Tracked here rather than
43726
+ * read off the screen buffer so a resize is reflected immediately, before
43727
+ * the next repaint. Consumed by the mesh_read_terminal viewport read. */
43728
+ getScreenSize() {
43729
+ return { cols: this.cols, rows: this.rows };
43730
+ }
43725
43731
  send_keys(text) {
43726
43732
  this.recordEvent("input", capPreview(escapeControl(text)), text.length);
43727
43733
  this.pty?.write(text);
@@ -44060,6 +44066,9 @@ var FsmDriver = class {
44060
44066
  getScreen() {
44061
44067
  return this.adapter.snapshot();
44062
44068
  }
44069
+ getScreenSize() {
44070
+ return this.adapter.getScreenSize();
44071
+ }
44063
44072
  /** Scrollback-inclusive screen as line array — used only for modal/button
44064
44073
  * content extraction so a tall prompt's off-screen anchors stay matchable.
44065
44074
  * Falls back to the viewport snapshot if scrollback read is unavailable. */
@@ -46098,6 +46107,8 @@ function readTailJsonlLines(filePath, maxBytes) {
46098
46107
 
46099
46108
  // src/providers/spec/cli-adapter.ts
46100
46109
  var fs20 = __toESM(require("fs"));
46110
+ var import_node_crypto4 = require("crypto");
46111
+ init_provider_cli_shared();
46101
46112
  init_logger();
46102
46113
  function stripAnsi3(text) {
46103
46114
  return String(text || "").replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, "").replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
@@ -46276,6 +46287,105 @@ var SpecCliAdapter = class _SpecCliAdapter {
46276
46287
  isReady() {
46277
46288
  return this.spawned && !this.exited;
46278
46289
  }
46290
+ // Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
46291
+ // The spec path drives the child through the transport/driver rather than a
46292
+ // directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
46293
+ // lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
46294
+ // session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
46295
+ isAlive() {
46296
+ return this.spawned && !this.exited;
46297
+ }
46298
+ // MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
46299
+ // ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
46300
+ // not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
46301
+ // the char count still looks safe.
46302
+ static TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
46303
+ static TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
46304
+ /**
46305
+ * MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
46306
+ * of the CURRENT rendered viewport for mesh_read_terminal on the spec path
46307
+ * (claude-cli / antigravity / codex-cli — the native-source providers that
46308
+ * route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
46309
+ * - returns ONLY the driver's current viewport snapshot, the cursor
46310
+ * position and the terminal geometry — NO scrollback, NO parser/FSM
46311
+ * state, NO debug buffers;
46312
+ * - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
46313
+ * screen of multi-byte glyphs can never exceed the MCP payload cap;
46314
+ * - `hash` is over the FULL untruncated viewport so a caller can detect a
46315
+ * screen change across polls even when the returned text was truncated.
46316
+ *
46317
+ * SECURITY: the raw viewport can carry tokens / command args / env / user
46318
+ * data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
46319
+ */
46320
+ getTerminalScreenSnapshot(maxBytes = _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES) {
46321
+ const cap = Math.min(
46322
+ _SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
46323
+ Math.max(1024, Math.floor(maxBytes) || _SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES)
46324
+ );
46325
+ let rawViewport = "";
46326
+ try {
46327
+ rawViewport = this.driver.snapshot() || "";
46328
+ } catch {
46329
+ rawViewport = "";
46330
+ }
46331
+ let cursor = { row: 0, col: 0 };
46332
+ try {
46333
+ cursor = this.driver.getCursorPosition();
46334
+ } catch {
46335
+ }
46336
+ let size = { cols: 0, rows: 0 };
46337
+ try {
46338
+ size = this.driver.getScreenSize?.() ?? size;
46339
+ } catch {
46340
+ }
46341
+ const truncation = truncateToByteTailByLine(rawViewport, cap);
46342
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(rawViewport, "utf8").digest("hex").slice(0, 16);
46343
+ return {
46344
+ text: truncation.text,
46345
+ cursor: { col: cursor.col, row: cursor.row },
46346
+ cols: size.cols,
46347
+ rows: size.rows,
46348
+ truncated: truncation.truncated,
46349
+ originalBytes: truncation.originalBytes,
46350
+ returnedBytes: truncation.returnedBytes,
46351
+ hash
46352
+ };
46353
+ }
46354
+ /**
46355
+ * MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
46356
+ * sequence into the spec-driven PTY for mesh_send_keys. Mirrors
46357
+ * ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
46358
+ * whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
46359
+ * contiguous string, so a submit key can never be separated from the text
46360
+ * it submits).
46361
+ *
46362
+ * The spec path drives the child through the FsmDriver, not a directly-held
46363
+ * ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
46364
+ * against here (the driver serializes its own writes), so the only guard is
46365
+ * the modal fail-closed: a NON-destructive injection into an actionable
46366
+ * approval modal is refused (use mesh_approve) unless explicitly overridden.
46367
+ * A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
46368
+ * past this gate (the tool layer owns the destructive double-gate + audit).
46369
+ * This method NEVER logs the literal text — only key enums / byte length.
46370
+ */
46371
+ async injectKeys(items, opts = {}) {
46372
+ if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
46373
+ const encoded = encodeMeshSendKeys(items);
46374
+ const modalActive = this.latestState?.status === "approval";
46375
+ if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
46376
+ LOG.warn("SpecAdapter", `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(",")} \u2014 use mesh_approve`);
46377
+ return { ok: false, refused: "actionable_modal", keys: encoded.keys, hasDestructive: encoded.hasDestructive };
46378
+ }
46379
+ this.driver.dispatch({ kind: "pty_write", data: encoded.sequence });
46380
+ LOG.info("SpecAdapter", `[${this.cliType}] send_keys injected keys=${encoded.keys.join(",") || "(text-only)"} bytes=${Buffer.byteLength(encoded.sequence, "utf8")} destructive=${encoded.hasDestructive}`);
46381
+ return {
46382
+ ok: true,
46383
+ keys: encoded.keys,
46384
+ hasDestructive: encoded.hasDestructive,
46385
+ submits: encoded.submits,
46386
+ bytes: Buffer.byteLength(encoded.sequence, "utf8")
46387
+ };
46388
+ }
46279
46389
  setOnStatusChange(cb) {
46280
46390
  this.statusCallback = cb;
46281
46391
  }
@@ -48543,6 +48653,25 @@ var CliProviderInstance = class _CliProviderInstance {
48543
48653
  }
48544
48654
  return "";
48545
48655
  }
48656
+ /**
48657
+ * NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
48658
+ * cached for the current turn (lastCompletionSummary), if any. The evidence
48659
+ * probe (completionFinalAssistantEvidence) is a POINT-SAMPLE: on a native-source
48660
+ * provider (antigravity) the parsed screen and the native transcript can both
48661
+ * momentarily yield no in-turn final assistant at the exact instant the
48662
+ * completion gate fires — source='unavailable', missingEvidence=true — even
48663
+ * though a prior poll already read the real answer off native-history and cached
48664
+ * it here (the same value mesh_read_chat.summary shows). Consulting the cache at
48665
+ * emit time lets that already-secured summary count as evidence, so the completion
48666
+ * notification carries the answer instead of completion_diagnostic=missing_final_assistant
48667
+ * with an empty summary. Returns '' when the cache is empty or was reset by the
48668
+ * next turn (see lastCompletionSummary = null on onTurnStarted).
48669
+ */
48670
+ cachedCompletionSummaryContent() {
48671
+ const cached3 = this.lastCompletionSummary;
48672
+ const content = typeof cached3?.content === "string" ? cached3.content.trim() : "";
48673
+ return content;
48674
+ }
48546
48675
  completionFinalAssistantEvidence(parsedMessages, turnStartedAt) {
48547
48676
  const turnClosed = !this.hasAdapterPendingResponse();
48548
48677
  if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
@@ -48606,12 +48735,19 @@ var CliProviderInstance = class _CliProviderInstance {
48606
48735
  const lastVisibleRole = typeof lastVisible?.role === "string" ? lastVisible.role.trim().toLowerCase() : null;
48607
48736
  const lastVisibleKind = typeof lastVisible?.kind === "string" ? lastVisible.kind : null;
48608
48737
  const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
48738
+ const cachedSummary = evidence.present ? "" : this.cachedCompletionSummaryContent();
48739
+ const creditedFromCache = !evidence.present && cachedSummary.length > 0;
48740
+ const finalAssistantPresent = evidence.present || creditedFromCache;
48741
+ const finalAssistantEvidenceSource = evidence.present ? evidence.source : creditedFromCache ? "cached-summary" : evidence.source;
48742
+ const clearMissingBlock = creditedFromCache && args.blockReason === "missing_final_assistant";
48743
+ const effectiveBlockReason = clearMissingBlock ? void 0 : args.blockReason;
48609
48744
  return {
48610
48745
  providerType: this.type,
48611
48746
  sessionId: this.instanceId,
48612
48747
  providerSessionId: this.providerSessionId || null,
48613
48748
  workspace: this.workingDir,
48614
- blockReason: args.blockReason,
48749
+ ...effectiveBlockReason ? { blockReason: effectiveBlockReason } : {},
48750
+ ...clearMissingBlock ? { originalBlockReason: args.blockReason } : {},
48615
48751
  emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
48616
48752
  waitedMs: args.waitedMs,
48617
48753
  maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
@@ -48619,8 +48755,9 @@ var CliProviderInstance = class _CliProviderInstance {
48619
48755
  latestVisibleStatus: args.latestVisibleStatus,
48620
48756
  parsedStatus: typeof parsed?.status === "string" ? parsed.status : parseError ? "parse_error" : "unknown",
48621
48757
  parseError: parseError || void 0,
48622
- finalAssistantPresent: evidence.present,
48623
- finalAssistantEvidenceSource: evidence.source,
48758
+ finalAssistantPresent,
48759
+ finalAssistantFromCachedSummary: !evidence.present && cachedSummary.length > 0,
48760
+ finalAssistantEvidenceSource,
48624
48761
  visibleMessageCount: visibleMessages.length,
48625
48762
  lastVisibleRole,
48626
48763
  lastVisibleKind,
@@ -48814,6 +48951,7 @@ var CliProviderInstance = class _CliProviderInstance {
48814
48951
  */
48815
48952
  getTerminalScreenSnapshot(maxBytes) {
48816
48953
  if (!this.isMeshWorkerSession()) return null;
48954
+ if (typeof this.adapter.getTerminalScreenSnapshot !== "function") return null;
48817
48955
  return this.adapter.getTerminalScreenSnapshot(maxBytes);
48818
48956
  }
48819
48957
  /**
@@ -48832,6 +48970,9 @@ var CliProviderInstance = class _CliProviderInstance {
48832
48970
  if (!this.isMeshWorkerSession()) {
48833
48971
  return { ok: false, refused: "not_mesh_worker", keys: [], hasDestructive: false };
48834
48972
  }
48973
+ if (typeof this.adapter.injectKeys !== "function") {
48974
+ return { ok: false, refused: "unsupported", keys: [], hasDestructive: false };
48975
+ }
48835
48976
  return this.adapter.injectKeys(items, opts);
48836
48977
  }
48837
48978
  /**
@@ -48864,7 +49005,7 @@ var CliProviderInstance = class _CliProviderInstance {
48864
49005
  this.meshStallEmittedForAnchor = false;
48865
49006
  return;
48866
49007
  }
48867
- if (!this.adapter.isAlive()) {
49008
+ if (typeof this.adapter.isAlive === "function" && !this.adapter.isAlive()) {
48868
49009
  this.meshStallAnchorAt = -1;
48869
49010
  this.meshStallEmittedForAnchor = false;
48870
49011
  return;
@@ -49221,7 +49362,13 @@ var CliProviderInstance = class _CliProviderInstance {
49221
49362
  // delegated session's inbox preview blank — or, for a LOCAL worktree session,
49222
49363
  // stuck on the dispatched user task. If the parser DID surface assistant text,
49223
49364
  // prefer it; only fall back to '' when no assistant summary can be derived.
49224
- finalSummary: blockReason.startsWith("parsed_status:") ? this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) ?? "" : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt),
49365
+ // NOTIF Defect-B: completionFinalSummary is a point-sample of native-history/
49366
+ // screen at THIS instant; on a native-source provider (antigravity) it can be
49367
+ // empty at the forced-emit instant even though a prior poll already cached the
49368
+ // real answer (lastCompletionSummary). Fall back to the cache so the notification
49369
+ // carries the summary that mesh_read_chat.summary already shows — consistent with
49370
+ // completionDiagnostic.finalAssistantPresent being credited from the same cache.
49371
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt) || this.cachedCompletionSummaryContent() || (blockReason.startsWith("parsed_status:") ? "" : void 0),
49225
49372
  completionDiagnostic
49226
49373
  });
49227
49374
  this.completedDebouncePending = null;
@@ -60917,6 +61064,150 @@ ${e?.stderr || ""}`
60917
61064
  };
60918
61065
  }
60919
61066
  }
61067
+ async function classifyPatchEquivalenceFailure(repoRoot, baseHead, branchHead, summary, options = {}) {
61068
+ const targetBaseRef = options.targetBaseRef || baseHead;
61069
+ const autoPublish = options.autoPublishSubmoduleMainCommits;
61070
+ const evidence = {
61071
+ baseHead,
61072
+ branchHead,
61073
+ mergeBase: summary.mergeBase,
61074
+ expectedPatchId: summary.expectedPatchId,
61075
+ actualPatchId: summary.actualPatchId,
61076
+ patchIdEqual: !!summary.expectedPatchId && summary.expectedPatchId === summary.actualPatchId,
61077
+ ...autoPublish !== void 0 ? { autoPublishSubmoduleMainCommits: autoPublish } : {}
61078
+ };
61079
+ try {
61080
+ const git = (args) => (0, import_node_child_process6.execFileSync)(GIT2, args, {
61081
+ cwd: repoRoot,
61082
+ encoding: "utf8",
61083
+ maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
61084
+ windowsHide: true
61085
+ });
61086
+ const gitOk = (args) => {
61087
+ try {
61088
+ git(args);
61089
+ return true;
61090
+ } catch {
61091
+ return false;
61092
+ }
61093
+ };
61094
+ let ahead = 0;
61095
+ let behind = 0;
61096
+ try {
61097
+ const out = git(["rev-list", "--left-right", "--count", `${targetBaseRef}...${branchHead}`]).trim();
61098
+ const [left, right] = out.split(/\s+/).map((n) => Number.parseInt(n, 10));
61099
+ behind = Number.isFinite(left) ? left : 0;
61100
+ ahead = Number.isFinite(right) ? right : 0;
61101
+ } catch {
61102
+ }
61103
+ evidence.ahead = ahead;
61104
+ evidence.behind = behind;
61105
+ const baseIsAncestor = gitOk(["merge-base", "--is-ancestor", targetBaseRef, branchHead]);
61106
+ evidence.baseDiverged = !baseIsAncestor;
61107
+ let diffStat = "";
61108
+ try {
61109
+ if (summary.mergedTree) {
61110
+ diffStat = git(["diff", "--stat", baseHead, summary.mergedTree]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
61111
+ } else {
61112
+ diffStat = git(["diff", "--stat", baseHead, branchHead]).trim().split("\n").filter(Boolean).slice(-1)[0] || "";
61113
+ }
61114
+ } catch {
61115
+ }
61116
+ if (diffStat) evidence.diffStat = diffStat;
61117
+ const submoduleGitlinks = [];
61118
+ try {
61119
+ const nameStatus = git(["diff", "--name-only", "--diff-filter=d", baseHead, branchHead]).trim();
61120
+ const changedPaths = nameStatus ? nameStatus.split("\n").map((p) => p.trim()).filter(Boolean) : [];
61121
+ for (const p of changedPaths) {
61122
+ let baseCommit;
61123
+ let branchCommit;
61124
+ try {
61125
+ const baseLs = git(["ls-tree", baseHead, "--", p]).trim();
61126
+ const branchLs = git(["ls-tree", branchHead, "--", p]).trim();
61127
+ const isGitlink = /(^|\s)160000\s/.test(baseLs) || /(^|\s)160000\s/.test(branchLs);
61128
+ if (!isGitlink) continue;
61129
+ baseCommit = baseLs.split(/\s+/)[2];
61130
+ branchCommit = branchLs.split(/\s+/)[2];
61131
+ } catch {
61132
+ continue;
61133
+ }
61134
+ const submoduleRepo = (0, import_path14.join)(repoRoot, p);
61135
+ let fastForward;
61136
+ let reachableFromOriginMain;
61137
+ if (branchCommit) {
61138
+ if (baseCommit) {
61139
+ fastForward = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", baseCommit, branchCommit]);
61140
+ }
61141
+ reachableFromOriginMain = execGitOk(submoduleRepo, ["merge-base", "--is-ancestor", branchCommit, "refs/remotes/origin/main"]);
61142
+ }
61143
+ submoduleGitlinks.push({ path: p, baseCommit, branchCommit, fastForward, reachableFromOriginMain });
61144
+ }
61145
+ } catch {
61146
+ }
61147
+ if (submoduleGitlinks.length) evidence.submoduleGitlinks = submoduleGitlinks;
61148
+ const gitlinkFf = summary.gitlinkTrivialFastForward;
61149
+ const noResidualDiff = !evidence.diffStat && (!summary.actualPatchId || summary.actualPatchId === "");
61150
+ if (ahead === 0 && behind === 0 && noResidualDiff) {
61151
+ return {
61152
+ detailedReason: "already_converged",
61153
+ detailedReasonDescription: "Branch is already identical to the target base (ahead 0, behind 0, no residual diff); the merge would be a no-op.",
61154
+ 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).",
61155
+ evidence
61156
+ };
61157
+ }
61158
+ const unreachable = submoduleGitlinks.filter((g) => g.reachableFromOriginMain === false);
61159
+ if (unreachable.length > 0) {
61160
+ const paths = unreachable.map((g) => g.path).join(", ");
61161
+ return {
61162
+ detailedReason: "submodule_unreachable",
61163
+ detailedReasonDescription: `Submodule gitlink commit(s) not reachable from submodule origin/main (publish needed): ${paths}.`,
61164
+ recommendedAction: `Publish the submodule commit(s) to submodule origin/main, then retry mesh_refine_node (policy allowAutoPublishSubmoduleMainCommits=${autoPublish === void 0 ? "unknown" : autoPublish}).`,
61165
+ evidence
61166
+ };
61167
+ }
61168
+ const changedGitlinks = submoduleGitlinks.length > 0;
61169
+ const allGitlinksFf = changedGitlinks && submoduleGitlinks.every((g) => g.fastForward === true);
61170
+ const gateSawUnresolvedGitlinkFf = gitlinkFf?.resolved === false && Array.isArray(gitlinkFf.gitlinks) && gitlinkFf.gitlinks.some((g) => g.fastForward);
61171
+ if (baseIsAncestor && (evidence.patchIdEqual || allGitlinksFf || gateSawUnresolvedGitlinkFf)) {
61172
+ return {
61173
+ detailedReason: "trivial_ff_misjudgment",
61174
+ 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.",
61175
+ 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.",
61176
+ evidence
61177
+ };
61178
+ }
61179
+ if (!baseIsAncestor) {
61180
+ return {
61181
+ detailedReason: "base_divergence",
61182
+ detailedReasonDescription: `Worktree base has diverged from ${targetBaseRef} (HEAD is not a descendant; ahead ${ahead}, behind ${behind}).`,
61183
+ recommendedAction: `Rebase the branch onto ${targetBaseRef}, then retry mesh_refine_node.`,
61184
+ evidence
61185
+ };
61186
+ }
61187
+ return {
61188
+ detailedReason: "actual_patch_diff",
61189
+ detailedReasonDescription: "The merge introduces content not equivalent to the branch's cumulative patch (expected tree vs actual merge diff differ).",
61190
+ recommendedAction: "Manual review required \u2014 inspect the residual diff; the branch content is not patch-equivalent to a clean merge onto the base.",
61191
+ evidence
61192
+ };
61193
+ } catch (e) {
61194
+ evidence.classifierError = e?.message || String(e);
61195
+ return {
61196
+ detailedReason: "unclassified",
61197
+ detailedReasonDescription: "Patch-equivalence sub-cause could not be classified (git inspection failed); see classifierError.",
61198
+ recommendedAction: "Inspect the refineStages and patchEquivalence summary manually to determine the cause.",
61199
+ evidence
61200
+ };
61201
+ }
61202
+ }
61203
+ function execGitOk(cwd, args) {
61204
+ try {
61205
+ (0, import_node_child_process6.execFileSync)(GIT2, args, { cwd, encoding: "utf8", maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES, windowsHide: true });
61206
+ return true;
61207
+ } catch {
61208
+ return false;
61209
+ }
61210
+ }
60920
61211
  async function checkWorktreeChangesPatchEquivalentInRef(repoRoot, ref, worktreeHead) {
60921
61212
  const startedAt = Date.now();
60922
61213
  try {
@@ -62236,9 +62527,23 @@ async function refineSyncBaseStage(self, ctx) {
62236
62527
  error: submoduleHintPatchEquivalence.error,
62237
62528
  actionableHint: submoduleHintPatchEquivalence.actionableHint
62238
62529
  });
62530
+ const classification = await classifyPatchEquivalenceFailure(
62531
+ repoRoot,
62532
+ baseHead,
62533
+ ctx.branchHead,
62534
+ submoduleHintPatchEquivalence,
62535
+ {
62536
+ targetBaseRef: baseHead,
62537
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(ctx.mesh, node.workspace).enabled
62538
+ }
62539
+ );
62239
62540
  return { kind: "terminal", result: {
62240
62541
  success: false,
62241
62542
  code: "patch_equivalence_failed",
62543
+ detailedReason: classification.detailedReason,
62544
+ detailedReasonDescription: classification.detailedReasonDescription,
62545
+ recommendedAction: classification.recommendedAction,
62546
+ evidence: classification.evidence,
62242
62547
  convergenceStatus: "blocked_review",
62243
62548
  error: "Refinery patch-equivalence preflight failed (submodule gitlink conflict); merge/refine was not attempted.",
62244
62549
  branch,
@@ -62378,7 +62683,7 @@ ${tail}` : ""
62378
62683
  return { kind: "continue", ctx };
62379
62684
  }
62380
62685
  async function refinePatchEquivalenceStage(self, ctx) {
62381
- const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, validationSummary, refineStages } = ctx;
62686
+ const { meshId, nodeId, args, repoRoot, baseHead, branch, baseBranch, mesh, node, validationSummary, refineStages } = ctx;
62382
62687
  const branchHead = ctx.branchHead;
62383
62688
  const patchEquivalenceStarted = Date.now();
62384
62689
  const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
@@ -62392,9 +62697,27 @@ async function refinePatchEquivalenceStage(self, ctx) {
62392
62697
  if (!patchEquivalence.equivalent) {
62393
62698
  const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
62394
62699
  if (!alreadyMergedViaOtherPath) {
62700
+ const classification = await classifyPatchEquivalenceFailure(
62701
+ repoRoot,
62702
+ baseHead,
62703
+ branchHead,
62704
+ patchEquivalence,
62705
+ {
62706
+ targetBaseRef: baseHead,
62707
+ autoPublishSubmoduleMainCommits: resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace).enabled
62708
+ }
62709
+ );
62710
+ recordMeshRefineStage(refineStages, "patch_equivalence_classification", "failed", patchEquivalenceStarted, {
62711
+ detailedReason: classification.detailedReason,
62712
+ recommendedAction: classification.recommendedAction
62713
+ });
62395
62714
  return { kind: "terminal", result: {
62396
62715
  success: false,
62397
62716
  code: "patch_equivalence_failed",
62717
+ detailedReason: classification.detailedReason,
62718
+ detailedReasonDescription: classification.detailedReasonDescription,
62719
+ recommendedAction: classification.recommendedAction,
62720
+ evidence: classification.evidence,
62398
62721
  convergenceStatus: "blocked_review",
62399
62722
  error: "Refinery patch-equivalence preflight failed; merge/refine was not attempted.",
62400
62723
  branch,
@@ -63501,6 +63824,10 @@ async function finishMeshRefineJob(self, handle, args) {
63501
63824
  };
63502
63825
  if (typeof result.error === "string") ctx.error = result.error;
63503
63826
  if (typeof result.blockedReason === "string") ctx.blockedReason = result.blockedReason;
63827
+ if (typeof result.detailedReason === "string") ctx.detailedReason = result.detailedReason;
63828
+ if (typeof result.detailedReasonDescription === "string") ctx.detailedReasonDescription = result.detailedReasonDescription;
63829
+ if (typeof result.recommendedAction === "string") ctx.recommendedAction = result.recommendedAction;
63830
+ if (result.evidence && typeof result.evidence === "object") ctx.evidence = result.evidence;
63504
63831
  if (stage === "patch_equivalence" && result.patchEquivalence) {
63505
63832
  const pe = result.patchEquivalence;
63506
63833
  ctx.details = {
@@ -63508,7 +63835,10 @@ async function finishMeshRefineJob(self, handle, args) {
63508
63835
  actualPatchId: pe.actualPatchId,
63509
63836
  status: pe.status,
63510
63837
  actionableHint: pe.actionableHint,
63511
- error: pe.error
63838
+ error: pe.error,
63839
+ ...typeof result.detailedReason === "string" ? { detailedReason: result.detailedReason } : {},
63840
+ ...typeof result.recommendedAction === "string" ? { recommendedAction: result.recommendedAction } : {},
63841
+ ...result.evidence && typeof result.evidence === "object" ? { evidence: result.evidence } : {}
63512
63842
  };
63513
63843
  }
63514
63844
  if (stage === "submodule_reachability" && Array.isArray(result.unreachableSubmoduleCommits)) {