triflux 10.20.0 → 10.20.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +34 -42
- package/README.md +30 -36
- package/bin/triflux.mjs +13 -4
- package/hooks/hook-orchestrator.mjs +1 -1
- package/hooks/hook-registry.json +76 -2
- package/hooks/hooks.json +49 -1
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +355 -16
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/session-end-cleanup.mjs +201 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +4 -0
- package/hub/account-broker.mjs +297 -8
- package/hub/codex-adapter.mjs +62 -2
- package/hub/server.mjs +51 -4
- package/hub/team/cli/services/native-control.mjs +1 -0
- package/hub/team/conductor.mjs +94 -29
- package/hub/team/native-supervisor.mjs +20 -7
- package/hub/team/swarm-cli.mjs +98 -4
- package/hub/team/swarm-hypervisor.mjs +112 -10
- package/hub/team/swarm-locks.mjs +7 -2
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/synapse-cli.mjs +243 -6
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worktree-lifecycle.mjs +49 -8
- package/hub/workers/delegator-mcp.mjs +14 -1
- package/package.json +1 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +4 -0
- package/scripts/__tests__/skill-surface.test.mjs +61 -0
- package/scripts/gen-skill-manifest.mjs +3 -0
- package/scripts/lib/skill-template.mjs +2 -0
- package/scripts/session-stale-cleanup.mjs +111 -2
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/skill.json +11 -3
- package/skills/tfx-auto/skill.json +2 -2
- package/skills/tfx-autopilot/skill.json +9 -4
- package/skills/tfx-consensus/skill.json +4 -3
- package/skills/tfx-debate/skill.json +11 -4
- package/skills/tfx-doctor/skill.json +3 -1
- package/skills/tfx-fullcycle/skill.json +10 -4
- package/skills/tfx-hooks/skill.json +3 -1
- package/skills/tfx-hub/skill.json +5 -3
- package/skills/tfx-index/skill.json +6 -1
- package/skills/tfx-interview/skill.json +6 -1
- package/skills/tfx-multi/skill.json +7 -3
- package/skills/tfx-panel/skill.json +11 -4
- package/skills/tfx-persist/skill.json +11 -4
- package/skills/tfx-plan/skill.json +13 -3
- package/skills/tfx-profile/skill.json +3 -1
- package/skills/tfx-prune/skill.json +7 -1
- package/skills/tfx-psmux-rules/SKILL.md +2 -0
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +22 -307
- package/skills/tfx-psmux-rules/skill.json +7 -2
- package/skills/tfx-qa/skill.json +12 -3
- package/skills/tfx-ralph/skill.json +4 -1
- package/skills/tfx-remote/skill.json +8 -0
- package/skills/tfx-remote-setup/SKILL.md +2 -0
- package/skills/tfx-remote-setup/SKILL.md.tmpl +23 -570
- package/skills/tfx-remote-setup/skill.json +7 -3
- package/skills/tfx-remote-spawn/SKILL.md +2 -0
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +31 -242
- package/skills/tfx-remote-spawn/skill.json +7 -3
- package/skills/tfx-research/skill.json +11 -4
- package/skills/tfx-review/skill.json +12 -3
- package/skills/tfx-setup/skill.json +3 -1
- package/skills/tfx-ship/skill.json +13 -0
- package/skills/tfx-swarm/skill.json +13 -0
- package/skills/tfx-wt/skill.json +12 -0
|
@@ -241,6 +241,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
241
241
|
integrationFailures: [],
|
|
242
242
|
skipped: [],
|
|
243
243
|
integrationBranch: null,
|
|
244
|
+
report: [],
|
|
244
245
|
processCleanup: {
|
|
245
246
|
totalKilled: 0,
|
|
246
247
|
byCategory: {},
|
|
@@ -319,6 +320,9 @@ export function createSwarmHypervisor(opts) {
|
|
|
319
320
|
]),
|
|
320
321
|
skipped: Object.freeze([...(payload.skipped || [])]),
|
|
321
322
|
integrationBranch: payload.integrationBranch || null,
|
|
323
|
+
report: Object.freeze(
|
|
324
|
+
(payload.report || []).map((row) => Object.freeze({ ...row })),
|
|
325
|
+
),
|
|
322
326
|
results: Object.freeze([...(payload.results || [])]),
|
|
323
327
|
processCleanup: Object.freeze({
|
|
324
328
|
totalKilled: processCleanup.totalKilled || 0,
|
|
@@ -340,6 +344,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
340
344
|
integrationFailures: [...integrationResult.integrationFailures],
|
|
341
345
|
skipped: [...integrationResult.skipped],
|
|
342
346
|
integrationBranch: integrationResult.integrationBranch,
|
|
347
|
+
report: integrationResult.report.map((row) => ({ ...row })),
|
|
343
348
|
processCleanup: integrationResult.processCleanup,
|
|
344
349
|
error: integrationResult.error,
|
|
345
350
|
};
|
|
@@ -373,6 +378,20 @@ export function createSwarmHypervisor(opts) {
|
|
|
373
378
|
return getProcessCleanupSummary();
|
|
374
379
|
}
|
|
375
380
|
|
|
381
|
+
function normalizeIntegrationReportRow(row) {
|
|
382
|
+
return {
|
|
383
|
+
shard: row.shard,
|
|
384
|
+
strategy: row.strategy || "failed",
|
|
385
|
+
commits:
|
|
386
|
+
Number.isFinite(Number(row.commits)) && row.commits !== null
|
|
387
|
+
? Number(row.commits)
|
|
388
|
+
: null,
|
|
389
|
+
finalSha: row.finalSha || null,
|
|
390
|
+
notes: row.notes || "",
|
|
391
|
+
recoveryPatch: row.recoveryPatch || null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
376
395
|
function git(args, cwd = workdir) {
|
|
377
396
|
return new Promise((resolve, reject) => {
|
|
378
397
|
execFile(
|
|
@@ -964,9 +983,23 @@ export function createSwarmHypervisor(opts) {
|
|
|
964
983
|
|
|
965
984
|
const integrated = [];
|
|
966
985
|
const integrationFailures = [];
|
|
986
|
+
const integrationReport = [];
|
|
967
987
|
const preIntegrationFailures = [...failures.keys()];
|
|
968
988
|
let integrationBranch = null;
|
|
969
989
|
|
|
990
|
+
const appendIntegrationReport = (row) => {
|
|
991
|
+
const normalized = normalizeIntegrationReportRow(row);
|
|
992
|
+
integrationReport.push(normalized);
|
|
993
|
+
eventLog.append("integration_shard_report", normalized);
|
|
994
|
+
return normalized;
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
const failureNotes = (shardName, fallback = "failed") => {
|
|
998
|
+
const info = failures.get(shardName);
|
|
999
|
+
if (!info) return fallback;
|
|
1000
|
+
return info.reason || info.mode || fallback;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
970
1003
|
try {
|
|
971
1004
|
({ integrationBranch } = await prepareIntegrationBranchImpl({
|
|
972
1005
|
runId,
|
|
@@ -977,6 +1010,11 @@ export function createSwarmHypervisor(opts) {
|
|
|
977
1010
|
for (const shardName of plan.mergeOrder) {
|
|
978
1011
|
if (failures.has(shardName)) {
|
|
979
1012
|
eventLog.append("skip_failed_shard", { shard: shardName });
|
|
1013
|
+
appendIntegrationReport({
|
|
1014
|
+
shard: shardName,
|
|
1015
|
+
strategy: "failed",
|
|
1016
|
+
notes: `skipped: ${failureNotes(shardName)}`,
|
|
1017
|
+
});
|
|
980
1018
|
continue;
|
|
981
1019
|
}
|
|
982
1020
|
|
|
@@ -1002,11 +1040,22 @@ export function createSwarmHypervisor(opts) {
|
|
|
1002
1040
|
shard: shardName,
|
|
1003
1041
|
error: fetchResult.error,
|
|
1004
1042
|
});
|
|
1005
|
-
await maybeCleanupWorktree(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1043
|
+
const cleanup = await maybeCleanupWorktree(
|
|
1044
|
+
shardName,
|
|
1045
|
+
worker,
|
|
1046
|
+
shard,
|
|
1047
|
+
{
|
|
1048
|
+
force: true,
|
|
1049
|
+
failureReason: "remote_fetch_failed",
|
|
1050
|
+
},
|
|
1051
|
+
);
|
|
1009
1052
|
integrationFailures.push(shardName);
|
|
1053
|
+
appendIntegrationReport({
|
|
1054
|
+
shard: shardName,
|
|
1055
|
+
strategy: "failed",
|
|
1056
|
+
notes: `remote_fetch_failed: ${fetchResult.error || "unknown"}`,
|
|
1057
|
+
recoveryPatch: cleanup?.recoveryPatch?.patchPath || null,
|
|
1058
|
+
});
|
|
1010
1059
|
continue;
|
|
1011
1060
|
}
|
|
1012
1061
|
eventLog.append("remote_fetch_ok", {
|
|
@@ -1042,12 +1091,20 @@ export function createSwarmHypervisor(opts) {
|
|
|
1042
1091
|
shard: shardName,
|
|
1043
1092
|
...commitEvidence,
|
|
1044
1093
|
});
|
|
1045
|
-
await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1094
|
+
const cleanup = await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1046
1095
|
force: true,
|
|
1047
1096
|
failureReason:
|
|
1048
1097
|
failures.get(shardName)?.mode || "no_commit_evidence",
|
|
1049
1098
|
});
|
|
1050
1099
|
integrationFailures.push(shardName);
|
|
1100
|
+
appendIntegrationReport({
|
|
1101
|
+
shard: shardName,
|
|
1102
|
+
strategy: "failed",
|
|
1103
|
+
commits: commitEvidence.commitsAhead,
|
|
1104
|
+
finalSha: commitEvidence.headCommit,
|
|
1105
|
+
notes: failureNotes(shardName, "no_commit_evidence"),
|
|
1106
|
+
recoveryPatch: cleanup?.recoveryPatch?.patchPath || null,
|
|
1107
|
+
});
|
|
1051
1108
|
continue;
|
|
1052
1109
|
}
|
|
1053
1110
|
|
|
@@ -1065,12 +1122,18 @@ export function createSwarmHypervisor(opts) {
|
|
|
1065
1122
|
shard: shardName,
|
|
1066
1123
|
violations: validation.violations,
|
|
1067
1124
|
});
|
|
1068
|
-
await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1125
|
+
const cleanup = await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1069
1126
|
force: true,
|
|
1070
1127
|
failureReason:
|
|
1071
1128
|
failures.get(shardName)?.mode || "lease_violation_revert",
|
|
1072
1129
|
});
|
|
1073
1130
|
integrationFailures.push(shardName);
|
|
1131
|
+
appendIntegrationReport({
|
|
1132
|
+
shard: shardName,
|
|
1133
|
+
strategy: "failed",
|
|
1134
|
+
notes: failureNotes(shardName, "lease_violation_revert"),
|
|
1135
|
+
recoveryPatch: cleanup?.recoveryPatch?.patchPath || null,
|
|
1136
|
+
});
|
|
1074
1137
|
continue;
|
|
1075
1138
|
}
|
|
1076
1139
|
|
|
@@ -1087,12 +1150,19 @@ export function createSwarmHypervisor(opts) {
|
|
|
1087
1150
|
integrationBranch,
|
|
1088
1151
|
error: rebaseResult.error,
|
|
1089
1152
|
});
|
|
1090
|
-
await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1153
|
+
const cleanup = await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1091
1154
|
force: true,
|
|
1092
1155
|
failureReason:
|
|
1093
1156
|
rebaseResult.error || FAILURE_MODES.F5_MERGE_CONFLICT,
|
|
1094
1157
|
});
|
|
1095
1158
|
integrationFailures.push(shardName);
|
|
1159
|
+
appendIntegrationReport({
|
|
1160
|
+
shard: shardName,
|
|
1161
|
+
strategy: "failed",
|
|
1162
|
+
commits: rebaseResult.commits ?? commitEvidence.commitsAhead,
|
|
1163
|
+
notes: rebaseResult.error || FAILURE_MODES.F5_MERGE_CONFLICT,
|
|
1164
|
+
recoveryPatch: cleanup?.recoveryPatch?.patchPath || null,
|
|
1165
|
+
});
|
|
1096
1166
|
continue;
|
|
1097
1167
|
}
|
|
1098
1168
|
|
|
@@ -1103,9 +1173,19 @@ export function createSwarmHypervisor(opts) {
|
|
|
1103
1173
|
worktreePath: worker.worktreePath || null,
|
|
1104
1174
|
integrationBranch,
|
|
1105
1175
|
headCommit: rebaseResult.headCommit,
|
|
1176
|
+
integrationStrategy: rebaseResult.strategy || "cherry-pick",
|
|
1177
|
+
commits: rebaseResult.commits ?? commitEvidence.commitsAhead,
|
|
1178
|
+
notes: rebaseResult.notes || "",
|
|
1106
1179
|
completedAt: Date.now(),
|
|
1107
1180
|
});
|
|
1108
1181
|
integrated.push(shardName);
|
|
1182
|
+
appendIntegrationReport({
|
|
1183
|
+
shard: shardName,
|
|
1184
|
+
strategy: rebaseResult.strategy || "cherry-pick",
|
|
1185
|
+
commits: rebaseResult.commits ?? commitEvidence.commitsAhead,
|
|
1186
|
+
finalSha: rebaseResult.headCommit,
|
|
1187
|
+
notes: rebaseResult.notes || "",
|
|
1188
|
+
});
|
|
1109
1189
|
|
|
1110
1190
|
await maybeCleanupWorktree(shardName, worker, shard, {
|
|
1111
1191
|
branchName: shardBranch,
|
|
@@ -1128,6 +1208,15 @@ export function createSwarmHypervisor(opts) {
|
|
|
1128
1208
|
const skipped = preIntegrationFailures.filter(
|
|
1129
1209
|
(name) => !integrationFailures.includes(name),
|
|
1130
1210
|
);
|
|
1211
|
+
const reported = new Set(integrationReport.map((row) => row.shard));
|
|
1212
|
+
for (const shardName of failed) {
|
|
1213
|
+
if (reported.has(shardName)) continue;
|
|
1214
|
+
appendIntegrationReport({
|
|
1215
|
+
shard: shardName,
|
|
1216
|
+
strategy: "failed",
|
|
1217
|
+
notes: `integration_error: ${err.message}`,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1131
1220
|
|
|
1132
1221
|
eventLog.append("integration_complete", {
|
|
1133
1222
|
integrated,
|
|
@@ -1135,6 +1224,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1135
1224
|
integrationFailures,
|
|
1136
1225
|
integrationBranch,
|
|
1137
1226
|
skipped,
|
|
1227
|
+
report: integrationReport,
|
|
1138
1228
|
error: err.message,
|
|
1139
1229
|
});
|
|
1140
1230
|
|
|
@@ -1145,6 +1235,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1145
1235
|
integrationFailures,
|
|
1146
1236
|
integrationBranch,
|
|
1147
1237
|
skipped,
|
|
1238
|
+
report: integrationReport,
|
|
1148
1239
|
results: [...results.values()],
|
|
1149
1240
|
partial: true,
|
|
1150
1241
|
error: err.message,
|
|
@@ -1164,6 +1255,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1164
1255
|
integrationFailures,
|
|
1165
1256
|
integrationBranch,
|
|
1166
1257
|
skipped,
|
|
1258
|
+
report: integrationReport,
|
|
1167
1259
|
});
|
|
1168
1260
|
|
|
1169
1261
|
if (failed.length > 0 && integrated.length === 0) {
|
|
@@ -1181,6 +1273,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1181
1273
|
integrationFailures,
|
|
1182
1274
|
integrationBranch,
|
|
1183
1275
|
skipped,
|
|
1276
|
+
report: integrationReport,
|
|
1184
1277
|
results: [...results.values()],
|
|
1185
1278
|
partial: failed.length > 0,
|
|
1186
1279
|
});
|
|
@@ -1198,9 +1291,10 @@ export function createSwarmHypervisor(opts) {
|
|
|
1198
1291
|
failureReason = null,
|
|
1199
1292
|
} = {},
|
|
1200
1293
|
) {
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1203
|
-
if (
|
|
1294
|
+
const outcome = { recoveryPatch: null, processCleanup: null };
|
|
1295
|
+
if (!worker?.worktreePath || shard?.host) return outcome;
|
|
1296
|
+
if (failureReason && keepFailedWorktrees) return outcome;
|
|
1297
|
+
if (cleanedWorktreePaths.has(worker.worktreePath)) return outcome;
|
|
1204
1298
|
|
|
1205
1299
|
// Best-effort: preserve any uncommitted worker changes before removing
|
|
1206
1300
|
// the worktree. Silent on failure — must not block cleanup or mask the
|
|
@@ -1213,6 +1307,10 @@ export function createSwarmHypervisor(opts) {
|
|
|
1213
1307
|
recoveryDir: join(workdir, ".codex-swarm", "recovery"),
|
|
1214
1308
|
});
|
|
1215
1309
|
if (preservation?.ok && !preservation.skipped) {
|
|
1310
|
+
outcome.recoveryPatch = {
|
|
1311
|
+
patchPath: preservation.patchPath,
|
|
1312
|
+
manifestPath: preservation.manifestPath || null,
|
|
1313
|
+
};
|
|
1216
1314
|
eventLog.append("recovery_patch_saved", {
|
|
1217
1315
|
shard: shardName,
|
|
1218
1316
|
worktreePath: worker.worktreePath,
|
|
@@ -1236,6 +1334,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1236
1334
|
runId: plan?.runId || runId,
|
|
1237
1335
|
shardName,
|
|
1238
1336
|
});
|
|
1337
|
+
outcome.processCleanup = processCleanup;
|
|
1239
1338
|
worker.processCleanup = processCleanup;
|
|
1240
1339
|
recordProcessCleanup(processCleanup);
|
|
1241
1340
|
eventLog.append("process_cleanup", {
|
|
@@ -1275,6 +1374,8 @@ export function createSwarmHypervisor(opts) {
|
|
|
1275
1374
|
error: err.message,
|
|
1276
1375
|
});
|
|
1277
1376
|
}
|
|
1377
|
+
|
|
1378
|
+
return outcome;
|
|
1278
1379
|
}
|
|
1279
1380
|
|
|
1280
1381
|
/**
|
|
@@ -1387,6 +1488,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1387
1488
|
integrationFailures: [...integrationPromiseState.integrationFailures],
|
|
1388
1489
|
skipped: [...integrationPromiseState.skipped],
|
|
1389
1490
|
integrationBranch: integrationPromiseState.integrationBranch,
|
|
1491
|
+
report: integrationPromiseState.report.map((row) => ({ ...row })),
|
|
1390
1492
|
processCleanup: Object.freeze({
|
|
1391
1493
|
totalKilled: integrationPromiseState.processCleanup.totalKilled,
|
|
1392
1494
|
byCategory: Object.freeze({
|
package/hub/team/swarm-locks.mjs
CHANGED
|
@@ -15,12 +15,12 @@ const LOCK_TTL_MS = 10 * 60_000; // 10 minutes default TTL
|
|
|
15
15
|
// override via validateChanges options.sensitiveDeny.
|
|
16
16
|
//
|
|
17
17
|
// Paths use POSIX separators (matches normalizePath output).
|
|
18
|
-
const SENSITIVE_PATH_PREFIXES = [
|
|
18
|
+
export const SENSITIVE_PATH_PREFIXES = [
|
|
19
19
|
".claude-plugin/",
|
|
20
20
|
"bin/",
|
|
21
21
|
".github/workflows/",
|
|
22
22
|
];
|
|
23
|
-
const SENSITIVE_PATH_FILES = [
|
|
23
|
+
export const SENSITIVE_PATH_FILES = [
|
|
24
24
|
".gitignore",
|
|
25
25
|
".npmignore",
|
|
26
26
|
"package.json",
|
|
@@ -28,6 +28,11 @@ const SENSITIVE_PATH_FILES = [
|
|
|
28
28
|
"biome.json",
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
export const DEFAULT_SENSITIVE_DENY = Object.freeze({
|
|
32
|
+
prefixes: Object.freeze([...SENSITIVE_PATH_PREFIXES]),
|
|
33
|
+
files: Object.freeze([...SENSITIVE_PATH_FILES]),
|
|
34
|
+
});
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Swarm lock manager factory.
|
|
33
38
|
* @param {object} [opts]
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
|
|
2
|
+
// Predicts launch-time blockers before any worker session or worktree spawn.
|
|
3
|
+
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { readHosts, resolveHost } from "../lib/hosts-compat.mjs";
|
|
6
|
+
import { whichCommand } from "../platform.mjs";
|
|
7
|
+
import { MCP_CATALOG } from "./mcp-selector.mjs";
|
|
8
|
+
import { DEFAULT_SENSITIVE_DENY } from "./swarm-locks.mjs";
|
|
9
|
+
import { planSwarm } from "./swarm-planner.mjs";
|
|
10
|
+
|
|
11
|
+
const AGENT_COMMANDS = Object.freeze({
|
|
12
|
+
codex: "codex",
|
|
13
|
+
gemini: "gemini",
|
|
14
|
+
claude: "claude",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function unique(values) {
|
|
18
|
+
return [...new Set(values.filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizePath(filePath) {
|
|
22
|
+
return String(filePath || "")
|
|
23
|
+
.replace(/\\/g, "/")
|
|
24
|
+
.replace(/^\.\//u, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSensitiveFile(filePath, sensitiveDeny = DEFAULT_SENSITIVE_DENY) {
|
|
28
|
+
const path = normalizePath(filePath);
|
|
29
|
+
const files = new Set(sensitiveDeny.files || []);
|
|
30
|
+
const prefixes = sensitiveDeny.prefixes || [];
|
|
31
|
+
return files.has(path) || prefixes.some((prefix) => path.startsWith(prefix));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function planToJson(plan) {
|
|
35
|
+
return {
|
|
36
|
+
totalShards: plan.shards.length,
|
|
37
|
+
shards: plan.shards.map((s) => ({
|
|
38
|
+
name: s.name,
|
|
39
|
+
agent: s.agent,
|
|
40
|
+
host: s.host || null,
|
|
41
|
+
files: [...s.files],
|
|
42
|
+
mcp: [...s.mcp],
|
|
43
|
+
depends: [...s.depends],
|
|
44
|
+
critical: s.critical,
|
|
45
|
+
})),
|
|
46
|
+
leaseMap: Object.fromEntries(plan.leaseMap),
|
|
47
|
+
mcpManifest: Object.fromEntries(plan.mcpManifest),
|
|
48
|
+
mergeOrder: [...plan.mergeOrder],
|
|
49
|
+
criticalShards: [...plan.criticalShards],
|
|
50
|
+
conflicts: [...plan.conflicts],
|
|
51
|
+
remoteSuggestion: plan.remoteSuggestion,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeCheck(name, ok, details = {}) {
|
|
56
|
+
return { name, ok: Boolean(ok), ...details };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildLeaseTable(plan, repoRoot) {
|
|
60
|
+
return plan.shards.map((shard) => ({
|
|
61
|
+
shard: shard.name,
|
|
62
|
+
host: shard.host || null,
|
|
63
|
+
worktreePath: resolve(repoRoot, ".codex-swarm", `wt-${shard.name}`),
|
|
64
|
+
files: [...shard.files],
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkSensitive(plan, sensitiveDeny) {
|
|
69
|
+
const matches = [];
|
|
70
|
+
for (const shard of plan.shards) {
|
|
71
|
+
for (const file of shard.files) {
|
|
72
|
+
if (!isSensitiveFile(file, sensitiveDeny)) continue;
|
|
73
|
+
matches.push({
|
|
74
|
+
shard: shard.name,
|
|
75
|
+
file,
|
|
76
|
+
kind: "sensitive-planned-lease",
|
|
77
|
+
severity: "warning",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return makeCheck("sensitiveDeny", true, { matches });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkHosts(plan, repoRoot) {
|
|
85
|
+
const registry = readHosts(repoRoot);
|
|
86
|
+
const missing = [];
|
|
87
|
+
const capabilityWarnings = [];
|
|
88
|
+
|
|
89
|
+
for (const shard of plan.shards) {
|
|
90
|
+
if (!shard.host) continue;
|
|
91
|
+
const resolvedHost = resolveHost(shard.host, repoRoot);
|
|
92
|
+
if (!resolvedHost) {
|
|
93
|
+
missing.push({ shard: shard.name, host: shard.host });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const capabilities = resolvedHost.host.capabilities || [];
|
|
97
|
+
if (shard.agent && !capabilities.includes(shard.agent)) {
|
|
98
|
+
capabilityWarnings.push({
|
|
99
|
+
shard: shard.name,
|
|
100
|
+
host: shard.host,
|
|
101
|
+
agent: shard.agent,
|
|
102
|
+
capabilities,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return makeCheck("hosts", missing.length === 0, {
|
|
108
|
+
registryPath: registry.path,
|
|
109
|
+
missing,
|
|
110
|
+
warnings: capabilityWarnings,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function checkLocalWorkerClis(plan, deps = {}) {
|
|
115
|
+
const which = deps.whichCommand || whichCommand;
|
|
116
|
+
const missing = [];
|
|
117
|
+
const present = [];
|
|
118
|
+
const checked = new Map();
|
|
119
|
+
|
|
120
|
+
for (const shard of plan.shards) {
|
|
121
|
+
if (shard.host) continue;
|
|
122
|
+
const command = AGENT_COMMANDS[shard.agent] || shard.agent;
|
|
123
|
+
if (!command) {
|
|
124
|
+
missing.push({ shard: shard.name, agent: shard.agent, command: null });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!checked.has(command)) {
|
|
128
|
+
checked.set(command, which(command, deps.whichOptions || {}));
|
|
129
|
+
}
|
|
130
|
+
const path = checked.get(command);
|
|
131
|
+
if (path)
|
|
132
|
+
present.push({ shard: shard.name, agent: shard.agent, command, path });
|
|
133
|
+
else missing.push({ shard: shard.name, agent: shard.agent, command });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return makeCheck("workerCli", missing.length === 0, { present, missing });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function checkMcp(plan) {
|
|
140
|
+
const catalog = new Map(MCP_CATALOG.map((entry) => [entry.name, entry]));
|
|
141
|
+
const unknown = [];
|
|
142
|
+
const incompatible = [];
|
|
143
|
+
|
|
144
|
+
for (const shard of plan.shards) {
|
|
145
|
+
for (const server of shard.mcp) {
|
|
146
|
+
const entry = catalog.get(server);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
unknown.push({ shard: shard.name, server });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
["codex", "gemini"].includes(shard.agent) &&
|
|
153
|
+
!entry.cli.includes(shard.agent)
|
|
154
|
+
) {
|
|
155
|
+
incompatible.push({
|
|
156
|
+
shard: shard.name,
|
|
157
|
+
agent: shard.agent,
|
|
158
|
+
server,
|
|
159
|
+
supportedCli: [...entry.cli],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return makeCheck("mcp", incompatible.length === 0, { unknown, incompatible });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function collectMessages(checks) {
|
|
169
|
+
const errors = [];
|
|
170
|
+
const warnings = [];
|
|
171
|
+
|
|
172
|
+
const leases = checks.leases;
|
|
173
|
+
for (const conflict of leases.conflicts || []) {
|
|
174
|
+
errors.push(
|
|
175
|
+
`lease conflict: ${conflict.file} assigned to ${conflict.shards.join(", ")}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const hosts = checks.hosts;
|
|
180
|
+
for (const missing of hosts.missing || []) {
|
|
181
|
+
errors.push(
|
|
182
|
+
`missing host: shard ${missing.shard} references ${missing.host}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
for (const warn of hosts.warnings || []) {
|
|
186
|
+
warnings.push(
|
|
187
|
+
`host capability warning: ${warn.host} does not advertise ${warn.agent} for shard ${warn.shard}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const workerCli = checks.workerCli;
|
|
192
|
+
for (const missing of workerCli.missing || []) {
|
|
193
|
+
errors.push(
|
|
194
|
+
`missing local worker CLI: shard ${missing.shard} requires ${missing.command || missing.agent}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const mcp = checks.mcp;
|
|
199
|
+
for (const item of mcp.incompatible || []) {
|
|
200
|
+
errors.push(
|
|
201
|
+
`MCP incompatibility: ${item.server} is not supported by ${item.agent} for shard ${item.shard}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
for (const item of mcp.unknown || []) {
|
|
205
|
+
warnings.push(
|
|
206
|
+
`unknown MCP server: ${item.server} requested by shard ${item.shard}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const sensitive = checks.sensitiveDeny;
|
|
211
|
+
for (const item of sensitive.matches || []) {
|
|
212
|
+
warnings.push(
|
|
213
|
+
`sensitive path planned lease: ${item.file} owned by shard ${item.shard}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { errors: unique(errors), warnings: unique(warnings) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Build a no-side-effect preflight report for a swarm PRD.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} prdPath
|
|
224
|
+
* @param {object} [opts]
|
|
225
|
+
* @param {string} [opts.repoRoot]
|
|
226
|
+
* @param {object} [opts.deps]
|
|
227
|
+
* @returns {object}
|
|
228
|
+
*/
|
|
229
|
+
export function runSwarmPreflight(prdPath, opts = {}) {
|
|
230
|
+
const repoRoot = opts.repoRoot || process.cwd();
|
|
231
|
+
const absPrd = resolve(prdPath);
|
|
232
|
+
const sensitiveDeny = opts.sensitiveDeny || DEFAULT_SENSITIVE_DENY;
|
|
233
|
+
let plan;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
plan = (opts.deps?.planSwarm || planSwarm)(absPrd, { repoRoot });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
verdict: "no-go",
|
|
242
|
+
prdPath: absPrd,
|
|
243
|
+
repoRoot,
|
|
244
|
+
checks: {
|
|
245
|
+
plan: makeCheck("plan", false, { error: message }),
|
|
246
|
+
},
|
|
247
|
+
errors: [message],
|
|
248
|
+
warnings: [],
|
|
249
|
+
plan: null,
|
|
250
|
+
leaseTable: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const checks = {
|
|
255
|
+
plan: makeCheck("plan", true),
|
|
256
|
+
leases: makeCheck("leases", plan.conflicts.length === 0, {
|
|
257
|
+
conflicts: [...plan.conflicts],
|
|
258
|
+
}),
|
|
259
|
+
sensitiveDeny: checkSensitive(plan, sensitiveDeny),
|
|
260
|
+
hosts: checkHosts(plan, repoRoot),
|
|
261
|
+
workerCli: checkLocalWorkerClis(plan, opts.deps),
|
|
262
|
+
mcp: checkMcp(plan),
|
|
263
|
+
};
|
|
264
|
+
const { errors, warnings } = collectMessages(checks);
|
|
265
|
+
const ok = Object.values(checks).every((check) => check.ok);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
ok,
|
|
269
|
+
verdict: ok ? "go" : "no-go",
|
|
270
|
+
prdPath: absPrd,
|
|
271
|
+
repoRoot,
|
|
272
|
+
checks,
|
|
273
|
+
errors,
|
|
274
|
+
warnings,
|
|
275
|
+
plan: planToJson(plan),
|
|
276
|
+
leaseTable: buildLeaseTable(plan, repoRoot),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function formatPreflightReport(report) {
|
|
281
|
+
const lines = [`SWARM PREFLIGHT: ${report.verdict.toUpperCase()}`];
|
|
282
|
+
|
|
283
|
+
if (report.plan) {
|
|
284
|
+
lines.push(`PRD: ${report.prdPath}`);
|
|
285
|
+
lines.push(`Shards: ${report.plan.totalShards}`);
|
|
286
|
+
lines.push("");
|
|
287
|
+
lines.push("Lease table:");
|
|
288
|
+
for (const lease of report.leaseTable) {
|
|
289
|
+
const host = lease.host ? ` @${lease.host}` : "";
|
|
290
|
+
lines.push(` - ${lease.shard}${host}`);
|
|
291
|
+
lines.push(` worktree: ${lease.worktreePath}`);
|
|
292
|
+
lines.push(
|
|
293
|
+
` files: ${lease.files.length ? lease.files.join(", ") : "(none)"}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lines.push("");
|
|
299
|
+
lines.push("Checks:");
|
|
300
|
+
for (const [name, check] of Object.entries(report.checks || {})) {
|
|
301
|
+
lines.push(` ${check.ok ? "✓" : "✗"} ${name}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (report.errors?.length) {
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("Blocking errors:");
|
|
307
|
+
for (const error of report.errors) lines.push(` - ${error}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (report.warnings?.length) {
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push("Warnings:");
|
|
313
|
+
for (const warning of report.warnings) lines.push(` - ${warning}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return `${lines.join("\n")}\n`;
|
|
317
|
+
}
|