@adhdev/daemon-core 0.9.82-rc.209 → 0.9.82-rc.210
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/terminal-backends/ghostty-vt-backend.d.ts +2 -0
- package/dist/commands/router.d.ts +6 -0
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5699 -3486
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5679 -3483
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +6 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -198
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +19 -0
- package/dist/mesh/mesh-missions.d.ts +58 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +175 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +82 -0
- package/dist/mesh/refine-config.d.ts +24 -2
- package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/spec/driver.d.ts +8 -0
- package/dist/providers/spec/evaluator.d.ts +4 -5
- package/dist/providers/spec/loader.d.ts +1 -0
- package/dist/providers/spec/schema.gen.d.ts +1409 -6
- package/dist/providers/spec/types.d.ts +188 -175
- package/dist/repo-mesh-types.d.ts +1 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
- package/src/commands/router.ts +594 -66
- package/src/git/git-commands.ts +5 -5
- package/src/git/git-diff.ts +53 -0
- package/src/index.ts +11 -5
- package/src/mesh/coordinator-prompt.ts +14 -1
- package/src/mesh/mesh-delivery-policy.ts +17 -0
- package/src/mesh/mesh-events-coordinator.ts +1404 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +161 -0
- package/src/mesh/mesh-events.ts +27 -2143
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +134 -2
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +539 -3
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +233 -17
- package/src/mesh/refine-config.ts +42 -5
- package/src/mesh/worktree-bootstrap-config.ts +79 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/cli-provider-instance.ts +34 -13
- package/src/providers/spec/driver.ts +57 -29
- package/src/providers/spec/evaluator.ts +302 -112
- package/src/providers/spec/loader.ts +226 -37
- package/src/providers/spec/schema.gen.ts +450 -334
- package/src/providers/spec/schema.json +162 -75
- package/src/providers/spec/types.ts +234 -183
- package/src/repo-mesh-types.ts +1 -0
package/src/commands/router.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
import {
|
|
57
57
|
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
58
58
|
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
59
|
+
evaluateWorktreeBootstrapState,
|
|
59
60
|
loadMeshWorktreeBootstrapConfig,
|
|
60
61
|
runMeshWorktreeBootstrap,
|
|
61
62
|
type WorktreeBootstrapState,
|
|
@@ -940,6 +941,7 @@ function finalizeMeshNodeStatus(args: {
|
|
|
940
941
|
status.launchBlockedReason = 'worktree_bootstrap_failed';
|
|
941
942
|
status.launchBlockedMessage = readStringValue(bootstrap.error)
|
|
942
943
|
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
944
|
+
status.recoveryHint = 'Run retry_mesh_node_bootstrap to retry';
|
|
943
945
|
return;
|
|
944
946
|
}
|
|
945
947
|
if (bootstrap.status === 'running' && bootstrap.required !== false) {
|
|
@@ -1306,6 +1308,27 @@ type MeshRefineValidationSummary = {
|
|
|
1306
1308
|
configSourceType?: string;
|
|
1307
1309
|
suggestions?: unknown[];
|
|
1308
1310
|
suggestedConfig?: unknown;
|
|
1311
|
+
/**
|
|
1312
|
+
* M2-3: the bootstrap stage recorded separately from validation so review
|
|
1313
|
+
* surfaces can distinguish environment failures from validation failures.
|
|
1314
|
+
* cached — worktree_bootstrap was 'ready' (staleInputs unchanged), skipped
|
|
1315
|
+
* ran — worktree_bootstrap was stale/never-ran and re-ran successfully
|
|
1316
|
+
* failed — bootstrap run failed (refine stops before validation)
|
|
1317
|
+
* skipped — refine config validation.bootstrap === 'skip'
|
|
1318
|
+
* legacy — deprecated validation.bootstrapCommands path was used
|
|
1319
|
+
* not_configured — no bootstrap definition anywhere
|
|
1320
|
+
*/
|
|
1321
|
+
bootstrap?: {
|
|
1322
|
+
stage: 'cached' | 'ran' | 'failed' | 'skipped' | 'legacy' | 'not_configured';
|
|
1323
|
+
status?: string;
|
|
1324
|
+
skipped?: boolean;
|
|
1325
|
+
configSource?: string;
|
|
1326
|
+
staleReason?: string;
|
|
1327
|
+
error?: string;
|
|
1328
|
+
commandsRun?: Array<Record<string, unknown>>;
|
|
1329
|
+
};
|
|
1330
|
+
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
1331
|
+
deprecationWarnings?: string[];
|
|
1309
1332
|
};
|
|
1310
1333
|
|
|
1311
1334
|
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
@@ -1404,6 +1427,12 @@ type MeshRefineJobHandle = {
|
|
|
1404
1427
|
completedAt?: string;
|
|
1405
1428
|
duplicate?: boolean;
|
|
1406
1429
|
retryOfJobId?: string;
|
|
1430
|
+
/**
|
|
1431
|
+
* The coordinator daemon ID that initiated this refine job.
|
|
1432
|
+
* When set, events for this job are scoped to that coordinator's
|
|
1433
|
+
* pending-events queue instead of the shared broadcast queue.
|
|
1434
|
+
*/
|
|
1435
|
+
targetCoordinatorDaemonId?: string;
|
|
1407
1436
|
eventDelivery: {
|
|
1408
1437
|
pendingEvents: true;
|
|
1409
1438
|
ledger: true;
|
|
@@ -1919,7 +1948,16 @@ function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<str
|
|
|
1919
1948
|
};
|
|
1920
1949
|
}
|
|
1921
1950
|
|
|
1922
|
-
async function runMeshRefineValidationGate(
|
|
1951
|
+
async function runMeshRefineValidationGate(
|
|
1952
|
+
mesh: any,
|
|
1953
|
+
workspace: string,
|
|
1954
|
+
opts?: {
|
|
1955
|
+
/** M2-2: persisted node bootstrap state for staleness evaluation. */
|
|
1956
|
+
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
1957
|
+
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
1958
|
+
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
1959
|
+
},
|
|
1960
|
+
): Promise<MeshRefineValidationSummary> {
|
|
1923
1961
|
const { execFile } = await import('node:child_process');
|
|
1924
1962
|
const { promisify } = await import('node:util');
|
|
1925
1963
|
const execFileAsync = promisify(execFile);
|
|
@@ -1937,6 +1975,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1937
1975
|
configSourceType: selection.sourceType,
|
|
1938
1976
|
suggestions: selection.suggestions,
|
|
1939
1977
|
suggestedConfig: selection.suggestedConfig,
|
|
1978
|
+
...(selection.deprecationWarnings.length > 0 ? { deprecationWarnings: selection.deprecationWarnings } : {}),
|
|
1940
1979
|
};
|
|
1941
1980
|
|
|
1942
1981
|
if (!selection.commands.length) {
|
|
@@ -1944,6 +1983,52 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1944
1983
|
return summary;
|
|
1945
1984
|
}
|
|
1946
1985
|
|
|
1986
|
+
// ── M2-2: Bootstrap stage — refine consumes the worktree_bootstrap config
|
|
1987
|
+
// instead of defining its own. Legacy validation.bootstrapCommands run
|
|
1988
|
+
// only when no worktree_bootstrap config exists (deprecation path).
|
|
1989
|
+
let runLegacyBootstrapCommands = selection.bootstrapCommands.length > 0;
|
|
1990
|
+
if (selection.bootstrapMode === 'skip') {
|
|
1991
|
+
summary.bootstrap = { stage: 'skipped', skipped: true };
|
|
1992
|
+
runLegacyBootstrapCommands = false;
|
|
1993
|
+
} else {
|
|
1994
|
+
const wbLoad = loadMeshWorktreeBootstrapConfig(mesh, workspace);
|
|
1995
|
+
const wbUsable = !!wbLoad.config && wbLoad.sourceType !== 'invalid'
|
|
1996
|
+
&& wbLoad.config.enabled !== false && wbLoad.config.runOnClone !== false;
|
|
1997
|
+
if (wbUsable) {
|
|
1998
|
+
runLegacyBootstrapCommands = false; // worktree_bootstrap wins over deprecated bootstrapCommands
|
|
1999
|
+
const evaluated = evaluateWorktreeBootstrapState(mesh, workspace, opts?.persistedBootstrapState);
|
|
2000
|
+
if (evaluated.status === 'ready') {
|
|
2001
|
+
summary.bootstrap = { stage: 'cached', status: 'ready', skipped: true, configSource: evaluated.configSource };
|
|
2002
|
+
} else {
|
|
2003
|
+
const ran = await runMeshWorktreeBootstrap(mesh, workspace);
|
|
2004
|
+
try { opts?.onBootstrapStateChange?.(ran); } catch { /* persistence is best-effort */ }
|
|
2005
|
+
if (ran.status === 'ready') {
|
|
2006
|
+
summary.bootstrap = {
|
|
2007
|
+
stage: 'ran',
|
|
2008
|
+
status: 'ready',
|
|
2009
|
+
configSource: ran.configSource,
|
|
2010
|
+
...(evaluated.staleReason ? { staleReason: evaluated.staleReason } : {}),
|
|
2011
|
+
commandsRun: ran.commandsRun,
|
|
2012
|
+
};
|
|
2013
|
+
} else {
|
|
2014
|
+
summary.bootstrap = {
|
|
2015
|
+
stage: 'failed',
|
|
2016
|
+
status: ran.status,
|
|
2017
|
+
configSource: ran.configSource,
|
|
2018
|
+
error: ran.error,
|
|
2019
|
+
commandsRun: ran.commandsRun,
|
|
2020
|
+
};
|
|
2021
|
+
summary.status = 'failed';
|
|
2022
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
2023
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
2024
|
+
return summary;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
} else if (!runLegacyBootstrapCommands) {
|
|
2028
|
+
summary.bootstrap = { stage: 'not_configured' };
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1947
2032
|
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1948
2033
|
command: candidate.command,
|
|
1949
2034
|
args: candidate.args,
|
|
@@ -1969,30 +2054,34 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1969
2054
|
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1970
2055
|
};
|
|
1971
2056
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2057
|
+
if (runLegacyBootstrapCommands) {
|
|
2058
|
+
summary.bootstrap = { stage: 'legacy' };
|
|
2059
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
2060
|
+
const startedAt = Date.now();
|
|
2061
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
2062
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
2063
|
+
try {
|
|
2064
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
2065
|
+
cwd,
|
|
2066
|
+
encoding: 'utf8',
|
|
2067
|
+
timeout,
|
|
2068
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
2069
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
2070
|
+
});
|
|
2071
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
2072
|
+
} catch (error: any) {
|
|
2073
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
2074
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
2075
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
2076
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
2077
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
2078
|
+
}));
|
|
2079
|
+
summary.bootstrap = { stage: 'failed', error: String(error?.message || error) };
|
|
2080
|
+
summary.status = 'failed';
|
|
2081
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
2082
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
2083
|
+
return summary;
|
|
2084
|
+
}
|
|
1996
2085
|
}
|
|
1997
2086
|
}
|
|
1998
2087
|
|
|
@@ -2000,7 +2089,8 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
2000
2089
|
const startedAt = Date.now();
|
|
2001
2090
|
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
2002
2091
|
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
2003
|
-
|
|
2092
|
+
const bootstrapProvidedDependencies = summary.bootstrap?.stage === 'cached' || summary.bootstrap?.stage === 'ran' || summary.bootstrap?.stage === 'legacy';
|
|
2093
|
+
if (!bootstrapProvidedDependencies && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
2004
2094
|
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
2005
2095
|
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
2006
2096
|
}, false, {
|
|
@@ -2599,6 +2689,7 @@ export class DaemonCommandRouter {
|
|
|
2599
2689
|
mesh: any;
|
|
2600
2690
|
node: any;
|
|
2601
2691
|
nodeId: string;
|
|
2692
|
+
force?: boolean;
|
|
2602
2693
|
}): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown> } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
|
|
2603
2694
|
const workspace = typeof args.node?.workspace === 'string' ? args.node.workspace.trim() : '';
|
|
2604
2695
|
if (!workspace) {
|
|
@@ -2675,15 +2766,13 @@ export class DaemonCommandRouter {
|
|
|
2675
2766
|
};
|
|
2676
2767
|
}
|
|
2677
2768
|
|
|
2678
|
-
const forceFallbackConvergence =
|
|
2679
|
-
|
|
2680
|
-
workspace,
|
|
2681
|
-
node: args.node,
|
|
2682
|
-
});
|
|
2769
|
+
const forceFallbackConvergence = args.force
|
|
2770
|
+
? { allow: true, status: 'force_override', source: 'caller_force_flag' }
|
|
2771
|
+
: await this.getWorktreeForceCleanupConvergence({ repoRoot, workspace, node: args.node });
|
|
2683
2772
|
|
|
2684
2773
|
try {
|
|
2685
2774
|
const result = await removeWorktree(repoRoot, workspace, {
|
|
2686
|
-
requireClean:
|
|
2775
|
+
requireClean: !args.force,
|
|
2687
2776
|
allowSubmoduleForceFallback: forceFallbackConvergence.allow,
|
|
2688
2777
|
});
|
|
2689
2778
|
return {
|
|
@@ -2714,7 +2803,7 @@ export class DaemonCommandRouter {
|
|
|
2714
2803
|
recoveryHint: dirty
|
|
2715
2804
|
? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
|
|
2716
2805
|
: submoduleForceBlocked
|
|
2717
|
-
? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state
|
|
2806
|
+
? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state, or pass force:true if content is confirmed already in main.'
|
|
2718
2807
|
: 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
|
|
2719
2808
|
...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
|
|
2720
2809
|
};
|
|
@@ -3117,6 +3206,7 @@ export class DaemonCommandRouter {
|
|
|
3117
3206
|
jobId?: string;
|
|
3118
3207
|
interactionId?: string;
|
|
3119
3208
|
retryOfJobId?: string;
|
|
3209
|
+
coordinatorDaemonId?: string;
|
|
3120
3210
|
}): MeshRefineJobHandle {
|
|
3121
3211
|
return {
|
|
3122
3212
|
success: true,
|
|
@@ -3132,6 +3222,7 @@ export class DaemonCommandRouter {
|
|
|
3132
3222
|
startedAt: args.startedAt || new Date().toISOString(),
|
|
3133
3223
|
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
3134
3224
|
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
3225
|
+
...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
|
|
3135
3226
|
eventDelivery: { pendingEvents: true, ledger: true },
|
|
3136
3227
|
evidence: {
|
|
3137
3228
|
pendingEventsCommand: 'get_pending_mesh_events',
|
|
@@ -3164,6 +3255,7 @@ export class DaemonCommandRouter {
|
|
|
3164
3255
|
workspace: handle.workspace,
|
|
3165
3256
|
metadataEvent,
|
|
3166
3257
|
queuedAt: Date.now(),
|
|
3258
|
+
...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
|
|
3167
3259
|
};
|
|
3168
3260
|
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
3169
3261
|
const forwarded = handleMeshForwardEvent(
|
|
@@ -3204,6 +3296,7 @@ export class DaemonCommandRouter {
|
|
|
3204
3296
|
meshId: handle.meshId,
|
|
3205
3297
|
nodeId: handle.targetNodeId,
|
|
3206
3298
|
targetDaemonId: handle.targetDaemonId,
|
|
3299
|
+
targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
3207
3300
|
workspace: handle.workspace,
|
|
3208
3301
|
startedAt: handle.startedAt,
|
|
3209
3302
|
completedAt: handle.completedAt,
|
|
@@ -3223,6 +3316,47 @@ export class DaemonCommandRouter {
|
|
|
3223
3316
|
}
|
|
3224
3317
|
}
|
|
3225
3318
|
|
|
3319
|
+
/**
|
|
3320
|
+
* On daemon restart, scan all mesh ledgers for refine jobs that were dispatched
|
|
3321
|
+
* but never completed/failed (i.e. the daemon died mid-job). Re-queue each one
|
|
3322
|
+
* so the job runs to completion automatically without coordinator intervention.
|
|
3323
|
+
*/
|
|
3324
|
+
async resumePendingRefineJobsOnStartup(): Promise<void> {
|
|
3325
|
+
try {
|
|
3326
|
+
const { listMeshes } = await import('../config/mesh-config.js');
|
|
3327
|
+
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
3328
|
+
const meshIds: string[] = listMeshes().map(m => m.id).filter(Boolean) as string[];
|
|
3329
|
+
for (const meshId of meshIds) {
|
|
3330
|
+
const entries = readLedgerEntries(meshId, { kind: ['task_dispatched', 'task_completed', 'task_failed'] });
|
|
3331
|
+
// Build set of nodeIds that already have a terminal entry.
|
|
3332
|
+
const terminal = new Set<string>();
|
|
3333
|
+
for (const e of entries) {
|
|
3334
|
+
if ((e.kind === 'task_completed' || e.kind === 'task_failed') && e.nodeId) {
|
|
3335
|
+
const jobId = (e.payload as any)?.refineJob?.jobId;
|
|
3336
|
+
if (jobId) terminal.add(`${e.nodeId}:${jobId}`);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
// Re-dispatch dispatched jobs with no matching terminal entry.
|
|
3340
|
+
for (const e of entries) {
|
|
3341
|
+
if (e.kind !== 'task_dispatched' || !e.nodeId) continue;
|
|
3342
|
+
const source = (e.payload as any)?.source;
|
|
3343
|
+
if (source !== 'refine_mesh_node_async_job') continue;
|
|
3344
|
+
const jobId = (e.payload as any)?.refineJob?.jobId;
|
|
3345
|
+
if (!jobId || terminal.has(`${e.nodeId}:${jobId}`)) continue;
|
|
3346
|
+
const key = this.buildRefineJobKey(meshId, e.nodeId);
|
|
3347
|
+
if (this.runningRefineJobs.has(key)) continue;
|
|
3348
|
+
const coordinatorDaemonId = (e.payload as any)?.refineJob?.targetCoordinatorDaemonId;
|
|
3349
|
+
LOG.info('Mesh', `[Refinery] Auto-resuming interrupted refine job for node ${e.nodeId} (jobId=${jobId})`);
|
|
3350
|
+
void this.startMeshRefineJob(meshId, e.nodeId, {
|
|
3351
|
+
coordinatorDaemonId,
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
} catch (e: any) {
|
|
3356
|
+
LOG.warn('Mesh', `[Refinery] resumePendingRefineJobsOnStartup failed: ${e?.message || e}`);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3226
3360
|
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3227
3361
|
const refineStages: Array<Record<string, unknown>> = [];
|
|
3228
3362
|
try {
|
|
@@ -3255,11 +3389,20 @@ export class DaemonCommandRouter {
|
|
|
3255
3389
|
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3256
3390
|
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3257
3391
|
const baseHead = baseHeadStdout.trim();
|
|
3258
|
-
|
|
3392
|
+
let branchHead = branchHeadStdout.trim();
|
|
3259
3393
|
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
3260
3394
|
|
|
3261
3395
|
const validationStarted = Date.now();
|
|
3262
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace
|
|
3396
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
3397
|
+
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
3398
|
+
persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
|
|
3399
|
+
onBootstrapStateChange: (state) => {
|
|
3400
|
+
(node as any).worktreeBootstrap = state;
|
|
3401
|
+
void import('../config/mesh-config.js')
|
|
3402
|
+
.then(({ updateNode }) => updateNode(mesh.id, node.id, { worktreeBootstrap: state } as any))
|
|
3403
|
+
.catch(() => { /* persistence is best-effort */ });
|
|
3404
|
+
},
|
|
3405
|
+
});
|
|
3263
3406
|
recordMeshRefineStage(
|
|
3264
3407
|
refineStages,
|
|
3265
3408
|
'validation',
|
|
@@ -3313,7 +3456,7 @@ export class DaemonCommandRouter {
|
|
|
3313
3456
|
}
|
|
3314
3457
|
|
|
3315
3458
|
const patchEquivalenceStarted = Date.now();
|
|
3316
|
-
|
|
3459
|
+
let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3317
3460
|
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3318
3461
|
equivalent: patchEquivalence.equivalent,
|
|
3319
3462
|
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
@@ -3322,26 +3465,171 @@ export class DaemonCommandRouter {
|
|
|
3322
3465
|
actionableHint: patchEquivalence.actionableHint,
|
|
3323
3466
|
});
|
|
3324
3467
|
if (!patchEquivalence.equivalent) {
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3468
|
+
// Auto-rebase: if branch is simply behind base, attempt rebase automatically before failing.
|
|
3469
|
+
let didAutoRebase = false;
|
|
3470
|
+
let isBehindBase = false;
|
|
3471
|
+
try {
|
|
3472
|
+
execFileSync('git', ['merge-base', '--is-ancestor', branchHead, baseHead], {
|
|
3473
|
+
cwd: node.workspace,
|
|
3474
|
+
stdio: 'ignore',
|
|
3475
|
+
});
|
|
3476
|
+
isBehindBase = true;
|
|
3477
|
+
} catch { /* non-zero exit means branchHead is not an ancestor of baseHead */ }
|
|
3478
|
+
|
|
3479
|
+
if (isBehindBase) {
|
|
3480
|
+
const autoRebaseStarted = Date.now();
|
|
3481
|
+
try {
|
|
3482
|
+
execFileSync('git', ['rebase', baseHead], {
|
|
3483
|
+
cwd: node.workspace,
|
|
3484
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3485
|
+
});
|
|
3486
|
+
const { stdout: rebasedHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: node.workspace, encoding: 'utf8' });
|
|
3487
|
+
branchHead = rebasedHeadStdout.trim();
|
|
3488
|
+
const rebasedPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3489
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', rebasedPatchEquivalence.status, autoRebaseStarted, {
|
|
3490
|
+
equivalent: rebasedPatchEquivalence.equivalent,
|
|
3491
|
+
expectedPatchId: rebasedPatchEquivalence.expectedPatchId,
|
|
3492
|
+
actualPatchId: rebasedPatchEquivalence.actualPatchId,
|
|
3493
|
+
error: rebasedPatchEquivalence.error,
|
|
3494
|
+
rebasedBranchHead: branchHead,
|
|
3495
|
+
});
|
|
3496
|
+
if (rebasedPatchEquivalence.equivalent) {
|
|
3497
|
+
patchEquivalence = rebasedPatchEquivalence;
|
|
3498
|
+
didAutoRebase = true;
|
|
3499
|
+
} else {
|
|
3500
|
+
return {
|
|
3501
|
+
success: false,
|
|
3502
|
+
code: 'needs_rebase',
|
|
3503
|
+
convergenceStatus: 'blocked_review',
|
|
3504
|
+
error: 'Branch was rebased onto base but patch equivalence still failed; manual intervention required.',
|
|
3505
|
+
branch,
|
|
3506
|
+
into: baseBranch,
|
|
3507
|
+
validationSummary,
|
|
3508
|
+
patchEquivalence: rebasedPatchEquivalence,
|
|
3509
|
+
refineStages,
|
|
3510
|
+
finalBranchConvergenceState: {
|
|
3511
|
+
branch,
|
|
3512
|
+
baseBranch,
|
|
3513
|
+
merged: false,
|
|
3514
|
+
removed: false,
|
|
3515
|
+
validation: 'passed',
|
|
3516
|
+
patchEquivalence: 'failed',
|
|
3517
|
+
status: 'blocked_review',
|
|
3518
|
+
},
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
} catch (rebaseErr: any) {
|
|
3522
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
3523
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
|
|
3524
|
+
error: rebaseErr?.message || String(rebaseErr),
|
|
3525
|
+
});
|
|
3526
|
+
return {
|
|
3527
|
+
success: false,
|
|
3528
|
+
code: 'needs_rebase_with_conflicts',
|
|
3529
|
+
convergenceStatus: 'blocked_review',
|
|
3530
|
+
error: 'Branch is behind base and auto-rebase failed due to conflicts; resolve conflicts manually and retry.',
|
|
3531
|
+
branch,
|
|
3532
|
+
into: baseBranch,
|
|
3533
|
+
validationSummary,
|
|
3534
|
+
patchEquivalence,
|
|
3535
|
+
refineStages,
|
|
3536
|
+
finalBranchConvergenceState: {
|
|
3537
|
+
branch,
|
|
3538
|
+
baseBranch,
|
|
3539
|
+
merged: false,
|
|
3540
|
+
removed: false,
|
|
3541
|
+
validation: 'passed',
|
|
3542
|
+
patchEquivalence: 'failed',
|
|
3543
|
+
status: 'blocked_review',
|
|
3544
|
+
},
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// If the actual patch-id is empty, the merge-tree produces no diff vs base —
|
|
3550
|
+
// meaning the branch content is already present in base (landed via a different
|
|
3551
|
+
// path, e.g. cherry-pick or direct commit). Treat this as "already merged":
|
|
3552
|
+
// skip the merge step but still run cleanup so the worktree node is removed.
|
|
3553
|
+
// "already merged via another path": the branch has real changes
|
|
3554
|
+
// (expectedPatchId non-empty) but the merge-tree produces no diff
|
|
3555
|
+
// against base (actualPatchId empty) — meaning every change in the
|
|
3556
|
+
// branch is already present in base via a cherry-pick or direct commit.
|
|
3557
|
+
// If both patch-ids are empty, the branch itself has no changes; that
|
|
3558
|
+
// is a degenerate worktree case, not an "already merged" scenario.
|
|
3559
|
+
const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
|
|
3560
|
+
if (!didAutoRebase && !alreadyMergedViaOtherPath) {
|
|
3561
|
+
return {
|
|
3562
|
+
success: false,
|
|
3563
|
+
code: 'patch_equivalence_failed',
|
|
3564
|
+
convergenceStatus: 'blocked_review',
|
|
3565
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3566
|
+
branch,
|
|
3567
|
+
into: baseBranch,
|
|
3568
|
+
validationSummary,
|
|
3569
|
+
patchEquivalence,
|
|
3570
|
+
refineStages,
|
|
3571
|
+
finalBranchConvergenceState: {
|
|
3572
|
+
branch,
|
|
3573
|
+
baseBranch,
|
|
3574
|
+
merged: false,
|
|
3575
|
+
removed: false,
|
|
3576
|
+
validation: 'passed',
|
|
3577
|
+
patchEquivalence: 'failed',
|
|
3578
|
+
status: 'blocked_review',
|
|
3579
|
+
},
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
if (!didAutoRebase && alreadyMergedViaOtherPath) {
|
|
3584
|
+
// Content already in base — skip merge, go straight to cleanup.
|
|
3585
|
+
recordMeshRefineStage(refineStages, 'merge', 'skipped', Date.now(), {
|
|
3586
|
+
reason: 'already_merged_via_other_path',
|
|
3587
|
+
note: 'actualPatchId is empty; branch content is already present in base via a different commit path',
|
|
3588
|
+
});
|
|
3589
|
+
const cleanupStarted = Date.now();
|
|
3590
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
3591
|
+
meshId,
|
|
3592
|
+
nodeId,
|
|
3593
|
+
sessionCleanupMode: 'preserve',
|
|
3594
|
+
inlineMesh: args?.inlineMesh,
|
|
3595
|
+
});
|
|
3596
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3597
|
+
removed: removeResult?.removed,
|
|
3598
|
+
code: removeResult?.code,
|
|
3599
|
+
error: removeResult?.error,
|
|
3600
|
+
});
|
|
3601
|
+
try {
|
|
3602
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3603
|
+
appendLedgerEntry(meshId, {
|
|
3604
|
+
kind: 'node_removed',
|
|
3605
|
+
nodeId,
|
|
3606
|
+
payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
3607
|
+
});
|
|
3608
|
+
} catch { /* ledger append is best-effort */ }
|
|
3609
|
+
return {
|
|
3610
|
+
success: removeResult?.success !== false,
|
|
3611
|
+
code: 'already_merged',
|
|
3612
|
+
merged: false,
|
|
3613
|
+
alreadyMergedViaOtherPath: true,
|
|
3614
|
+
branch,
|
|
3615
|
+
into: baseBranch,
|
|
3616
|
+
removeResult,
|
|
3617
|
+
validationSummary,
|
|
3618
|
+
patchEquivalence,
|
|
3619
|
+
refineStages,
|
|
3620
|
+
finalBranchConvergenceState: {
|
|
3621
|
+
branch: baseBranch,
|
|
3622
|
+
mergedBranch: branch,
|
|
3623
|
+
baseBranch,
|
|
3624
|
+
merged: false,
|
|
3625
|
+
alreadyMergedViaOtherPath: true,
|
|
3626
|
+
removed: removeResult?.success !== false,
|
|
3627
|
+
validation: 'passed',
|
|
3628
|
+
patchEquivalence: 'already_merged',
|
|
3629
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
|
|
3630
|
+
},
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3345
3633
|
}
|
|
3346
3634
|
|
|
3347
3635
|
const submoduleReachabilityStarted = Date.now();
|
|
@@ -3620,23 +3908,61 @@ export class DaemonCommandRouter {
|
|
|
3620
3908
|
result = { success: false, error: e?.message || String(e) };
|
|
3621
3909
|
}
|
|
3622
3910
|
const completedAt = new Date().toISOString();
|
|
3911
|
+
|
|
3912
|
+
// B1: Discriminated terminal status — do not rely solely on result.success.
|
|
3913
|
+
// Map known failure codes to structured terminal kinds.
|
|
3914
|
+
type RefineTerminalKind = 'completed' | 'blocked_review' | 'validation_failed' | 'submodule_reachability_failed' | 'merge_failed' | 'cleanup_failed';
|
|
3915
|
+
const refineCode = typeof result.code === 'string' ? result.code : '';
|
|
3916
|
+
const refineTerminalKind: RefineTerminalKind = result.success === true
|
|
3917
|
+
? 'completed'
|
|
3918
|
+
: refineCode === 'blocked_review'
|
|
3919
|
+
? 'blocked_review'
|
|
3920
|
+
: refineCode === 'validation_failed' || refineCode === 'validation_dependencies_missing'
|
|
3921
|
+
? 'validation_failed'
|
|
3922
|
+
: refineCode === 'submodule_reachability_failed'
|
|
3923
|
+
? 'submodule_reachability_failed'
|
|
3924
|
+
: refineCode === 'merge_failed' || refineCode === 'patch_equivalence_failed' || refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts'
|
|
3925
|
+
? 'merge_failed'
|
|
3926
|
+
: refineCode === 'cleanup_failed'
|
|
3927
|
+
? 'cleanup_failed'
|
|
3928
|
+
: 'merge_failed'; // fallback for unclassified failures
|
|
3929
|
+
const isTerminalSuccess = refineTerminalKind === 'completed';
|
|
3930
|
+
const normalizedResult = {
|
|
3931
|
+
...result,
|
|
3932
|
+
terminalKind: refineTerminalKind,
|
|
3933
|
+
...(result.nextStep === undefined && !isTerminalSuccess ? {
|
|
3934
|
+
nextStep: refineTerminalKind === 'blocked_review'
|
|
3935
|
+
? 'Request user review/approval before attempting to merge again.'
|
|
3936
|
+
: refineTerminalKind === 'validation_failed'
|
|
3937
|
+
? 'Fix failing tests or configure validation.bootstrapCommands and retry mesh_refine_node.'
|
|
3938
|
+
: refineTerminalKind === 'submodule_reachability_failed'
|
|
3939
|
+
? 'Push unreachable submodule commits to origin/main, then retry mesh_refine_node.'
|
|
3940
|
+
: refineTerminalKind === 'merge_failed'
|
|
3941
|
+
? 'Resolve merge conflicts or patch equivalence issues, then retry mesh_refine_node.'
|
|
3942
|
+
: refineTerminalKind === 'cleanup_failed'
|
|
3943
|
+
? 'Manually remove the worktree and retry or use mesh_remove_node.'
|
|
3944
|
+
: 'Inspect refineStages for the failing stage and retry.',
|
|
3945
|
+
} : {}),
|
|
3946
|
+
};
|
|
3947
|
+
|
|
3623
3948
|
const terminalHandle = this.buildRefineJobHandle({
|
|
3624
3949
|
meshId: handle.meshId,
|
|
3625
3950
|
nodeId: handle.targetNodeId,
|
|
3626
|
-
status:
|
|
3951
|
+
status: isTerminalSuccess ? 'completed' : 'failed',
|
|
3627
3952
|
startedAt: handle.startedAt,
|
|
3628
3953
|
completedAt,
|
|
3629
3954
|
jobId: handle.jobId,
|
|
3630
3955
|
interactionId: handle.interactionId,
|
|
3631
3956
|
retryOfJobId: handle.retryOfJobId,
|
|
3632
3957
|
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
3958
|
+
coordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
3633
3959
|
});
|
|
3634
|
-
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
3960
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result: normalizedResult };
|
|
3635
3961
|
this.terminalRefineJobs.set(key, terminal);
|
|
3636
3962
|
this.runningRefineJobs.delete(key);
|
|
3637
3963
|
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
3638
|
-
await this.appendRefineJobLedger(
|
|
3639
|
-
this.queueRefineJobEvent(
|
|
3964
|
+
await this.appendRefineJobLedger(isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
|
|
3965
|
+
this.queueRefineJobEvent(isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
|
|
3640
3966
|
}
|
|
3641
3967
|
|
|
3642
3968
|
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
@@ -3651,7 +3977,12 @@ export class DaemonCommandRouter {
|
|
|
3651
3977
|
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
3652
3978
|
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
3653
3979
|
|
|
3654
|
-
|
|
3980
|
+
// Capture the caller's coordinator daemon ID so completed/failed events are
|
|
3981
|
+
// scoped to that coordinator's pending-events queue and survive daemon restarts.
|
|
3982
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
3983
|
+
? args.coordinatorDaemonId.trim()
|
|
3984
|
+
: (this.deps.statusInstanceId || undefined);
|
|
3985
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId, coordinatorDaemonId });
|
|
3655
3986
|
this.runningRefineJobs.set(key, handle);
|
|
3656
3987
|
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
3657
3988
|
this.queueRefineJobEvent('refine:accepted', handle);
|
|
@@ -4815,11 +5146,17 @@ export class DaemonCommandRouter {
|
|
|
4815
5146
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4816
5147
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4817
5148
|
try {
|
|
4818
|
-
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
5149
|
+
const { getMeshQueueStats, getQueue, describeTaskDependencyState } = await import('../mesh/mesh-work-queue.js');
|
|
4819
5150
|
const status = Array.isArray(args?.status)
|
|
4820
5151
|
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
4821
5152
|
: undefined;
|
|
4822
|
-
const
|
|
5153
|
+
const rawQueue = getQueue(meshId, { status: status as any });
|
|
5154
|
+
// M1: annotate dependency state at view time (waitingOn / dependenciesSatisfied).
|
|
5155
|
+
const statusById = new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
5156
|
+
const queue = rawQueue.map(task =>
|
|
5157
|
+
Array.isArray(task.dependsOn) && task.dependsOn.length > 0
|
|
5158
|
+
? { ...task, ...describeTaskDependencyState(task, statusById) }
|
|
5159
|
+
: task);
|
|
4823
5160
|
const summary = getMeshQueueStats(meshId);
|
|
4824
5161
|
return {
|
|
4825
5162
|
success: true,
|
|
@@ -5105,7 +5442,7 @@ export class DaemonCommandRouter {
|
|
|
5105
5442
|
|
|
5106
5443
|
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
5107
5444
|
if (node?.isLocalWorktree) {
|
|
5108
|
-
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
|
|
5445
|
+
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
|
|
5109
5446
|
if (cleanupResult.success === false) {
|
|
5110
5447
|
return {
|
|
5111
5448
|
success: false,
|
|
@@ -5281,6 +5618,40 @@ export class DaemonCommandRouter {
|
|
|
5281
5618
|
{ timeoutMs: 120000 },
|
|
5282
5619
|
);
|
|
5283
5620
|
submodulesInitialized = true;
|
|
5621
|
+
|
|
5622
|
+
// Sync oss submodule to source node HEAD (best-effort)
|
|
5623
|
+
const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
|
|
5624
|
+
if (sourceWorkspace) {
|
|
5625
|
+
try {
|
|
5626
|
+
const { runGit: rg } = await import('../git/git-executor.js');
|
|
5627
|
+
const sourceCtx = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
5628
|
+
const worktreeCtx = { workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true };
|
|
5629
|
+
|
|
5630
|
+
// Read source node's oss submodule SHA
|
|
5631
|
+
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', 'oss'], { timeoutMs: 10000 });
|
|
5632
|
+
const sourceStatusLine = (typeof sourceStatusOut === 'string' ? sourceStatusOut : (sourceStatusOut as any)?.stdout ?? '').trim();
|
|
5633
|
+
const sourceShaMatch = sourceStatusLine.match(/^[+\- ]?([0-9a-f]{40})/);
|
|
5634
|
+
const sourceSha = sourceShaMatch?.[1];
|
|
5635
|
+
|
|
5636
|
+
if (sourceSha) {
|
|
5637
|
+
// Read worktree's current oss HEAD
|
|
5638
|
+
const ossCtx = { workspace: `${result.worktreePath}/oss`, repoRoot: `${result.worktreePath}/oss`, isGitRepo: true };
|
|
5639
|
+
const worktreeOssHeadOut = await rg(ossCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 });
|
|
5640
|
+
const worktreeOssSha = (typeof worktreeOssHeadOut === 'string' ? worktreeOssHeadOut : (worktreeOssHeadOut as any)?.stdout ?? '').trim();
|
|
5641
|
+
|
|
5642
|
+
if (worktreeOssSha !== sourceSha) {
|
|
5643
|
+
// Fetch target SHA from source node's oss directory
|
|
5644
|
+
await rg(ossCtx, ['fetch', `${sourceWorkspace}/oss`, 'HEAD'], { timeoutMs: 60000 });
|
|
5645
|
+
await rg(ossCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
5646
|
+
await rg(worktreeCtx, ['add', 'oss'], { timeoutMs: 10000 });
|
|
5647
|
+
await rg(worktreeCtx, ['commit', '-m', 'chore: sync oss to source node HEAD on clone'], { timeoutMs: 10000 });
|
|
5648
|
+
console.log(`[mesh] Synced oss submodule to source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
} catch (ossErr: any) {
|
|
5652
|
+
console.warn('[mesh] oss submodule sync to source HEAD failed (best-effort):', ossErr.message);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5284
5655
|
} catch (subErr: any) {
|
|
5285
5656
|
// Submodule init is best-effort; don't fail the clone
|
|
5286
5657
|
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
@@ -5342,6 +5713,63 @@ export class DaemonCommandRouter {
|
|
|
5342
5713
|
return { success: false, error: e.message };
|
|
5343
5714
|
}
|
|
5344
5715
|
}
|
|
5716
|
+
case 'retry_mesh_node_bootstrap': {
|
|
5717
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5718
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
5719
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
5720
|
+
if (!nodeId) return { success: false, error: 'nodeId required' };
|
|
5721
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
|
|
5722
|
+
if (ownerFailure) return ownerFailure;
|
|
5723
|
+
|
|
5724
|
+
try {
|
|
5725
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
5726
|
+
const mesh = meshRecord?.mesh;
|
|
5727
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5728
|
+
|
|
5729
|
+
const node = mesh.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
5730
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
5731
|
+
if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
|
|
5732
|
+
|
|
5733
|
+
const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
|
|
5734
|
+
if (currentBootstrap?.status === 'running') {
|
|
5735
|
+
return { success: false, error: 'Bootstrap is already running for this node' };
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5738
|
+
const worktreePath: string = node.workspace || node.repoRoot;
|
|
5739
|
+
if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
|
|
5740
|
+
|
|
5741
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
|
|
5742
|
+
const runningState: WorktreeBootstrapState = {
|
|
5743
|
+
status: 'running',
|
|
5744
|
+
required: loadedBootstrap.config?.required !== false,
|
|
5745
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
5746
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
5747
|
+
startedAt: new Date().toISOString(),
|
|
5748
|
+
};
|
|
5749
|
+
|
|
5750
|
+
const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5751
|
+
node.worktreeBootstrap = bootstrapState;
|
|
5752
|
+
if (meshRecord.inline) {
|
|
5753
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
5754
|
+
return;
|
|
5755
|
+
}
|
|
5756
|
+
try {
|
|
5757
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
5758
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
5759
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
5760
|
+
} catch { /* best-effort */ }
|
|
5761
|
+
};
|
|
5762
|
+
|
|
5763
|
+
await persistState(runningState);
|
|
5764
|
+
const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
|
|
5765
|
+
await persistState(bootstrapState);
|
|
5766
|
+
|
|
5767
|
+
return { success: true, bootstrapState };
|
|
5768
|
+
} catch (e: any) {
|
|
5769
|
+
return { success: false, error: e.message };
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5345
5773
|
case 'trigger_mesh_queue': {
|
|
5346
5774
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5347
5775
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -5377,6 +5805,12 @@ export class DaemonCommandRouter {
|
|
|
5377
5805
|
|
|
5378
5806
|
try {
|
|
5379
5807
|
const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
|
|
5808
|
+
const { buildMissionPromptSection } = await import('../mesh/mesh-missions.js');
|
|
5809
|
+
// M3-3: inject the active mission summary into the coordinator prompt.
|
|
5810
|
+
// Best-effort — a store failure must not block coordinator launch.
|
|
5811
|
+
const buildMissionSectionBestEffort = (id: string): string => {
|
|
5812
|
+
try { return buildMissionPromptSection(id); } catch { return ''; }
|
|
5813
|
+
};
|
|
5380
5814
|
|
|
5381
5815
|
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
5382
5816
|
let mesh: any;
|
|
@@ -5483,7 +5917,7 @@ export class DaemonCommandRouter {
|
|
|
5483
5917
|
// Build coordinator prompt first — fail closed on errors.
|
|
5484
5918
|
let cliCmdSystemPrompt = '';
|
|
5485
5919
|
try {
|
|
5486
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
5920
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
5487
5921
|
} catch (error: any) {
|
|
5488
5922
|
const message = error?.message || String(error);
|
|
5489
5923
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -5701,7 +6135,7 @@ export class DaemonCommandRouter {
|
|
|
5701
6135
|
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
5702
6136
|
let systemPrompt = '';
|
|
5703
6137
|
try {
|
|
5704
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
6138
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined, missionSection: buildMissionSectionBestEffort(mesh.id) });
|
|
5705
6139
|
} catch (error: any) {
|
|
5706
6140
|
const message = error?.message || String(error);
|
|
5707
6141
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -6346,6 +6780,16 @@ export class DaemonCommandRouter {
|
|
|
6346
6780
|
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
6347
6781
|
...(historicalSessions ? { historicalSessions } : {}),
|
|
6348
6782
|
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
6783
|
+
activeRefineJobs: Array.from(this.runningRefineJobs.values())
|
|
6784
|
+
.filter(job => job.meshId === meshId)
|
|
6785
|
+
.map(job => ({
|
|
6786
|
+
jobId: job.jobId,
|
|
6787
|
+
nodeId: job.targetNodeId,
|
|
6788
|
+
workspace: job.workspace,
|
|
6789
|
+
startedAt: job.startedAt,
|
|
6790
|
+
status: job.status,
|
|
6791
|
+
targetCoordinatorDaemonId: job.targetCoordinatorDaemonId,
|
|
6792
|
+
})),
|
|
6349
6793
|
};
|
|
6350
6794
|
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
6351
6795
|
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
@@ -6367,6 +6811,90 @@ export class DaemonCommandRouter {
|
|
|
6367
6811
|
}
|
|
6368
6812
|
}
|
|
6369
6813
|
|
|
6814
|
+
case 'get_mesh_review_inbox': {
|
|
6815
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
6816
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
6817
|
+
try {
|
|
6818
|
+
const { deriveMeshReviewInboxItems } = await import('../mesh/mesh-review-inbox.js');
|
|
6819
|
+
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
6820
|
+
const { getGitDiffSummary } = await import('../git/git-diff.js');
|
|
6821
|
+
const { existsSync } = await import('node:fs');
|
|
6822
|
+
|
|
6823
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6824
|
+
const mesh = meshRecord?.mesh;
|
|
6825
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6826
|
+
|
|
6827
|
+
// Ensure we have a fresh aggregate status so nodeStatuses carry
|
|
6828
|
+
// computed fields (connection.state, branchConvergence, isLocalWorktree)
|
|
6829
|
+
// that the raw mesh.nodes config objects don't have.
|
|
6830
|
+
// When the caller provides an inlineMesh, prefer its nodes directly
|
|
6831
|
+
// (they already carry the computed fields from the coordinator).
|
|
6832
|
+
const inlineNodes = args?.inlineMesh && Array.isArray((args.inlineMesh as any)?.nodes)
|
|
6833
|
+
? (args.inlineMesh as any).nodes as Record<string, unknown>[]
|
|
6834
|
+
: null;
|
|
6835
|
+
let cachedStatus = !inlineNodes ? this.getCachedAggregateMeshStatus(meshId, mesh, {}) : null;
|
|
6836
|
+
if (!cachedStatus && !inlineNodes) {
|
|
6837
|
+
const freshStatus = await this.execute('mesh_status', {
|
|
6838
|
+
meshId,
|
|
6839
|
+
inlineMesh: args?.inlineMesh,
|
|
6840
|
+
refresh: true,
|
|
6841
|
+
}, 'get_mesh_review_inbox');
|
|
6842
|
+
cachedStatus = (freshStatus?.success !== false) ? freshStatus : null;
|
|
6843
|
+
}
|
|
6844
|
+
const nodeStatuses: Record<string, unknown>[] = inlineNodes
|
|
6845
|
+
? inlineNodes
|
|
6846
|
+
: Array.isArray(cachedStatus?.nodes)
|
|
6847
|
+
? cachedStatus.nodes as Record<string, unknown>[]
|
|
6848
|
+
: Array.isArray(mesh.nodes)
|
|
6849
|
+
? mesh.nodes as Record<string, unknown>[]
|
|
6850
|
+
: [];
|
|
6851
|
+
|
|
6852
|
+
const ledgerEntries = readLedgerEntries(meshId, { tail: 300 });
|
|
6853
|
+
const derivation = deriveMeshReviewInboxItems({ nodes: nodeStatuses, ledgerEntries });
|
|
6854
|
+
|
|
6855
|
+
for (const item of derivation.items) {
|
|
6856
|
+
const workspace = item.workspace;
|
|
6857
|
+
if (!workspace || !existsSync(workspace)) continue;
|
|
6858
|
+
const baseRef = item.defaultBranch
|
|
6859
|
+
? `origin/${item.defaultBranch}`
|
|
6860
|
+
: 'origin/main';
|
|
6861
|
+
try {
|
|
6862
|
+
const diffResult = await getGitDiffSummary(workspace, { baseRef, maxFiles: 100 });
|
|
6863
|
+
if (diffResult.isGitRepo) {
|
|
6864
|
+
item.diffSummary = {
|
|
6865
|
+
baseRef,
|
|
6866
|
+
files: diffResult.files.map(f => ({
|
|
6867
|
+
path: f.path,
|
|
6868
|
+
status: f.status,
|
|
6869
|
+
insertions: f.insertions,
|
|
6870
|
+
deletions: f.deletions,
|
|
6871
|
+
binary: f.binary,
|
|
6872
|
+
oldPath: f.oldPath,
|
|
6873
|
+
})),
|
|
6874
|
+
totalFiles: diffResult.files.length,
|
|
6875
|
+
totalInsertions: diffResult.totalInsertions,
|
|
6876
|
+
totalDeletions: diffResult.totalDeletions,
|
|
6877
|
+
truncated: diffResult.truncated,
|
|
6878
|
+
...(diffResult.error ? { error: diffResult.error } : {}),
|
|
6879
|
+
};
|
|
6880
|
+
}
|
|
6881
|
+
} catch {
|
|
6882
|
+
item.diffSummary = null;
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
|
|
6886
|
+
return {
|
|
6887
|
+
success: true,
|
|
6888
|
+
meshId,
|
|
6889
|
+
inbox: derivation.items,
|
|
6890
|
+
remoteNodesExcluded: derivation.remoteNodesExcluded,
|
|
6891
|
+
excludedRemoteNodeIds: derivation.excludedRemoteNodeIds,
|
|
6892
|
+
};
|
|
6893
|
+
} catch (e: any) {
|
|
6894
|
+
return { success: false, error: e.message };
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6370
6898
|
default:
|
|
6371
6899
|
break;
|
|
6372
6900
|
}
|