@triflux/remote 10.50.2 → 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.
Files changed (97) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +0 -73
  3. package/cto/current.schema.json +0 -4
  4. package/cto/status.mjs +2 -50
  5. package/hub/index.mjs +0 -8
  6. package/hub/team/agent-map.json +0 -1
  7. package/hub/team/ansi.mjs +1 -35
  8. package/hub/team/backend.mjs +9 -145
  9. package/hub/team/cli/commands/attach.mjs +1 -17
  10. package/hub/team/cli/commands/control.mjs +12 -38
  11. package/hub/team/cli/commands/debug.mjs +1 -37
  12. package/hub/team/cli/commands/focus.mjs +5 -31
  13. package/hub/team/cli/commands/interrupt.mjs +6 -25
  14. package/hub/team/cli/commands/kill.mjs +1 -30
  15. package/hub/team/cli/commands/list.mjs +0 -16
  16. package/hub/team/cli/commands/send.mjs +6 -26
  17. package/hub/team/cli/commands/start/index.mjs +41 -133
  18. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  19. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  20. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  21. package/hub/team/cli/commands/status.mjs +2 -57
  22. package/hub/team/cli/commands/stop.mjs +14 -19
  23. package/hub/team/cli/help.mjs +4 -5
  24. package/hub/team/cli/manifest.mjs +5 -1
  25. package/hub/team/cli/render.mjs +0 -12
  26. package/hub/team/cli/services/hub-client.mjs +2 -88
  27. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  28. package/hub/team/handoff.mjs +3 -34
  29. package/hub/team/headless.mjs +13 -754
  30. package/hub/team/orchestrator.mjs +4 -30
  31. package/hub/team/pane.mjs +6 -28
  32. package/hub/team/psmux.mjs +0 -1
  33. package/hub/team/remote-session.mjs +1 -2
  34. package/hub/team/session.mjs +7 -91
  35. package/hub/team/swarm-intent.mjs +0 -198
  36. package/hub/team/tui-core.mjs +1 -1
  37. package/hub/team/tui-viewer.mjs +2 -2
  38. package/hub/team/tui.mjs +8 -269
  39. package/hub/team/wt-manager.mjs +2 -2
  40. package/hub/workers/factory.mjs +4 -37
  41. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  42. package/package.json +1 -1
  43. package/scripts/lib/agent-route-policy.mjs +162 -14
  44. package/scripts/lib/claudemd-scanner.mjs +2 -199
  45. package/scripts/lib/cli-claude.mjs +0 -2
  46. package/scripts/lib/context.mjs +3 -19
  47. package/scripts/lib/env-probe.mjs +5 -64
  48. package/scripts/lib/gemini-profiles.mjs +2 -69
  49. package/scripts/lib/machine-profile.mjs +3 -17
  50. package/scripts/lib/mcp-filter.mjs +109 -53
  51. package/scripts/lib/mcp-health.mjs +2 -2
  52. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  53. package/scripts/lib/psmux-info.mjs +11 -59
  54. package/scripts/lib/skill-template.mjs +0 -245
  55. package/hub/team/build-worker-prompt.mjs +0 -92
  56. package/hub/team/check-mcp-hub.mjs +0 -49
  57. package/hub/team/claude-native-bridge.mjs +0 -987
  58. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  59. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  60. package/hub/team/cli/services/native-control.mjs +0 -145
  61. package/hub/team/codex-review.mjs +0 -520
  62. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  63. package/hub/team/conductor.mjs +0 -1529
  64. package/hub/team/consensus-meta.mjs +0 -176
  65. package/hub/team/cto-pull.mjs +0 -31
  66. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  67. package/hub/team/dashboard.mjs +0 -322
  68. package/hub/team/event-log.mjs +0 -84
  69. package/hub/team/execution-mode.mjs +0 -324
  70. package/hub/team/extract-completion-payload.mjs +0 -110
  71. package/hub/team/headless-bridge-session.mjs +0 -17
  72. package/hub/team/health-probe.mjs +0 -435
  73. package/hub/team/interactive-native-launcher.mjs +0 -119
  74. package/hub/team/interactive-native-worker.mjs +0 -80
  75. package/hub/team/interactive-tui-transport.mjs +0 -479
  76. package/hub/team/launcher-template.mjs +0 -144
  77. package/hub/team/mcp-selector.mjs +0 -145
  78. package/hub/team/native-supervisor.mjs +0 -460
  79. package/hub/team/native.mjs +0 -727
  80. package/hub/team/recovery-store.mjs +0 -149
  81. package/hub/team/remote-probe.mjs +0 -292
  82. package/hub/team/remote-watcher.mjs +0 -506
  83. package/hub/team/runtime-strategy.mjs +0 -200
  84. package/hub/team/schemas/worker-completion.json +0 -39
  85. package/hub/team/sentinel-capture.mjs +0 -127
  86. package/hub/team/swarm-cli.mjs +0 -390
  87. package/hub/team/swarm-hypervisor.mjs +0 -2206
  88. package/hub/team/swarm-locks.mjs +0 -335
  89. package/hub/team/swarm-planner.mjs +0 -371
  90. package/hub/team/swarm-preflight.mjs +0 -366
  91. package/hub/team/swarm-reconciler.mjs +0 -242
  92. package/hub/team/tui-remote-adapter.mjs +0 -377
  93. package/hub/team/worker-completion-validator.mjs +0 -79
  94. package/hub/team/worktree-lifecycle.mjs +0 -639
  95. package/hub/team/wt-templates.json +0 -43
  96. package/hub/workers/antigravity-route-worker.mjs +0 -179
  97. package/scripts/lib/dynamic-route-cli.mjs +0 -107
@@ -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
- }