@adhdev/daemon-core 0.9.82-rc.89 → 0.9.82-rc.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +179 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +179 -43
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/refine-config.d.ts +45 -0
- package/package.json +1 -1
- package/src/commands/router.ts +174 -43
- package/src/mesh/refine-config.ts +38 -3
|
@@ -21,6 +21,11 @@ export interface RepoMeshRefineConfig {
|
|
|
21
21
|
allowAutoPublishSubmoduleMainCommits?: boolean;
|
|
22
22
|
validation?: {
|
|
23
23
|
required?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Optional dependency/bootstrap commands that Refinery runs before
|
|
26
|
+
* validation commands. Refinery never infers installs on its own.
|
|
27
|
+
*/
|
|
28
|
+
bootstrapCommands?: RepoMeshRefineValidationCommandConfig[];
|
|
24
29
|
commands?: RepoMeshRefineValidationCommandConfig[];
|
|
25
30
|
};
|
|
26
31
|
}
|
|
@@ -44,6 +49,7 @@ export interface MeshRefineConfigLoadResult {
|
|
|
44
49
|
export interface MeshRefineValidationPlan {
|
|
45
50
|
source: string;
|
|
46
51
|
sourceType: MeshRefineConfigLoadResult['sourceType'];
|
|
52
|
+
bootstrapCommands: MeshRefineValidationCommandPlan[];
|
|
47
53
|
commands: MeshRefineValidationCommandPlan[];
|
|
48
54
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
49
55
|
suggestions: RepoMeshRefineValidationCommandConfig[];
|
|
@@ -113,6 +119,44 @@ export declare const MESH_REFINE_CONFIG_SCHEMA: {
|
|
|
113
119
|
};
|
|
114
120
|
};
|
|
115
121
|
};
|
|
122
|
+
readonly bootstrapCommands: {
|
|
123
|
+
readonly type: "array";
|
|
124
|
+
readonly maxItems: 4;
|
|
125
|
+
readonly items: {
|
|
126
|
+
readonly type: "object";
|
|
127
|
+
readonly additionalProperties: false;
|
|
128
|
+
readonly required: readonly ["command"];
|
|
129
|
+
readonly properties: {
|
|
130
|
+
readonly command: {
|
|
131
|
+
readonly type: "string";
|
|
132
|
+
readonly minLength: 1;
|
|
133
|
+
};
|
|
134
|
+
readonly args: {
|
|
135
|
+
readonly type: "array";
|
|
136
|
+
readonly items: {
|
|
137
|
+
readonly type: "string";
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly category: {
|
|
141
|
+
readonly enum: readonly ["typecheck", "test", "lint", "build", "custom"];
|
|
142
|
+
};
|
|
143
|
+
readonly cwd: {
|
|
144
|
+
readonly type: "string";
|
|
145
|
+
};
|
|
146
|
+
readonly timeoutMs: {
|
|
147
|
+
readonly type: "number";
|
|
148
|
+
readonly minimum: 1000;
|
|
149
|
+
readonly maximum: 600000;
|
|
150
|
+
};
|
|
151
|
+
readonly env: {
|
|
152
|
+
readonly type: "object";
|
|
153
|
+
readonly additionalProperties: {
|
|
154
|
+
readonly type: "string";
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
};
|
|
116
160
|
};
|
|
117
161
|
};
|
|
118
162
|
};
|
|
@@ -120,6 +164,7 @@ export declare const MESH_REFINE_CONFIG_SCHEMA: {
|
|
|
120
164
|
export declare function validateMeshRefineConfig(config: unknown, source?: string): {
|
|
121
165
|
valid: boolean;
|
|
122
166
|
errors: string[];
|
|
167
|
+
bootstrapCommands: MeshRefineValidationCommandPlan[];
|
|
123
168
|
commands: MeshRefineValidationCommandPlan[];
|
|
124
169
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
125
170
|
};
|
package/package.json
CHANGED
package/src/commands/router.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
41
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
42
42
|
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
43
43
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
44
44
|
import {
|
|
@@ -1070,8 +1070,11 @@ type MeshRefineValidationSummary = {
|
|
|
1070
1070
|
status: MeshRefineValidationStatus;
|
|
1071
1071
|
required: true;
|
|
1072
1072
|
commandsRun: Array<Record<string, unknown>>;
|
|
1073
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
1073
1074
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
1074
1075
|
skippedReason?: string;
|
|
1076
|
+
failureKind?: string;
|
|
1077
|
+
failureCode?: string;
|
|
1075
1078
|
timeoutMs: number;
|
|
1076
1079
|
outputLimitBytes: number;
|
|
1077
1080
|
configSource?: string;
|
|
@@ -1107,6 +1110,8 @@ type MeshRefineSubmoduleReachabilityEntry = {
|
|
|
1107
1110
|
autoPublishSucceeded?: boolean;
|
|
1108
1111
|
autoPublishVerified?: boolean;
|
|
1109
1112
|
autoPublishRefspec?: string;
|
|
1113
|
+
autoPublishSkippedReason?: string;
|
|
1114
|
+
importedFromWorktree?: boolean;
|
|
1110
1115
|
checkedLocal?: boolean;
|
|
1111
1116
|
localReachable?: boolean;
|
|
1112
1117
|
remote?: string;
|
|
@@ -1284,7 +1289,7 @@ async function runMeshRefinePatchEquivalenceGate(
|
|
|
1284
1289
|
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1285
1290
|
repoRoot: string,
|
|
1286
1291
|
mergedTree: string,
|
|
1287
|
-
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string } = {},
|
|
1292
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1288
1293
|
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1289
1294
|
const startedAt = Date.now();
|
|
1290
1295
|
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
@@ -1317,6 +1322,17 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1317
1322
|
});
|
|
1318
1323
|
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1319
1324
|
};
|
|
1325
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1326
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1327
|
+
try {
|
|
1328
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1329
|
+
} catch {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1333
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1334
|
+
return true;
|
|
1335
|
+
};
|
|
1320
1336
|
|
|
1321
1337
|
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1322
1338
|
const gitlinks = treeOutput
|
|
@@ -1339,6 +1355,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1339
1355
|
if (!fs.existsSync(submodulePath)) {
|
|
1340
1356
|
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1341
1357
|
entry.publishRequired = true;
|
|
1358
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1359
|
+
entry.autoPublishAllowed = true;
|
|
1360
|
+
entry.autoPublishAttempted = false;
|
|
1361
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1362
|
+
}
|
|
1342
1363
|
entries.push(entry);
|
|
1343
1364
|
continue;
|
|
1344
1365
|
}
|
|
@@ -1349,6 +1370,21 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1349
1370
|
entry.localReachable = true;
|
|
1350
1371
|
} catch {
|
|
1351
1372
|
entry.localReachable = false;
|
|
1373
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1374
|
+
try {
|
|
1375
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1376
|
+
submodulePath,
|
|
1377
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1378
|
+
gitlink.commit,
|
|
1379
|
+
);
|
|
1380
|
+
if (imported) {
|
|
1381
|
+
entry.localReachable = true;
|
|
1382
|
+
entry.importedFromWorktree = true;
|
|
1383
|
+
}
|
|
1384
|
+
} catch (importError: any) {
|
|
1385
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1352
1388
|
// Probe the submodule remote before allowing cleanup/completion.
|
|
1353
1389
|
}
|
|
1354
1390
|
|
|
@@ -1362,6 +1398,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1362
1398
|
} catch {
|
|
1363
1399
|
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1364
1400
|
entry.publishRequired = true;
|
|
1401
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1402
|
+
entry.autoPublishAllowed = true;
|
|
1403
|
+
entry.autoPublishAttempted = false;
|
|
1404
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1405
|
+
}
|
|
1365
1406
|
entries.push(entry);
|
|
1366
1407
|
continue;
|
|
1367
1408
|
}
|
|
@@ -1401,6 +1442,11 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1401
1442
|
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1402
1443
|
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1403
1444
|
}
|
|
1445
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1446
|
+
entry.autoPublishAllowed = true;
|
|
1447
|
+
entry.autoPublishAttempted = false;
|
|
1448
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1449
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1404
1450
|
}
|
|
1405
1451
|
}
|
|
1406
1452
|
} catch (e: any) {
|
|
@@ -1444,16 +1490,18 @@ async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1444
1490
|
|
|
1445
1491
|
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1446
1492
|
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1493
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1494
|
+
displayCommand: command.displayCommand,
|
|
1495
|
+
category: command.category,
|
|
1496
|
+
source: command.source,
|
|
1497
|
+
cwd: command.cwd,
|
|
1498
|
+
timeoutMs: command.timeoutMs,
|
|
1499
|
+
});
|
|
1447
1500
|
return {
|
|
1448
1501
|
source: plan.source,
|
|
1449
1502
|
sourceType: plan.sourceType,
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
category: command.category,
|
|
1453
|
-
source: command.source,
|
|
1454
|
-
cwd: command.cwd,
|
|
1455
|
-
timeoutMs: command.timeoutMs,
|
|
1456
|
-
})),
|
|
1503
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1504
|
+
commands: plan.commands.map(mapCommand),
|
|
1457
1505
|
unavailableReason: plan.unavailableReason,
|
|
1458
1506
|
rejectedCommands: plan.rejectedCommands,
|
|
1459
1507
|
suggestions: plan.suggestions,
|
|
@@ -1473,6 +1521,7 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1473
1521
|
status: 'skipped',
|
|
1474
1522
|
required: true,
|
|
1475
1523
|
commandsRun: [],
|
|
1524
|
+
bootstrapCommandsRun: [],
|
|
1476
1525
|
rejectedCommands: selection.rejectedCommands,
|
|
1477
1526
|
skippedReason: undefined,
|
|
1478
1527
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
@@ -1488,7 +1537,32 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1488
1537
|
return summary;
|
|
1489
1538
|
}
|
|
1490
1539
|
|
|
1491
|
-
|
|
1540
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1541
|
+
command: candidate.command,
|
|
1542
|
+
args: candidate.args,
|
|
1543
|
+
displayCommand: candidate.displayCommand,
|
|
1544
|
+
category: candidate.category,
|
|
1545
|
+
source: candidate.source,
|
|
1546
|
+
cwd,
|
|
1547
|
+
passed,
|
|
1548
|
+
durationMs: Date.now() - startedAt,
|
|
1549
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1550
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1551
|
+
...extras,
|
|
1552
|
+
});
|
|
1553
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1554
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1555
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1556
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1557
|
+
};
|
|
1558
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1559
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1560
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1561
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1562
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
1492
1566
|
const startedAt = Date.now();
|
|
1493
1567
|
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1494
1568
|
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
@@ -1500,36 +1574,61 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
1500
1574
|
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1501
1575
|
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1502
1576
|
});
|
|
1503
|
-
summary.
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1577
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1578
|
+
} catch (error: any) {
|
|
1579
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1580
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1581
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1582
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1583
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
1584
|
+
}));
|
|
1585
|
+
summary.status = 'failed';
|
|
1586
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1587
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1588
|
+
return summary;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
for (const candidate of selection.commands) {
|
|
1593
|
+
const startedAt = Date.now();
|
|
1594
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1595
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1596
|
+
if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
1597
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1598
|
+
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.',
|
|
1599
|
+
}, false, {
|
|
1600
|
+
exitCode: null,
|
|
1601
|
+
skipped: true,
|
|
1602
|
+
failureKind: 'missing_dependencies',
|
|
1603
|
+
}));
|
|
1604
|
+
summary.status = 'failed';
|
|
1605
|
+
summary.failureKind = 'missing_dependencies';
|
|
1606
|
+
summary.failureCode = 'missing_dependencies';
|
|
1607
|
+
return summary;
|
|
1608
|
+
}
|
|
1609
|
+
try {
|
|
1610
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1509
1611
|
cwd,
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
1612
|
+
encoding: 'utf8',
|
|
1613
|
+
timeout,
|
|
1614
|
+
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1615
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1515
1616
|
});
|
|
1617
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1516
1618
|
} catch (error: any) {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
displayCommand: candidate.displayCommand,
|
|
1521
|
-
category: candidate.category,
|
|
1522
|
-
source: candidate.source,
|
|
1523
|
-
cwd,
|
|
1524
|
-
passed: false,
|
|
1619
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
1620
|
+
const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
1621
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1525
1622
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1526
1623
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1527
1624
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
stderr: truncateValidationOutput(error?.stderr || error?.message),
|
|
1531
|
-
});
|
|
1625
|
+
...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
1626
|
+
}));
|
|
1532
1627
|
summary.status = 'failed';
|
|
1628
|
+
if (missingDependencyFailure) {
|
|
1629
|
+
summary.failureKind = 'missing_dependencies';
|
|
1630
|
+
summary.failureCode = 'missing_dependencies';
|
|
1631
|
+
}
|
|
1533
1632
|
return summary;
|
|
1534
1633
|
}
|
|
1535
1634
|
}
|
|
@@ -2751,9 +2850,13 @@ export class DaemonCommandRouter {
|
|
|
2751
2850
|
if (validationSummary.status === 'failed') {
|
|
2752
2851
|
return {
|
|
2753
2852
|
success: false,
|
|
2754
|
-
code: 'validation_failed',
|
|
2853
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
2755
2854
|
convergenceStatus: 'blocked_review',
|
|
2756
|
-
error:
|
|
2855
|
+
error: validationSummary.failureCode === 'missing_dependencies'
|
|
2856
|
+
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
2857
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
2858
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
2859
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2757
2860
|
branch,
|
|
2758
2861
|
into: baseBranch,
|
|
2759
2862
|
validationSummary,
|
|
@@ -2825,6 +2928,7 @@ export class DaemonCommandRouter {
|
|
|
2825
2928
|
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
2826
2929
|
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
2827
2930
|
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
2931
|
+
worktreeRoot: node.workspace,
|
|
2828
2932
|
});
|
|
2829
2933
|
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
2830
2934
|
checked: submoduleReachability.checked,
|
|
@@ -2844,6 +2948,16 @@ export class DaemonCommandRouter {
|
|
|
2844
2948
|
remoteMainReachable: entry.remoteMainReachable,
|
|
2845
2949
|
error: entry.error,
|
|
2846
2950
|
})),
|
|
2951
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
2952
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
2953
|
+
.map(entry => ({
|
|
2954
|
+
path: entry.path,
|
|
2955
|
+
commit: entry.commit,
|
|
2956
|
+
remote: entry.remote,
|
|
2957
|
+
remoteUrl: entry.remoteUrl,
|
|
2958
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
2959
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
2960
|
+
})),
|
|
2847
2961
|
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
2848
2962
|
path: entry.path,
|
|
2849
2963
|
commit: entry.commit,
|
|
@@ -2851,9 +2965,10 @@ export class DaemonCommandRouter {
|
|
|
2851
2965
|
autoPublishAllowed: entry.autoPublishAllowed,
|
|
2852
2966
|
autoPublishAttempted: entry.autoPublishAttempted,
|
|
2853
2967
|
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2968
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
2969
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
2970
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
2971
|
+
remote: entry.remote,
|
|
2857
2972
|
remoteUrl: entry.remoteUrl,
|
|
2858
2973
|
remoteReachable: entry.remoteReachable,
|
|
2859
2974
|
remoteMainBranch: entry.remoteMainBranch,
|
|
@@ -2891,6 +3006,7 @@ export class DaemonCommandRouter {
|
|
|
2891
3006
|
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
2892
3007
|
autoPublishVerified: entry.autoPublishVerified,
|
|
2893
3008
|
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3009
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
2894
3010
|
error: entry.error,
|
|
2895
3011
|
})),
|
|
2896
3012
|
branch,
|
|
@@ -4897,8 +5013,9 @@ export class DaemonCommandRouter {
|
|
|
4897
5013
|
const meshHost = resolveMeshHostStatus(mesh);
|
|
4898
5014
|
|
|
4899
5015
|
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
5016
|
+
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId).length;
|
|
4900
5017
|
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
4901
|
-
if (!refreshRequested) {
|
|
5018
|
+
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
4902
5019
|
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
4903
5020
|
if (cachedStatus) {
|
|
4904
5021
|
logRepoMeshStatusDebug('return_cached', {
|
|
@@ -4912,6 +5029,8 @@ export class DaemonCommandRouter {
|
|
|
4912
5029
|
}
|
|
4913
5030
|
const refreshReason = refreshRequested
|
|
4914
5031
|
? 'explicit_refresh'
|
|
5032
|
+
: pendingCoordinatorEventCount > 0
|
|
5033
|
+
? 'pending_coordinator_events'
|
|
4915
5034
|
: hadAggregateCache
|
|
4916
5035
|
? 'stale_pending_cache_refresh'
|
|
4917
5036
|
: 'cold_cache_miss';
|
|
@@ -4954,8 +5073,14 @@ export class DaemonCommandRouter {
|
|
|
4954
5073
|
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
4955
5074
|
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
4956
5075
|
: directTruth;
|
|
5076
|
+
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5077
|
+
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
5078
|
+
&& Array.isArray(mesh.nodes)
|
|
5079
|
+
&& mesh.nodes
|
|
5080
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
|
|
5081
|
+
.every((node: any) => node?.isLocalWorktree === true);
|
|
4957
5082
|
const directTruthSatisfied = !requireDirectPeerTruth
|
|
4958
|
-
|| effectiveDirectTruth.directEvidenceCount > 0;
|
|
5083
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
4959
5084
|
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4960
5085
|
const failureResult = {
|
|
4961
5086
|
success: false,
|
|
@@ -5216,6 +5341,7 @@ export class DaemonCommandRouter {
|
|
|
5216
5341
|
nodeStatuses.push(status);
|
|
5217
5342
|
}
|
|
5218
5343
|
|
|
5344
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId);
|
|
5219
5345
|
const statusResult = {
|
|
5220
5346
|
success: true,
|
|
5221
5347
|
meshId: mesh.id,
|
|
@@ -5257,8 +5383,13 @@ export class DaemonCommandRouter {
|
|
|
5257
5383
|
nodes: nodeStatuses,
|
|
5258
5384
|
queue: { tasks: queue, summary: queueSummary },
|
|
5259
5385
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
5386
|
+
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
5260
5387
|
};
|
|
5261
|
-
const
|
|
5388
|
+
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
5389
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
5390
|
+
const returnedStatus = pendingCoordinatorEvents.length > 0
|
|
5391
|
+
? { ...rememberedStatus, pendingCoordinatorEvents }
|
|
5392
|
+
: rememberedStatus;
|
|
5262
5393
|
logRepoMeshStatusDebug('return_live', {
|
|
5263
5394
|
meshId,
|
|
5264
5395
|
command: 'mesh_status',
|
|
@@ -5266,9 +5397,9 @@ export class DaemonCommandRouter {
|
|
|
5266
5397
|
refreshReason,
|
|
5267
5398
|
meshSource: meshRecord.source,
|
|
5268
5399
|
directTruth,
|
|
5269
|
-
summary: summarizeRepoMeshStatusDebug(
|
|
5400
|
+
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
5270
5401
|
});
|
|
5271
|
-
return
|
|
5402
|
+
return returnedStatus;
|
|
5272
5403
|
} catch (e: any) {
|
|
5273
5404
|
return { success: false, error: e.message };
|
|
5274
5405
|
}
|
|
@@ -27,6 +27,11 @@ export interface RepoMeshRefineConfig {
|
|
|
27
27
|
allowAutoPublishSubmoduleMainCommits?: boolean;
|
|
28
28
|
validation?: {
|
|
29
29
|
required?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Optional dependency/bootstrap commands that Refinery runs before
|
|
32
|
+
* validation commands. Refinery never infers installs on its own.
|
|
33
|
+
*/
|
|
34
|
+
bootstrapCommands?: RepoMeshRefineValidationCommandConfig[];
|
|
30
35
|
commands?: RepoMeshRefineValidationCommandConfig[];
|
|
31
36
|
};
|
|
32
37
|
}
|
|
@@ -53,6 +58,7 @@ export interface MeshRefineConfigLoadResult {
|
|
|
53
58
|
export interface MeshRefineValidationPlan {
|
|
54
59
|
source: string;
|
|
55
60
|
sourceType: MeshRefineConfigLoadResult['sourceType'];
|
|
61
|
+
bootstrapCommands: MeshRefineValidationCommandPlan[];
|
|
56
62
|
commands: MeshRefineValidationCommandPlan[];
|
|
57
63
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
58
64
|
suggestions: RepoMeshRefineValidationCommandConfig[];
|
|
@@ -108,6 +114,23 @@ export const MESH_REFINE_CONFIG_SCHEMA = {
|
|
|
108
114
|
},
|
|
109
115
|
},
|
|
110
116
|
},
|
|
117
|
+
bootstrapCommands: {
|
|
118
|
+
type: 'array',
|
|
119
|
+
maxItems: 4,
|
|
120
|
+
items: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
required: ['command'],
|
|
124
|
+
properties: {
|
|
125
|
+
command: { type: 'string', minLength: 1 },
|
|
126
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
127
|
+
category: { enum: [...MESH_REFINE_VALIDATION_CATEGORIES, 'custom'] },
|
|
128
|
+
cwd: { type: 'string' },
|
|
129
|
+
timeoutMs: { type: 'number', minimum: 1000, maximum: 600000 },
|
|
130
|
+
env: { type: 'object', additionalProperties: { type: 'string' } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
111
134
|
},
|
|
112
135
|
},
|
|
113
136
|
},
|
|
@@ -184,12 +207,13 @@ function normalizeCommandConfig(entry: unknown, source: string): { command?: Mes
|
|
|
184
207
|
};
|
|
185
208
|
}
|
|
186
209
|
|
|
187
|
-
export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
|
|
210
|
+
export function validateMeshRefineConfig(config: unknown, source = 'inline'): { valid: boolean; errors: string[]; bootstrapCommands: MeshRefineValidationCommandPlan[]; commands: MeshRefineValidationCommandPlan[]; rejectedCommands: Array<Record<string, unknown>> } {
|
|
188
211
|
const errors: string[] = [];
|
|
212
|
+
const bootstrapCommands: MeshRefineValidationCommandPlan[] = [];
|
|
189
213
|
const commands: MeshRefineValidationCommandPlan[] = [];
|
|
190
214
|
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
191
215
|
|
|
192
|
-
if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], commands, rejectedCommands };
|
|
216
|
+
if (!isRecord(config)) return { valid: false, errors: ['config must be an object'], bootstrapCommands, commands, rejectedCommands };
|
|
193
217
|
if (config.version !== 1) errors.push('version must be 1');
|
|
194
218
|
if (config.allowAutoPublishSubmoduleMainCommits !== undefined && typeof config.allowAutoPublishSubmoduleMainCommits !== 'boolean') {
|
|
195
219
|
errors.push('allowAutoPublishSubmoduleMainCommits must be a boolean when provided');
|
|
@@ -197,7 +221,16 @@ export function validateMeshRefineConfig(config: unknown, source = 'inline'): {
|
|
|
197
221
|
const validation = config.validation;
|
|
198
222
|
if (validation !== undefined && !isRecord(validation)) errors.push('validation must be an object');
|
|
199
223
|
const rawCommands = isRecord(validation) ? validation.commands : undefined;
|
|
224
|
+
const rawBootstrapCommands = isRecord(validation) ? validation.bootstrapCommands : undefined;
|
|
200
225
|
if (rawCommands !== undefined && !Array.isArray(rawCommands)) errors.push('validation.commands must be an array');
|
|
226
|
+
if (rawBootstrapCommands !== undefined && !Array.isArray(rawBootstrapCommands)) errors.push('validation.bootstrapCommands must be an array');
|
|
227
|
+
if (Array.isArray(rawBootstrapCommands)) {
|
|
228
|
+
rawBootstrapCommands.forEach((entry, index) => {
|
|
229
|
+
const normalized = normalizeCommandConfig(entry, `${source}:validation.bootstrapCommands[${index}]`);
|
|
230
|
+
if (normalized.command) bootstrapCommands.push(normalized.command);
|
|
231
|
+
if (normalized.rejected) rejectedCommands.push(normalized.rejected);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
201
234
|
if (Array.isArray(rawCommands)) {
|
|
202
235
|
rawCommands.forEach((entry, index) => {
|
|
203
236
|
const normalized = normalizeCommandConfig(entry, `${source}:validation.commands[${index}]`);
|
|
@@ -206,7 +239,7 @@ export function validateMeshRefineConfig(config: unknown, source = 'inline'): {
|
|
|
206
239
|
});
|
|
207
240
|
}
|
|
208
241
|
if (rejectedCommands.length) errors.push('one or more validation commands are invalid');
|
|
209
|
-
return { valid: errors.length === 0, errors, commands, rejectedCommands };
|
|
242
|
+
return { valid: errors.length === 0, errors, bootstrapCommands, commands, rejectedCommands };
|
|
210
243
|
}
|
|
211
244
|
|
|
212
245
|
function parseConfigText(path: string, text: string): unknown {
|
|
@@ -300,6 +333,7 @@ export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): M
|
|
|
300
333
|
return {
|
|
301
334
|
source: loaded.source,
|
|
302
335
|
sourceType: loaded.sourceType,
|
|
336
|
+
bootstrapCommands: [],
|
|
303
337
|
commands: [],
|
|
304
338
|
rejectedCommands: loaded.error ? [{ source: loaded.source, reason: loaded.error }] : [],
|
|
305
339
|
suggestions: suggestion.suggestions,
|
|
@@ -312,6 +346,7 @@ export function resolveMeshRefineValidationPlan(mesh: any, workspace: string): M
|
|
|
312
346
|
return {
|
|
313
347
|
source: loaded.path || loaded.source,
|
|
314
348
|
sourceType: loaded.sourceType,
|
|
349
|
+
bootstrapCommands: validation.bootstrapCommands,
|
|
315
350
|
commands: validation.commands,
|
|
316
351
|
rejectedCommands: validation.rejectedCommands,
|
|
317
352
|
suggestions: suggestion.suggestions,
|