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