@adhdev/daemon-core 1.0.18-rc.1 → 1.0.18-rc.11
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-adapters/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +7 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +63 -0
- package/dist/index.js +6513 -4536
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6539 -4556
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-node-identity.d.ts +23 -0
- package/dist/mesh/mesh-refine-gates.d.ts +89 -0
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/cli-provider-instance-types.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +14 -0
- package/dist/providers/types/interactive-prompt.d.ts +18 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +204 -3
- package/src/cli-adapters/provider-cli-adapter.ts +39 -5
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/commands/upgrade-helper.ts +146 -79
- package/src/commands/windows-atomic-upgrade.ts +631 -0
- package/src/config/mesh-json-config.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +258 -11
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +46 -4
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +77 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +144 -2
- package/src/mesh/mesh-reconcile-loop.ts +55 -1
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/cli-provider-instance-types.ts +53 -0
- package/src/providers/cli-provider-instance.ts +193 -2
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +23 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +20 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/session-host/managed-host.ts +34 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execFileSync, type ExecFileSyncOptions } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ProcessLifecycleOptions {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
execFileSync?: typeof import('child_process').execFileSync;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OwnedProcessInfo {
|
|
10
|
+
pid: number;
|
|
11
|
+
commandLine: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultExecFileSync(): typeof import('child_process').execFileSync {
|
|
15
|
+
return execFileSync;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getWindowsProcessCommandLine(
|
|
19
|
+
pid: number,
|
|
20
|
+
exec: typeof import('child_process').execFileSync,
|
|
21
|
+
): string | null {
|
|
22
|
+
const pidFilter = `ProcessId=${pid}`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const psOut = exec('powershell.exe', [
|
|
26
|
+
'-NoProfile',
|
|
27
|
+
'-NonInteractive',
|
|
28
|
+
'-ExecutionPolicy', 'Bypass',
|
|
29
|
+
'-Command',
|
|
30
|
+
`(Get-CimInstance Win32_Process -Filter "${pidFilter}").CommandLine`,
|
|
31
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
32
|
+
const text = String(psOut).trim();
|
|
33
|
+
if (text) return text;
|
|
34
|
+
} catch {
|
|
35
|
+
// fall through to wmic fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const wmicOut = exec('wmic', [
|
|
40
|
+
'process', 'where', pidFilter, 'get', 'CommandLine',
|
|
41
|
+
], { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
42
|
+
const text = String(wmicOut).trim();
|
|
43
|
+
if (text) return text;
|
|
44
|
+
} catch {
|
|
45
|
+
// noop
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getProcessCommandLine(
|
|
52
|
+
pid: number,
|
|
53
|
+
options: ProcessLifecycleOptions = {},
|
|
54
|
+
): string | null {
|
|
55
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
56
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
57
|
+
const platform = options.platform ?? process.platform;
|
|
58
|
+
|
|
59
|
+
if (platform === 'win32') {
|
|
60
|
+
return getWindowsProcessCommandLine(pid, exec);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const text = String(exec('ps', ['-o', 'command=', '-p', String(pid)], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 3000,
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
})).trim();
|
|
69
|
+
return text || null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract the script argument (the token after the node executable) from a
|
|
77
|
+
* command line. Handles both quoted and unquoted executables/scripts.
|
|
78
|
+
*/
|
|
79
|
+
export function parseNodeScriptPath(commandLine: string | null): string | null {
|
|
80
|
+
if (!commandLine) return null;
|
|
81
|
+
let rest = commandLine.trim();
|
|
82
|
+
|
|
83
|
+
// Skip the leading executable token.
|
|
84
|
+
if (rest.startsWith('"')) {
|
|
85
|
+
const end = rest.indexOf('"', 1);
|
|
86
|
+
if (end === -1) return null;
|
|
87
|
+
rest = rest.slice(end + 1).trim();
|
|
88
|
+
} else {
|
|
89
|
+
const idx = rest.search(/\s/);
|
|
90
|
+
if (idx === -1) return null;
|
|
91
|
+
rest = rest.slice(idx + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
if (!rest) return null;
|
|
94
|
+
|
|
95
|
+
if (rest.startsWith('"')) {
|
|
96
|
+
const end = rest.indexOf('"', 1);
|
|
97
|
+
return end === -1 ? rest.slice(1) : rest.slice(1, end);
|
|
98
|
+
}
|
|
99
|
+
const idx = rest.search(/\s/);
|
|
100
|
+
return idx === -1 ? rest : rest.slice(0, idx);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeWindowsPath(value: string): string {
|
|
104
|
+
return value.toLowerCase().replace(/\//g, '\\').replace(/\\+$/, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCommandLineUnderPrefix(commandLine: string | null, prefix: string): boolean {
|
|
108
|
+
if (!commandLine) return false;
|
|
109
|
+
const needle = normalizeWindowsPath(prefix);
|
|
110
|
+
// Command-line paths may quote Windows separators as either single or
|
|
111
|
+
// doubled backslashes; collapse doubles before matching.
|
|
112
|
+
const haystack = commandLine.split('\\\\').join('\\').toLowerCase();
|
|
113
|
+
return haystack.includes(needle);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function killProcess(pid: number, options: ProcessLifecycleOptions = {}): boolean {
|
|
117
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
118
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
119
|
+
const platform = options.platform ?? process.platform;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (platform === 'win32') {
|
|
123
|
+
exec('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
process.kill(pid, 'SIGTERM');
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
|
|
137
|
+
const start = Date.now();
|
|
138
|
+
while (Date.now() - start < timeoutMs) {
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, 0);
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
142
|
+
} catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List Node processes whose command line places them under any of the supplied
|
|
151
|
+
* prefixes. Windows-only — the versioned-prefix lifecycle this supports does not
|
|
152
|
+
* exist on POSIX, so the helper is a no-op there.
|
|
153
|
+
*/
|
|
154
|
+
export function listOwnedNodeProcesses(options: {
|
|
155
|
+
prefixes: readonly string[];
|
|
156
|
+
excludePids?: readonly number[];
|
|
157
|
+
markers?: readonly string[];
|
|
158
|
+
} & ProcessLifecycleOptions): OwnedProcessInfo[] {
|
|
159
|
+
const platform = options.platform ?? process.platform;
|
|
160
|
+
if (platform !== 'win32') return [];
|
|
161
|
+
|
|
162
|
+
const exec = options.execFileSync ?? defaultExecFileSync();
|
|
163
|
+
const prefixes = options.prefixes.map((p) => normalizeWindowsPath(p));
|
|
164
|
+
const exclude = new Set((options.excludePids ?? []).filter((n) => Number.isFinite(n) && n > 0));
|
|
165
|
+
// Command-line paths use Windows separators; normalize marker separators the
|
|
166
|
+
// same way so a marker like "dist/cli/index.js" matches "dist\cli\index.js".
|
|
167
|
+
const markers = (options.markers ?? []).map((m) => m.toLowerCase().replace(/\//g, '\\'));
|
|
168
|
+
|
|
169
|
+
let pids: number[] = [];
|
|
170
|
+
try {
|
|
171
|
+
const out = String(exec('powershell.exe', [
|
|
172
|
+
'-NoProfile',
|
|
173
|
+
'-NonInteractive',
|
|
174
|
+
'-ExecutionPolicy', 'Bypass',
|
|
175
|
+
'-Command',
|
|
176
|
+
'Get-Process node -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id | ConvertTo-Json -Compress',
|
|
177
|
+
], { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true })).trim();
|
|
178
|
+
if (out) {
|
|
179
|
+
const parsed = JSON.parse(out);
|
|
180
|
+
pids = Array.isArray(parsed) ? parsed : [parsed];
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const results: OwnedProcessInfo[] = [];
|
|
187
|
+
for (const pid of pids) {
|
|
188
|
+
if (!Number.isFinite(pid) || pid <= 0 || exclude.has(pid)) continue;
|
|
189
|
+
const commandLine = getProcessCommandLine(pid, { platform, execFileSync: exec });
|
|
190
|
+
if (!commandLine) continue;
|
|
191
|
+
const lower = commandLine.toLowerCase();
|
|
192
|
+
const underPrefix = prefixes.some((prefix) => lower.includes(prefix));
|
|
193
|
+
if (!underPrefix) continue;
|
|
194
|
+
if (markers.length > 0 && !markers.some((marker) => lower.includes(marker))) continue;
|
|
195
|
+
results.push({ pid, commandLine });
|
|
196
|
+
}
|
|
197
|
+
return results;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function stopOwnedProcesses(options: {
|
|
201
|
+
processes: readonly OwnedProcessInfo[];
|
|
202
|
+
waitMs?: number;
|
|
203
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
204
|
+
const waitMs = options.waitMs ?? 15_000;
|
|
205
|
+
const killed = new Set<number>();
|
|
206
|
+
|
|
207
|
+
for (const p of options.processes) {
|
|
208
|
+
if (killProcess(p.pid, options)) killed.add(p.pid);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const survivors: OwnedProcessInfo[] = [];
|
|
212
|
+
for (const p of options.processes) {
|
|
213
|
+
if (!killed.has(p.pid)) {
|
|
214
|
+
survivors.push(p);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const exited = await waitForPidExit(p.pid, waitMs);
|
|
218
|
+
if (!exited) survivors.push(p);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A process counts as stopped only when it left the process table; a failed
|
|
222
|
+
// kill or a post-kill timeout both land it in `survivors`.
|
|
223
|
+
return { stopped: options.processes.length - survivors.length, survivors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function stopOwnedProcessesForPrefixes(options: {
|
|
227
|
+
prefixes: readonly string[];
|
|
228
|
+
excludePids?: readonly number[];
|
|
229
|
+
markers?: readonly string[];
|
|
230
|
+
waitMs?: number;
|
|
231
|
+
log?: (message: string) => void;
|
|
232
|
+
} & ProcessLifecycleOptions): Promise<{ stopped: number; survivors: OwnedProcessInfo[] }> {
|
|
233
|
+
const processes = listOwnedNodeProcesses(options);
|
|
234
|
+
if (processes.length === 0) return { stopped: 0, survivors: [] };
|
|
235
|
+
|
|
236
|
+
options.log?.(`Stopping ${processes.length} owned process(es) under prefixes: ${options.prefixes.join(', ')}`);
|
|
237
|
+
const result = await stopOwnedProcesses({
|
|
238
|
+
processes,
|
|
239
|
+
waitMs: options.waitMs,
|
|
240
|
+
platform: options.platform,
|
|
241
|
+
execFileSync: options.execFileSync,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (result.survivors.length > 0) {
|
|
245
|
+
options.log?.(`Could not stop ${result.survivors.length} owned process(es): ${result.survivors.map((s) => s.pid).join(', ')}`);
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
RefineExecFileAsync,
|
|
37
37
|
RefineStageOutcome,
|
|
38
38
|
classifyPatchEquivalenceFailure,
|
|
39
|
+
convergeDivergedSubmoduleGitlinks,
|
|
39
40
|
recordMeshRefineStage,
|
|
40
41
|
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
42
|
+
rootRebaseResolvingGitlinks,
|
|
41
43
|
runMeshRefineEffectiveDiffGate,
|
|
42
44
|
runMeshRefinePatchEquivalenceGate,
|
|
43
45
|
runMeshRefineSubmoduleReachabilityGate,
|
|
@@ -539,6 +541,9 @@ async function computeBranchBaseDivergence(
|
|
|
539
541
|
export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
540
542
|
const { repoRoot, baseHead, node, branch, baseBranch, refineStages, execFileAsync } = ctx;
|
|
541
543
|
let branchHead = ctx.branchHead;
|
|
544
|
+
// Converged submodule gitlink resolutions (path → rebased commit) from STEP 1;
|
|
545
|
+
// consumed by the gitlink-aware root rebase (STEP 2) to resolve gitlink conflicts.
|
|
546
|
+
let gitlinkResolutions: Array<{ path: string; rebasedCommit: string }> = [];
|
|
542
547
|
const syncStarted = Date.now();
|
|
543
548
|
const divergence = await computeBranchBaseDivergence(execFileAsync, node.workspace, baseHead, branchHead);
|
|
544
549
|
|
|
@@ -571,25 +576,87 @@ export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: Refine
|
|
|
571
576
|
const preRebasePe = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
572
577
|
const alreadyMerged = !preRebasePe.actualPatchId && !!preRebasePe.expectedPatchId;
|
|
573
578
|
const submoduleConflict = preRebasePe.actionableHint?.kind === 'submodule_conflict';
|
|
574
|
-
if (alreadyMerged
|
|
579
|
+
if (alreadyMerged) {
|
|
575
580
|
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
576
581
|
ahead: divergence.ahead,
|
|
577
582
|
behind: divergence.behind,
|
|
578
583
|
rebased: false,
|
|
579
|
-
reason:
|
|
584
|
+
reason: 'already_merged_via_other_path_skip_rebase',
|
|
580
585
|
});
|
|
581
586
|
return { kind: 'continue', ctx };
|
|
582
587
|
}
|
|
588
|
+
if (submoduleConflict) {
|
|
589
|
+
// DS3: a "submodule conflict" here means base and branch advanced the
|
|
590
|
+
// SAME submodule to DIVERGED sibling commits (neither an ancestor of the
|
|
591
|
+
// other), so the gitlink stays in the diff and patch-equivalence fails.
|
|
592
|
+
// Attempt to auto-converge it (STEP 1): rebase the branch-side submodule
|
|
593
|
+
// commit onto the base-side commit INSIDE the worktree submodule, so the
|
|
594
|
+
// base-side commit becomes a strict ancestor of the rebased tip. The root
|
|
595
|
+
// rebase below (STEP 2) then resolves the gitlink conflict to that rebased
|
|
596
|
+
// commit. Together this automates the documented manual strict-ff bypass
|
|
597
|
+
// and keeps the landed oss history linear. On any real submodule content
|
|
598
|
+
// conflict it backs out cleanly → we FALL BACK to the historical
|
|
599
|
+
// defer→patch_equivalence path below.
|
|
600
|
+
const converge = convergeDivergedSubmoduleGitlinks(node.workspace, repoRoot, baseHead, branchHead);
|
|
601
|
+
if (converge.converged) {
|
|
602
|
+
gitlinkResolutions = converge.resolutions;
|
|
603
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'passed', syncStarted, {
|
|
604
|
+
reason: 'submodule_diverged_auto_rebased',
|
|
605
|
+
gitlinks: converge.gitlinks,
|
|
606
|
+
});
|
|
607
|
+
LOG.info('Mesh', `[Refinery] Auto-converged diverged submodule gitlink(s) onto base for node ${node.id}: `
|
|
608
|
+
+ converge.resolutions.map(r => `${r.path}→${r.rebasedCommit.slice(0, 12)}`).join(', '));
|
|
609
|
+
// Fall through (do NOT return) → the gitlink-aware root rebase below runs.
|
|
610
|
+
} else {
|
|
611
|
+
// Fail-safe: convergence declined (conflict / unreachable / not a real
|
|
612
|
+
// divergence) → preserve the historical defer→blocked_review behavior.
|
|
613
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'skipped', syncStarted, {
|
|
614
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
615
|
+
convergeReason: converge.reason,
|
|
616
|
+
gitlinks: converge.gitlinks,
|
|
617
|
+
});
|
|
618
|
+
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
619
|
+
ahead: divergence.ahead,
|
|
620
|
+
behind: divergence.behind,
|
|
621
|
+
rebased: false,
|
|
622
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
623
|
+
});
|
|
624
|
+
return { kind: 'continue', ctx };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
583
627
|
} catch { /* fail-open: on gate error, fall through to the rebase */ }
|
|
584
628
|
|
|
585
629
|
// behind>0: strictly-behind OR diverged. Rebase the branch onto the pinned
|
|
586
630
|
// baseHead. A conflict aborts and terminates blocked_review (retryable=false —
|
|
587
631
|
// a real content conflict needs human resolution, not a base-movement retry).
|
|
632
|
+
//
|
|
633
|
+
// When STEP 1 converged a diverged submodule gitlink, use the gitlink-aware
|
|
634
|
+
// root rebase (STEP 2): git's recursive merge refuses to auto-merge the still-
|
|
635
|
+
// diverged intermediate gitlink, so we drive the rebase and resolve each
|
|
636
|
+
// submodule-gitlink conflict to the converged commit. A non-gitlink conflict
|
|
637
|
+
// aborts and falls through to the same blocked_review handling as a plain
|
|
638
|
+
// rebase conflict below (via the thrown gitlinkRebaseError).
|
|
588
639
|
const rebaseStarted = Date.now();
|
|
589
640
|
try {
|
|
590
|
-
|
|
641
|
+
if (gitlinkResolutions.length > 0) {
|
|
642
|
+
const gitlinkRebase = rootRebaseResolvingGitlinks(node.workspace, baseHead, gitlinkResolutions);
|
|
643
|
+
if (!gitlinkRebase.ok) {
|
|
644
|
+
// Surface as a rebase failure so the shared blocked_review handling
|
|
645
|
+
// (submodule-hint recovery included) runs — nothing was left mid-rebase
|
|
646
|
+
// (rootRebaseResolvingGitlinks aborts on failure).
|
|
647
|
+
const err: any = new Error(`gitlink-aware rebase aborted: ${gitlinkRebase.reason || 'unknown'}`);
|
|
648
|
+
err.gitlinkRebaseReason = gitlinkRebase.reason;
|
|
649
|
+
err.gitlinkRebaseConflicts = gitlinkRebase.conflictPaths;
|
|
650
|
+
err.alreadyAborted = true;
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
655
|
+
}
|
|
591
656
|
} catch (rebaseErr: any) {
|
|
592
|
-
|
|
657
|
+
if (!rebaseErr?.alreadyAborted) {
|
|
658
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
659
|
+
}
|
|
593
660
|
// A rebase conflict on a submodule/gitlink divergence is a SPECIAL case the
|
|
594
661
|
// patch-equivalence gate describes with a rich actionable hint (which
|
|
595
662
|
// submodule, base vs branch commit, how to resolve). Run that gate against
|
package/src/commands/router.ts
CHANGED
|
@@ -227,6 +227,14 @@ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
|
|
|
227
227
|
// MUTATES the worker PTY, so forwarding to the real owner (not a wrong local session) is
|
|
228
228
|
// doubly important. The daemon re-enforces the destructive-key confirm gate after the forward.
|
|
229
229
|
'send_keys',
|
|
230
|
+
// interactive_prompt_response (mesh_answer_question, mission f1d25e11): the coordinator
|
|
231
|
+
// answers a REMOTE worker's AskUserQuestion (waiting_choice). The answer must reach the
|
|
232
|
+
// OWNING worker session's live instance — its activeInteractivePrompt (the authoritative
|
|
233
|
+
// prompt the labels/indexes resolve against) and its adapter.setInteractivePromptResponse
|
|
234
|
+
// live only there. Without forwarding, the coordinator's local high-family handler returns
|
|
235
|
+
// 'No running instance for session …' and the question is never answered — the exact
|
|
236
|
+
// remote-worker forwarding gap of mission 6938892f, now closed for questions too.
|
|
237
|
+
'interactive_prompt_response',
|
|
230
238
|
]);
|
|
231
239
|
|
|
232
240
|
function normalizeCommandSource(source: string): CommandLogEntry['source'] {
|