@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/cli-adapter-types.d.ts +29 -0
- package/dist/index.js +341 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +341 -11
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-refine-gates.d.ts +86 -0
- package/dist/providers/cli-provider-instance.d.ts +16 -1
- package/dist/providers/spec/adapter.d.ts +7 -0
- package/dist/providers/spec/cli-adapter.d.ts +64 -0
- package/dist/providers/spec/fsm-driver.d.ts +11 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.ts +29 -0
- package/src/commands/router-refine.ts +40 -1
- package/src/mesh/mesh-refine-gates.ts +256 -0
- package/src/providers/cli-provider-instance.ts +83 -8
- package/src/providers/spec/adapter.ts +7 -0
- package/src/providers/spec/cli-adapter.ts +122 -0
- package/src/providers/spec/fsm-driver.ts +5 -0
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 ? "
|
|
427
|
-
const commitShort = readInjected(true ? "
|
|
428
|
-
const version = readInjected(true ? "0.9.82-rc.
|
|
429
|
-
const builtAt = readInjected(true ? "2026-07-
|
|
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:
|
|
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
|
|
48190
|
-
|
|
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
|
-
|
|
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)) {
|