@triflux/remote 10.50.1 → 10.51.0
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/cto/brief.mjs +0 -12
- package/cto/collect.mjs +16 -247
- package/cto/current.schema.json +4 -20
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +33 -128
- package/hub/index.mjs +0 -8
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/agent-map.json +0 -1
- package/hub/team/ansi.mjs +1 -35
- package/hub/team/backend.mjs +9 -145
- package/hub/team/cli/commands/attach.mjs +1 -17
- package/hub/team/cli/commands/control.mjs +12 -38
- package/hub/team/cli/commands/debug.mjs +1 -37
- package/hub/team/cli/commands/focus.mjs +5 -31
- package/hub/team/cli/commands/interrupt.mjs +6 -25
- package/hub/team/cli/commands/kill.mjs +1 -30
- package/hub/team/cli/commands/list.mjs +0 -16
- package/hub/team/cli/commands/send.mjs +6 -26
- package/hub/team/cli/commands/start/index.mjs +41 -133
- package/hub/team/cli/commands/start/parse-args.mjs +15 -40
- package/hub/team/cli/commands/start/start-headless.mjs +138 -105
- package/hub/team/cli/commands/start/start-mux.mjs +0 -3
- package/hub/team/cli/commands/status.mjs +2 -57
- package/hub/team/cli/commands/stop.mjs +14 -19
- package/hub/team/cli/help.mjs +4 -5
- package/hub/team/cli/manifest.mjs +5 -1
- package/hub/team/cli/render.mjs +0 -12
- package/hub/team/cli/services/hub-client.mjs +2 -88
- package/hub/team/cli/services/runtime-mode.mjs +27 -98
- package/hub/team/handoff.mjs +3 -34
- package/hub/team/headless.mjs +14 -755
- package/hub/team/orchestrator.mjs +4 -30
- package/hub/team/pane.mjs +6 -28
- package/hub/team/psmux.mjs +16 -403
- package/hub/team/remote-session.mjs +1 -2
- package/hub/team/session.mjs +8 -119
- package/hub/team/swarm-intent.mjs +0 -198
- package/hub/team/terminal-opener.mjs +11 -35
- package/hub/team/tui-core.mjs +1 -1
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +8 -269
- package/hub/team/wt-manager.mjs +2 -2
- package/hub/workers/factory.mjs +4 -37
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/skill-template.mjs +0 -271
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- package/hub/team/build-worker-prompt.mjs +0 -92
- package/hub/team/check-mcp-hub.mjs +0 -49
- package/hub/team/claude-native-bridge.mjs +0 -987
- package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
- package/hub/team/cli/commands/start/start-wt.mjs +0 -86
- package/hub/team/cli/services/native-control.mjs +0 -145
- package/hub/team/codex-review.mjs +0 -520
- package/hub/team/conductor-mesh-bridge.mjs +0 -122
- package/hub/team/conductor.mjs +0 -1529
- package/hub/team/consensus-meta.mjs +0 -176
- package/hub/team/cto-pull.mjs +0 -31
- package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
- package/hub/team/dashboard.mjs +0 -322
- package/hub/team/event-log.mjs +0 -84
- package/hub/team/execution-mode.mjs +0 -324
- package/hub/team/extract-completion-payload.mjs +0 -110
- package/hub/team/headless-bridge-session.mjs +0 -17
- package/hub/team/health-probe.mjs +0 -435
- package/hub/team/interactive-native-launcher.mjs +0 -119
- package/hub/team/interactive-native-worker.mjs +0 -80
- package/hub/team/interactive-tui-transport.mjs +0 -479
- package/hub/team/launcher-template.mjs +0 -144
- package/hub/team/mcp-selector.mjs +0 -145
- package/hub/team/native-supervisor.mjs +0 -460
- package/hub/team/native.mjs +0 -727
- package/hub/team/recovery-store.mjs +0 -149
- package/hub/team/remote-probe.mjs +0 -292
- package/hub/team/remote-watcher.mjs +0 -506
- package/hub/team/runtime-strategy.mjs +0 -200
- package/hub/team/schemas/worker-completion.json +0 -39
- package/hub/team/sentinel-capture.mjs +0 -127
- package/hub/team/swarm-cli.mjs +0 -390
- package/hub/team/swarm-hypervisor.mjs +0 -2206
- package/hub/team/swarm-locks.mjs +0 -335
- package/hub/team/swarm-planner.mjs +0 -371
- package/hub/team/swarm-preflight.mjs +0 -366
- package/hub/team/swarm-reconciler.mjs +0 -242
- package/hub/team/tui-remote-adapter.mjs +0 -377
- package/hub/team/worker-completion-validator.mjs +0 -79
- package/hub/team/worktree-lifecycle.mjs +0 -639
- package/hub/team/wt-templates.json +0 -43
- package/hub/workers/antigravity-route-worker.mjs +0 -179
- package/hub/workers/gemini-worker.mjs +0 -217
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,366 +0,0 @@
|
|
|
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 { execFileSync } from "node:child_process";
|
|
5
|
-
import { resolve } from "node:path";
|
|
6
|
-
import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
7
|
-
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
8
|
-
import { MCP_CATALOG } from "./mcp-selector.mjs";
|
|
9
|
-
import { DEFAULT_SENSITIVE_DENY } from "./swarm-locks.mjs";
|
|
10
|
-
import { planSwarm } from "./swarm-planner.mjs";
|
|
11
|
-
|
|
12
|
-
const AGENT_COMMANDS = Object.freeze({
|
|
13
|
-
codex: "codex",
|
|
14
|
-
gemini: "agy",
|
|
15
|
-
antigravity: "agy",
|
|
16
|
-
agy: "agy",
|
|
17
|
-
claude: "claude",
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function unique(values) {
|
|
21
|
-
return [...new Set(values.filter(Boolean))];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizePath(filePath) {
|
|
25
|
-
return String(filePath || "")
|
|
26
|
-
.replace(/\\/g, "/")
|
|
27
|
-
.replace(/^\.\//u, "");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function isSensitiveFile(filePath, sensitiveDeny = DEFAULT_SENSITIVE_DENY) {
|
|
31
|
-
const path = normalizePath(filePath);
|
|
32
|
-
const files = new Set(sensitiveDeny.files || []);
|
|
33
|
-
const prefixes = sensitiveDeny.prefixes || [];
|
|
34
|
-
return files.has(path) || prefixes.some((prefix) => path.startsWith(prefix));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function planToJson(plan) {
|
|
38
|
-
return {
|
|
39
|
-
totalShards: plan.shards.length,
|
|
40
|
-
shards: plan.shards.map((s) => ({
|
|
41
|
-
name: s.name,
|
|
42
|
-
agent: s.agent,
|
|
43
|
-
host: s.host || null,
|
|
44
|
-
files: [...s.files],
|
|
45
|
-
mcp: [...s.mcp],
|
|
46
|
-
depends: [...s.depends],
|
|
47
|
-
critical: s.critical,
|
|
48
|
-
})),
|
|
49
|
-
leaseMap: Object.fromEntries(plan.leaseMap),
|
|
50
|
-
mcpManifest: Object.fromEntries(plan.mcpManifest),
|
|
51
|
-
mergeOrder: [...plan.mergeOrder],
|
|
52
|
-
criticalShards: [...plan.criticalShards],
|
|
53
|
-
conflicts: [...plan.conflicts],
|
|
54
|
-
remoteSuggestion: plan.remoteSuggestion,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function makeCheck(name, ok, details = {}) {
|
|
59
|
-
return { name, ok: Boolean(ok), ...details };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildLeaseTable(plan, repoRoot) {
|
|
63
|
-
return plan.shards.map((shard) => ({
|
|
64
|
-
shard: shard.name,
|
|
65
|
-
host: shard.host || null,
|
|
66
|
-
worktreePath: resolve(repoRoot, ".codex-swarm", `wt-${shard.name}`),
|
|
67
|
-
files: [...shard.files],
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function checkSensitive(plan, sensitiveDeny) {
|
|
72
|
-
const matches = [];
|
|
73
|
-
for (const shard of plan.shards) {
|
|
74
|
-
for (const file of shard.files) {
|
|
75
|
-
if (!isSensitiveFile(file, sensitiveDeny)) continue;
|
|
76
|
-
matches.push({
|
|
77
|
-
shard: shard.name,
|
|
78
|
-
file,
|
|
79
|
-
kind: "sensitive-planned-lease",
|
|
80
|
-
severity: "warning",
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return makeCheck("sensitiveDeny", true, { matches });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function parseTrackedStatus(rawStatus) {
|
|
88
|
-
const files = [];
|
|
89
|
-
const records = String(rawStatus || "").split("\0");
|
|
90
|
-
for (let index = 0; index < records.length; index++) {
|
|
91
|
-
const record = records[index];
|
|
92
|
-
if (record.length < 4) continue;
|
|
93
|
-
const status = record.slice(0, 2);
|
|
94
|
-
files.push(normalizePath(record.slice(3)));
|
|
95
|
-
if (/[RC]/u.test(status) && records[index + 1]) {
|
|
96
|
-
files.push(normalizePath(records[++index]));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return unique(files);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function checkRootDirtyLease(plan, repoRoot, deps = {}) {
|
|
103
|
-
const gitStatus =
|
|
104
|
-
deps.gitStatus ||
|
|
105
|
-
((cwd) =>
|
|
106
|
-
execFileSync(
|
|
107
|
-
"git",
|
|
108
|
-
["status", "--porcelain=v1", "-z", "--untracked-files=no"],
|
|
109
|
-
{ cwd, encoding: "utf8", windowsHide: true },
|
|
110
|
-
));
|
|
111
|
-
const dirtyFiles = parseTrackedStatus(gitStatus(repoRoot));
|
|
112
|
-
const leasedFiles = unique(
|
|
113
|
-
[...plan.leaseMap.values()].flat().map(normalizePath),
|
|
114
|
-
);
|
|
115
|
-
const leaseSet = new Set(leasedFiles);
|
|
116
|
-
const overlappingFiles = dirtyFiles.filter((file) => leaseSet.has(file));
|
|
117
|
-
|
|
118
|
-
return makeCheck("rootDirtyLease", overlappingFiles.length === 0, {
|
|
119
|
-
dirtyFiles,
|
|
120
|
-
leasedFiles,
|
|
121
|
-
overlappingFiles,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function checkHosts(plan, repoRoot) {
|
|
126
|
-
const registry = readHosts(repoRoot);
|
|
127
|
-
const missing = [];
|
|
128
|
-
const capabilityWarnings = [];
|
|
129
|
-
|
|
130
|
-
for (const shard of plan.shards) {
|
|
131
|
-
if (!shard.host) continue;
|
|
132
|
-
const resolvedHost = resolveHost(shard.host, repoRoot);
|
|
133
|
-
if (!resolvedHost) {
|
|
134
|
-
missing.push({ shard: shard.name, host: shard.host });
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const capabilities = resolvedHost.host.capabilities || [];
|
|
138
|
-
if (shard.agent && !capabilities.includes(shard.agent)) {
|
|
139
|
-
capabilityWarnings.push({
|
|
140
|
-
shard: shard.name,
|
|
141
|
-
host: shard.host,
|
|
142
|
-
agent: shard.agent,
|
|
143
|
-
capabilities,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return makeCheck("hosts", missing.length === 0, {
|
|
149
|
-
registryPath: registry.path,
|
|
150
|
-
missing,
|
|
151
|
-
warnings: capabilityWarnings,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function checkLocalWorkerClis(plan, deps = {}) {
|
|
156
|
-
const which = deps.whichCommand || whichCommand;
|
|
157
|
-
const missing = [];
|
|
158
|
-
const present = [];
|
|
159
|
-
const checked = new Map();
|
|
160
|
-
|
|
161
|
-
for (const shard of plan.shards) {
|
|
162
|
-
if (shard.host) continue;
|
|
163
|
-
const command = AGENT_COMMANDS[shard.agent] || shard.agent;
|
|
164
|
-
if (!command) {
|
|
165
|
-
missing.push({ shard: shard.name, agent: shard.agent, command: null });
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (!checked.has(command)) {
|
|
169
|
-
checked.set(command, which(command, deps.whichOptions || {}));
|
|
170
|
-
}
|
|
171
|
-
const path = checked.get(command);
|
|
172
|
-
if (path)
|
|
173
|
-
present.push({ shard: shard.name, agent: shard.agent, command, path });
|
|
174
|
-
else missing.push({ shard: shard.name, agent: shard.agent, command });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return makeCheck("workerCli", missing.length === 0, { present, missing });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function checkMcp(plan) {
|
|
181
|
-
const catalog = new Map(MCP_CATALOG.map((entry) => [entry.name, entry]));
|
|
182
|
-
const unknown = [];
|
|
183
|
-
const incompatible = [];
|
|
184
|
-
|
|
185
|
-
for (const shard of plan.shards) {
|
|
186
|
-
for (const server of shard.mcp) {
|
|
187
|
-
const entry = catalog.get(server);
|
|
188
|
-
if (!entry) {
|
|
189
|
-
unknown.push({ shard: shard.name, server });
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (
|
|
193
|
-
["codex", "gemini"].includes(shard.agent) &&
|
|
194
|
-
!entry.cli.includes(shard.agent)
|
|
195
|
-
) {
|
|
196
|
-
incompatible.push({
|
|
197
|
-
shard: shard.name,
|
|
198
|
-
agent: shard.agent,
|
|
199
|
-
server,
|
|
200
|
-
supportedCli: [...entry.cli],
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return makeCheck("mcp", incompatible.length === 0, { unknown, incompatible });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function collectMessages(checks) {
|
|
210
|
-
const errors = [];
|
|
211
|
-
const warnings = [];
|
|
212
|
-
|
|
213
|
-
const leases = checks.leases;
|
|
214
|
-
for (const conflict of leases.conflicts || []) {
|
|
215
|
-
errors.push(
|
|
216
|
-
`lease conflict: ${conflict.file} assigned to ${conflict.shards.join(", ")}`,
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const hosts = checks.hosts;
|
|
221
|
-
for (const missing of hosts.missing || []) {
|
|
222
|
-
errors.push(
|
|
223
|
-
`missing host: shard ${missing.shard} references ${missing.host}`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
for (const warn of hosts.warnings || []) {
|
|
227
|
-
warnings.push(
|
|
228
|
-
`host capability warning: ${warn.host} does not advertise ${warn.agent} for shard ${warn.shard}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const workerCli = checks.workerCli;
|
|
233
|
-
for (const missing of workerCli.missing || []) {
|
|
234
|
-
errors.push(
|
|
235
|
-
`missing local worker CLI: shard ${missing.shard} requires ${missing.command || missing.agent}`,
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const mcp = checks.mcp;
|
|
240
|
-
for (const item of mcp.incompatible || []) {
|
|
241
|
-
errors.push(
|
|
242
|
-
`MCP incompatibility: ${item.server} is not supported by ${item.agent} for shard ${item.shard}`,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
for (const item of mcp.unknown || []) {
|
|
246
|
-
warnings.push(
|
|
247
|
-
`unknown MCP server: ${item.server} requested by shard ${item.shard}`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const sensitive = checks.sensitiveDeny;
|
|
252
|
-
for (const item of sensitive.matches || []) {
|
|
253
|
-
warnings.push(
|
|
254
|
-
`sensitive path planned lease: ${item.file} owned by shard ${item.shard}`,
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const rootDirtyLease = checks.rootDirtyLease;
|
|
259
|
-
if (rootDirtyLease && !rootDirtyLease.ok) {
|
|
260
|
-
errors.push(
|
|
261
|
-
`rootDir tracked changes overlap shard leases: ${rootDirtyLease.overlappingFiles.join(", ")}`,
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return { errors: unique(errors), warnings: unique(warnings) };
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Build a no-side-effect preflight report for a swarm PRD.
|
|
270
|
-
*
|
|
271
|
-
* @param {string} prdPath
|
|
272
|
-
* @param {object} [opts]
|
|
273
|
-
* @param {string} [opts.repoRoot]
|
|
274
|
-
* @param {object} [opts.deps]
|
|
275
|
-
* @returns {object}
|
|
276
|
-
*/
|
|
277
|
-
export function runSwarmPreflight(prdPath, opts = {}) {
|
|
278
|
-
const repoRoot = opts.repoRoot || process.cwd();
|
|
279
|
-
const absPrd = resolve(prdPath);
|
|
280
|
-
const sensitiveDeny = opts.sensitiveDeny || DEFAULT_SENSITIVE_DENY;
|
|
281
|
-
let plan;
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
plan = (opts.deps?.planSwarm || planSwarm)(absPrd, { repoRoot });
|
|
285
|
-
} catch (error) {
|
|
286
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
287
|
-
return {
|
|
288
|
-
ok: false,
|
|
289
|
-
verdict: "no-go",
|
|
290
|
-
prdPath: absPrd,
|
|
291
|
-
repoRoot,
|
|
292
|
-
checks: {
|
|
293
|
-
plan: makeCheck("plan", false, { error: message }),
|
|
294
|
-
},
|
|
295
|
-
errors: [message],
|
|
296
|
-
warnings: [],
|
|
297
|
-
plan: null,
|
|
298
|
-
leaseTable: [],
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const checks = {
|
|
303
|
-
plan: makeCheck("plan", true),
|
|
304
|
-
leases: makeCheck("leases", plan.conflicts.length === 0, {
|
|
305
|
-
conflicts: [...plan.conflicts],
|
|
306
|
-
}),
|
|
307
|
-
rootDirtyLease: checkRootDirtyLease(plan, repoRoot, opts.deps),
|
|
308
|
-
sensitiveDeny: checkSensitive(plan, sensitiveDeny),
|
|
309
|
-
hosts: checkHosts(plan, repoRoot),
|
|
310
|
-
workerCli: checkLocalWorkerClis(plan, opts.deps),
|
|
311
|
-
mcp: checkMcp(plan),
|
|
312
|
-
};
|
|
313
|
-
const { errors, warnings } = collectMessages(checks);
|
|
314
|
-
const ok = Object.values(checks).every((check) => check.ok);
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
ok,
|
|
318
|
-
verdict: ok ? "go" : "no-go",
|
|
319
|
-
prdPath: absPrd,
|
|
320
|
-
repoRoot,
|
|
321
|
-
checks,
|
|
322
|
-
errors,
|
|
323
|
-
warnings,
|
|
324
|
-
plan: planToJson(plan),
|
|
325
|
-
leaseTable: buildLeaseTable(plan, repoRoot),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export function formatPreflightReport(report) {
|
|
330
|
-
const lines = [`SWARM PREFLIGHT: ${report.verdict.toUpperCase()}`];
|
|
331
|
-
|
|
332
|
-
if (report.plan) {
|
|
333
|
-
lines.push(`PRD: ${report.prdPath}`);
|
|
334
|
-
lines.push(`Shards: ${report.plan.totalShards}`);
|
|
335
|
-
lines.push("");
|
|
336
|
-
lines.push("Lease table:");
|
|
337
|
-
for (const lease of report.leaseTable) {
|
|
338
|
-
const host = lease.host ? ` @${lease.host}` : "";
|
|
339
|
-
lines.push(` - ${lease.shard}${host}`);
|
|
340
|
-
lines.push(` worktree: ${lease.worktreePath}`);
|
|
341
|
-
lines.push(
|
|
342
|
-
` files: ${lease.files.length ? lease.files.join(", ") : "(none)"}`,
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
lines.push("");
|
|
348
|
-
lines.push("Checks:");
|
|
349
|
-
for (const [name, check] of Object.entries(report.checks || {})) {
|
|
350
|
-
lines.push(` ${check.ok ? "✓" : "✗"} ${name}`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (report.errors?.length) {
|
|
354
|
-
lines.push("");
|
|
355
|
-
lines.push("Blocking errors:");
|
|
356
|
-
for (const error of report.errors) lines.push(` - ${error}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (report.warnings?.length) {
|
|
360
|
-
lines.push("");
|
|
361
|
-
lines.push("Warnings:");
|
|
362
|
-
for (const warning of report.warnings) lines.push(` - ${warning}`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return `${lines.join("\n")}\n`;
|
|
366
|
-
}
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
// hub/team/swarm-reconciler.mjs — Redundant execution + result reconciliation
|
|
2
|
-
// For critical shards: launches primary + verifier sessions, compares results,
|
|
3
|
-
// applies conservative adoption (fewer changes wins) or HITL fallback.
|
|
4
|
-
// v2 (Synapse): parses X-Intent trailers to route complementary/contradictory
|
|
5
|
-
// commits through intent-aware paths before falling back to conservative logic.
|
|
6
|
-
|
|
7
|
-
import { execFile } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
import { classifyIntentPair, parseIntentTrailer } from "./swarm-intent.mjs";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Compare two shard results and decide which to accept.
|
|
13
|
-
* Strategy: conservative adoption — the result with fewer changed files wins.
|
|
14
|
-
* If results are identical (same diff), primary is accepted.
|
|
15
|
-
* If they diverge significantly, mark for HITL review.
|
|
16
|
-
*
|
|
17
|
-
* @param {object} primaryResult — { branchName, worktreePath, status }
|
|
18
|
-
* @param {object} verifierResult — { branchName, worktreePath, status }
|
|
19
|
-
* @param {object} [opts]
|
|
20
|
-
* @param {string} [opts.rootDir=process.cwd()]
|
|
21
|
-
* @param {number} [opts.maxDivergenceFiles=5] — beyond this, trigger HITL
|
|
22
|
-
* @returns {Promise<ReconcileDecision>}
|
|
23
|
-
*/
|
|
24
|
-
export async function reconcile(primaryResult, verifierResult, opts = {}) {
|
|
25
|
-
const { rootDir = process.cwd(), maxDivergenceFiles = 5 } = opts;
|
|
26
|
-
|
|
27
|
-
// If either failed, pick the one that succeeded
|
|
28
|
-
if (
|
|
29
|
-
primaryResult.status !== "completed" &&
|
|
30
|
-
verifierResult.status === "completed"
|
|
31
|
-
) {
|
|
32
|
-
return decision("verifier", "primary_failed", verifierResult);
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
verifierResult.status !== "completed" &&
|
|
36
|
-
primaryResult.status === "completed"
|
|
37
|
-
) {
|
|
38
|
-
return decision("primary", "verifier_failed", primaryResult);
|
|
39
|
-
}
|
|
40
|
-
if (
|
|
41
|
-
primaryResult.status !== "completed" &&
|
|
42
|
-
verifierResult.status !== "completed"
|
|
43
|
-
) {
|
|
44
|
-
return decision("none", "both_failed", null);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Both completed — compare diffs
|
|
48
|
-
const primaryDiff = await getDiffStat(primaryResult.branchName, rootDir);
|
|
49
|
-
const verifierDiff = await getDiffStat(verifierResult.branchName, rootDir);
|
|
50
|
-
|
|
51
|
-
// Identical diffs → accept primary (no divergence)
|
|
52
|
-
if (primaryDiff.hash === verifierDiff.hash) {
|
|
53
|
-
return decision("primary", "identical", primaryResult);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Intent-aware classification: parse X-Intent trailers from both commits.
|
|
57
|
-
// When both commits carry an intent, route contradictory pairs to HITL and
|
|
58
|
-
// complementary (non-overlapping) pairs to a merge-friendly path. Missing or
|
|
59
|
-
// malformed trailers fall through to the existing divergence/conservative
|
|
60
|
-
// adoption logic below.
|
|
61
|
-
const [primaryMsg, verifierMsg] = await Promise.all([
|
|
62
|
-
getCommitMessage(primaryResult.branchName, rootDir),
|
|
63
|
-
getCommitMessage(verifierResult.branchName, rootDir),
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
const primaryIntent = parseIntentTrailer(primaryMsg);
|
|
67
|
-
const verifierIntent = parseIntentTrailer(verifierMsg);
|
|
68
|
-
|
|
69
|
-
if (primaryIntent && verifierIntent) {
|
|
70
|
-
const classification = classifyIntentPair(primaryIntent, verifierIntent);
|
|
71
|
-
|
|
72
|
-
if (classification.relation === "contradictory") {
|
|
73
|
-
return {
|
|
74
|
-
selected: "hitl",
|
|
75
|
-
reason: `intent_contradictory: ${classification.reason}`,
|
|
76
|
-
result: null,
|
|
77
|
-
requiresManualReview: true,
|
|
78
|
-
intentClassification: classification,
|
|
79
|
-
primaryIntent,
|
|
80
|
-
verifierIntent,
|
|
81
|
-
primary: {
|
|
82
|
-
filesChanged: primaryDiff.filesChanged,
|
|
83
|
-
linesChanged: primaryDiff.linesChanged,
|
|
84
|
-
},
|
|
85
|
-
verifier: {
|
|
86
|
-
filesChanged: verifierDiff.filesChanged,
|
|
87
|
-
linesChanged: verifierDiff.linesChanged,
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (classification.relation === "complementary") {
|
|
93
|
-
return {
|
|
94
|
-
selected: "complementary",
|
|
95
|
-
reason: `intent_complementary: ${classification.reason}`,
|
|
96
|
-
result: { primary: primaryResult, verifier: verifierResult },
|
|
97
|
-
requiresManualReview: false,
|
|
98
|
-
intentClassification: classification,
|
|
99
|
-
primaryIntent,
|
|
100
|
-
verifierIntent,
|
|
101
|
-
shouldAttemptMerge: true,
|
|
102
|
-
primary: {
|
|
103
|
-
filesChanged: primaryDiff.filesChanged,
|
|
104
|
-
linesChanged: primaryDiff.linesChanged,
|
|
105
|
-
},
|
|
106
|
-
verifier: {
|
|
107
|
-
filesChanged: verifierDiff.filesChanged,
|
|
108
|
-
linesChanged: verifierDiff.linesChanged,
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// complementary-risky / independent → fall through to existing logic.
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Compute divergence
|
|
117
|
-
const divergence = Math.abs(
|
|
118
|
-
primaryDiff.filesChanged - verifierDiff.filesChanged,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// High divergence → HITL
|
|
122
|
-
if (divergence > maxDivergenceFiles) {
|
|
123
|
-
return {
|
|
124
|
-
selected: "hitl",
|
|
125
|
-
reason: `divergence_too_high (${divergence} files differ)`,
|
|
126
|
-
result: null,
|
|
127
|
-
requiresManualReview: true,
|
|
128
|
-
primary: {
|
|
129
|
-
filesChanged: primaryDiff.filesChanged,
|
|
130
|
-
linesChanged: primaryDiff.linesChanged,
|
|
131
|
-
},
|
|
132
|
-
verifier: {
|
|
133
|
-
filesChanged: verifierDiff.filesChanged,
|
|
134
|
-
linesChanged: verifierDiff.linesChanged,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Conservative adoption: fewer changes wins
|
|
140
|
-
if (primaryDiff.linesChanged <= verifierDiff.linesChanged) {
|
|
141
|
-
return decision("primary", "conservative_adoption", primaryResult);
|
|
142
|
-
}
|
|
143
|
-
return decision("verifier", "conservative_adoption", verifierResult);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function decision(selected, reason, result) {
|
|
147
|
-
return {
|
|
148
|
-
selected,
|
|
149
|
-
reason,
|
|
150
|
-
result,
|
|
151
|
-
requiresManualReview: selected === "hitl" || selected === "none",
|
|
152
|
-
primary: null,
|
|
153
|
-
verifier: null,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Read the HEAD commit message of a branch.
|
|
159
|
-
*
|
|
160
|
-
* @param {string} branch
|
|
161
|
-
* @param {string} cwd
|
|
162
|
-
* @returns {Promise<string>}
|
|
163
|
-
*/
|
|
164
|
-
async function getCommitMessage(branch, cwd) {
|
|
165
|
-
try {
|
|
166
|
-
const msg = await gitExec(["log", "-1", "--format=%B", branch], cwd);
|
|
167
|
-
return msg;
|
|
168
|
-
} catch {
|
|
169
|
-
return "";
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get diff statistics for a branch relative to its merge-base.
|
|
175
|
-
*
|
|
176
|
-
* @param {string} branch
|
|
177
|
-
* @param {string} cwd
|
|
178
|
-
* @returns {Promise<{ filesChanged: number, linesChanged: number, hash: string }>}
|
|
179
|
-
*/
|
|
180
|
-
async function getDiffStat(branch, cwd) {
|
|
181
|
-
try {
|
|
182
|
-
const stat = await gitExec(
|
|
183
|
-
["diff", "--stat", "--numstat", `${branch}~1..${branch}`],
|
|
184
|
-
cwd,
|
|
185
|
-
);
|
|
186
|
-
const lines = stat.split("\n").filter(Boolean);
|
|
187
|
-
let filesChanged = 0;
|
|
188
|
-
let linesChanged = 0;
|
|
189
|
-
|
|
190
|
-
for (const line of lines) {
|
|
191
|
-
const match = line.match(/^(\d+)\s+(\d+)\s+/);
|
|
192
|
-
if (match) {
|
|
193
|
-
filesChanged++;
|
|
194
|
-
linesChanged += Number(match[1]) + Number(match[2]);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Get tree hash for identity comparison
|
|
199
|
-
const hash = await gitExec(["rev-parse", `${branch}^{tree}`], cwd);
|
|
200
|
-
|
|
201
|
-
return { filesChanged, linesChanged, hash: hash.trim() };
|
|
202
|
-
} catch {
|
|
203
|
-
return { filesChanged: 0, linesChanged: 0, hash: "" };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function gitExec(args, cwd) {
|
|
208
|
-
return new Promise((res, rej) => {
|
|
209
|
-
execFile(
|
|
210
|
-
"git",
|
|
211
|
-
args,
|
|
212
|
-
{ cwd, windowsHide: true, timeout: 15_000 },
|
|
213
|
-
(err, stdout) => {
|
|
214
|
-
if (err) rej(err);
|
|
215
|
-
else res(stdout);
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Build session configs for redundant execution (primary + verifier).
|
|
223
|
-
*
|
|
224
|
-
* @param {object} shard — from SwarmPlan
|
|
225
|
-
* @param {string} runId
|
|
226
|
-
* @returns {{ primaryId: string, verifierId: string }}
|
|
227
|
-
*/
|
|
228
|
-
export function buildRedundantIds(shard, runId) {
|
|
229
|
-
return {
|
|
230
|
-
primaryId: `${runId}-${shard.id}-primary`,
|
|
231
|
-
verifierId: `${runId}-${shard.id}-verifier`,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Check if a shard should use redundant execution.
|
|
237
|
-
* @param {object} shard
|
|
238
|
-
* @returns {boolean}
|
|
239
|
-
*/
|
|
240
|
-
export function shouldRunRedundant(shard) {
|
|
241
|
-
return shard.critical === true;
|
|
242
|
-
}
|