@triflux/remote 10.50.2 → 10.52.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 (134) hide show
  1. package/cto/brief.mjs +0 -20
  2. package/cto/collect.mjs +1 -200
  3. package/cto/current.schema.json +2 -23
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -187
  7. package/hub/index.mjs +0 -17
  8. package/hub/team/agent-map.json +0 -1
  9. package/hub/team/agents-row-attach.py +120 -0
  10. package/hub/team/agents-row.mjs +200 -0
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/claude-daemon-control.mjs +1 -2
  14. package/hub/team/claude-session-projection.mjs +5 -73
  15. package/hub/team/cli/commands/attach.mjs +1 -17
  16. package/hub/team/cli/commands/control.mjs +12 -38
  17. package/hub/team/cli/commands/debug.mjs +2 -39
  18. package/hub/team/cli/commands/focus.mjs +5 -31
  19. package/hub/team/cli/commands/interrupt.mjs +6 -25
  20. package/hub/team/cli/commands/kill.mjs +1 -30
  21. package/hub/team/cli/commands/list.mjs +0 -16
  22. package/hub/team/cli/commands/send.mjs +6 -26
  23. package/hub/team/cli/commands/start/index.mjs +42 -133
  24. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  25. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  26. package/hub/team/cli/commands/start/start-mux.mjs +2 -7
  27. package/hub/team/cli/commands/status.mjs +2 -57
  28. package/hub/team/cli/commands/stop.mjs +14 -19
  29. package/hub/team/cli/help.mjs +4 -5
  30. package/hub/team/cli/manifest.mjs +5 -1
  31. package/hub/team/cli/render.mjs +0 -12
  32. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  33. package/hub/team/cli/services/state-store.mjs +5 -20
  34. package/hub/team/handoff.mjs +3 -34
  35. package/hub/team/headless.mjs +108 -1296
  36. package/hub/team/orchestrator.mjs +4 -30
  37. package/hub/team/pane.mjs +6 -28
  38. package/hub/team/psmux.mjs +3 -34
  39. package/hub/team/remote-session.mjs +1 -2
  40. package/hub/team/session-context.mjs +1 -1
  41. package/hub/team/session.mjs +7 -91
  42. package/hub/team/tui-core.mjs +1 -1
  43. package/hub/team/tui-viewer.mjs +11 -58
  44. package/hub/team/tui.mjs +8 -306
  45. package/hub/team/uds-orchestrator.mjs +2 -580
  46. package/hub/team/wt-manager.mjs +2 -2
  47. package/hub/workers/factory.mjs +6 -84
  48. package/hub/workers/interface.mjs +1 -7
  49. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  50. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -8
  51. package/package.json +3 -8
  52. package/scripts/lib/agent-route-policy.mjs +143 -62
  53. package/scripts/lib/cli-claude.mjs +0 -14
  54. package/scripts/lib/env-probe.mjs +2 -131
  55. package/scripts/lib/gemini-profiles.mjs +2 -69
  56. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  57. package/scripts/lib/machine-profile.mjs +3 -17
  58. package/scripts/lib/mcp-filter.mjs +109 -53
  59. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  60. package/scripts/lib/mcp-health.mjs +2 -2
  61. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  62. package/scripts/lib/psmux-info.mjs +11 -59
  63. package/scripts/lib/skill-template.mjs +0 -245
  64. package/hub/hub-lifecycle.mjs +0 -248
  65. package/hub/pipe.mjs +0 -825
  66. package/hub/promote-penalties.mjs +0 -138
  67. package/hub/public/dashboard.html +0 -416
  68. package/hub/schema.sql +0 -209
  69. package/hub/server.mjs +0 -3048
  70. package/hub/store-adapter.mjs +0 -182
  71. package/hub/store.mjs +0 -1736
  72. package/hub/team/build-worker-prompt.mjs +0 -92
  73. package/hub/team/check-mcp-hub.mjs +0 -49
  74. package/hub/team/claude-native-bridge.mjs +0 -987
  75. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  76. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  77. package/hub/team/cli/services/hub-client.mjs +0 -264
  78. package/hub/team/cli/services/native-control.mjs +0 -145
  79. package/hub/team/codex-review.mjs +0 -520
  80. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  81. package/hub/team/conductor-registry.mjs +0 -121
  82. package/hub/team/conductor.mjs +0 -1529
  83. package/hub/team/consensus-meta.mjs +0 -176
  84. package/hub/team/cto-auto-collect.mjs +0 -115
  85. package/hub/team/cto-pull.mjs +0 -31
  86. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  87. package/hub/team/dashboard.mjs +0 -322
  88. package/hub/team/event-log.mjs +0 -84
  89. package/hub/team/execution-mode.mjs +0 -324
  90. package/hub/team/extract-completion-payload.mjs +0 -110
  91. package/hub/team/headless-bridge-session.mjs +0 -17
  92. package/hub/team/health-probe.mjs +0 -435
  93. package/hub/team/interactive-native-launcher.mjs +0 -119
  94. package/hub/team/interactive-native-worker.mjs +0 -80
  95. package/hub/team/interactive-tui-transport.mjs +0 -479
  96. package/hub/team/launcher-template.mjs +0 -144
  97. package/hub/team/lead-control.mjs +0 -191
  98. package/hub/team/mcp-selector.mjs +0 -145
  99. package/hub/team/native-supervisor.mjs +0 -460
  100. package/hub/team/native.mjs +0 -727
  101. package/hub/team/recovery-store.mjs +0 -149
  102. package/hub/team/remote-probe.mjs +0 -292
  103. package/hub/team/remote-watcher.mjs +0 -506
  104. package/hub/team/runtime-strategy.mjs +0 -200
  105. package/hub/team/schemas/worker-completion.json +0 -39
  106. package/hub/team/sentinel-capture.mjs +0 -127
  107. package/hub/team/session-sync.mjs +0 -316
  108. package/hub/team/swarm-cli.mjs +0 -390
  109. package/hub/team/swarm-hypervisor.mjs +0 -2206
  110. package/hub/team/swarm-intent.mjs +0 -216
  111. package/hub/team/swarm-locks.mjs +0 -335
  112. package/hub/team/swarm-planner.mjs +0 -371
  113. package/hub/team/swarm-preflight.mjs +0 -366
  114. package/hub/team/swarm-reconciler.mjs +0 -242
  115. package/hub/team/synapse-cli.mjs +0 -458
  116. package/hub/team/synapse-http.mjs +0 -311
  117. package/hub/team/synapse-registry.mjs +0 -618
  118. package/hub/team/tui-remote-adapter.mjs +0 -377
  119. package/hub/team/tui-synapse.mjs +0 -223
  120. package/hub/team/worker-completion-validator.mjs +0 -79
  121. package/hub/team/worker-sandbox.mjs +0 -125
  122. package/hub/team/worktree-lifecycle.mjs +0 -639
  123. package/hub/team/wt-templates.json +0 -43
  124. package/hub/tools.mjs +0 -900
  125. package/hub/workers/antigravity-route-worker.mjs +0 -179
  126. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  127. package/hub/workers/delegator-mcp.mjs +0 -1098
  128. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  129. package/scripts/lib/claudemd-scanner.mjs +0 -223
  130. package/scripts/lib/context.mjs +0 -67
  131. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  132. package/scripts/lib/handoff.mjs +0 -187
  133. package/scripts/lib/logger.mjs +0 -122
  134. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -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
- }