@triflux/remote 10.1.0 → 10.25.1

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 (155) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2165 -708
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +84 -17
  9. package/hub/team/build-worker-prompt.mjs +42 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +219 -0
  12. package/hub/team/claude-native-bridge.mjs +784 -0
  13. package/hub/team/claude-session-projection.mjs +65 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +154 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +89 -12
  24. package/hub/team/cli/commands/start/start-headless.mjs +125 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/index.mjs +2 -2
  33. package/hub/team/cli/manifest.mjs +38 -8
  34. package/hub/team/cli/render.mjs +52 -30
  35. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  36. package/hub/team/cli/services/hub-client.mjs +44 -16
  37. package/hub/team/cli/services/member-selector.mjs +37 -30
  38. package/hub/team/cli/services/native-control.mjs +144 -116
  39. package/hub/team/cli/services/runtime-mode.mjs +55 -1
  40. package/hub/team/cli/services/state-store.mjs +66 -9
  41. package/hub/team/cli/services/task-model.mjs +40 -30
  42. package/hub/team/codex-review.mjs +521 -0
  43. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  44. package/hub/team/conductor-registry.mjs +121 -0
  45. package/hub/team/conductor.mjs +1379 -671
  46. package/hub/team/consensus-meta.mjs +176 -0
  47. package/hub/team/dashboard-anchor.mjs +4 -5
  48. package/hub/team/dashboard-layout.mjs +3 -1
  49. package/hub/team/dashboard-open.mjs +20 -118
  50. package/hub/team/dashboard.mjs +76 -28
  51. package/hub/team/event-log.mjs +18 -10
  52. package/hub/team/execution-mode.mjs +241 -0
  53. package/hub/team/extract-completion-payload.mjs +110 -0
  54. package/hub/team/git-preflight.mjs +286 -0
  55. package/hub/team/handoff.mjs +49 -18
  56. package/hub/team/headless-bridge-session.mjs +17 -0
  57. package/hub/team/headless.mjs +2065 -1156
  58. package/hub/team/health-probe.mjs +232 -69
  59. package/hub/team/launcher-template.mjs +23 -13
  60. package/hub/team/lead-control.mjs +91 -4
  61. package/hub/team/mcp-selector.mjs +145 -0
  62. package/hub/team/native-supervisor.mjs +90 -26
  63. package/hub/team/native.mjs +74 -35
  64. package/hub/team/nativeProxy.mjs +184 -113
  65. package/hub/team/notify.mjs +147 -78
  66. package/hub/team/orchestrator.mjs +173 -161
  67. package/hub/team/pane.mjs +43 -15
  68. package/hub/team/process-cleanup.mjs +69 -25
  69. package/hub/team/psmux.mjs +1020 -279
  70. package/hub/team/recovery-store.mjs +149 -0
  71. package/hub/team/remote-probe.mjs +68 -52
  72. package/hub/team/remote-session.mjs +131 -59
  73. package/hub/team/remote-watcher.mjs +61 -33
  74. package/hub/team/retry-state-machine.mjs +267 -0
  75. package/hub/team/routing.mjs +51 -25
  76. package/hub/team/runtime-strategy.mjs +200 -0
  77. package/hub/team/schemas/worker-completion.json +39 -0
  78. package/hub/team/sentinel-capture.mjs +127 -0
  79. package/hub/team/session-sync.mjs +153 -6
  80. package/hub/team/session.mjs +605 -611
  81. package/hub/team/shared.mjs +13 -13
  82. package/hub/team/staleState.mjs +72 -30
  83. package/hub/team/swarm-cli.mjs +390 -0
  84. package/hub/team/swarm-hypervisor.mjs +1522 -193
  85. package/hub/team/swarm-intent.mjs +216 -0
  86. package/hub/team/swarm-locks.mjs +140 -25
  87. package/hub/team/swarm-planner.mjs +121 -21
  88. package/hub/team/swarm-preflight.mjs +317 -0
  89. package/hub/team/swarm-reconciler.mjs +128 -23
  90. package/hub/team/synapse-cli.mjs +458 -0
  91. package/hub/team/synapse-http.mjs +60 -0
  92. package/hub/team/synapse-registry.mjs +329 -0
  93. package/hub/team/terminal-opener.mjs +216 -0
  94. package/hub/team/tui-core.mjs +288 -0
  95. package/hub/team/tui-lite.mjs +234 -146
  96. package/hub/team/tui-remote-adapter.mjs +18 -34
  97. package/hub/team/tui-synapse.mjs +223 -0
  98. package/hub/team/tui-viewer.mjs +104 -44
  99. package/hub/team/tui-widgets.mjs +262 -0
  100. package/hub/team/tui.mjs +927 -437
  101. package/hub/team/worker-completion-validator.mjs +84 -0
  102. package/hub/team/worker-sandbox.mjs +93 -0
  103. package/hub/team/worker-signal.mjs +263 -0
  104. package/hub/team/worker-signal.types.d.ts +52 -0
  105. package/hub/team/worktree-lifecycle.mjs +517 -63
  106. package/hub/team/wt-manager.mjs +297 -26
  107. package/hub/tools.mjs +474 -252
  108. package/hub/tray.mjs +411 -368
  109. package/hub/workers/claude-worker.mjs +162 -118
  110. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  111. package/hub/workers/codex-mcp.mjs +724 -507
  112. package/hub/workers/delegator-mcp.mjs +662 -335
  113. package/hub/workers/factory.mjs +100 -21
  114. package/hub/workers/gemini-worker.mjs +179 -86
  115. package/hub/workers/interface.mjs +23 -2
  116. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  117. package/hub/workers/worker-utils.mjs +22 -16
  118. package/package.json +4 -3
  119. package/scripts/lib/agent-json.mjs +27 -0
  120. package/scripts/lib/async.mjs +174 -0
  121. package/scripts/lib/claudemd-scanner.mjs +223 -0
  122. package/scripts/lib/cli-agy.mjs +62 -0
  123. package/scripts/lib/cli-claude.mjs +78 -0
  124. package/scripts/lib/cli-codex.mjs +199 -0
  125. package/scripts/lib/cli-gemini.mjs +67 -0
  126. package/scripts/lib/codex-recovery.sh +50 -0
  127. package/scripts/lib/context.mjs +67 -0
  128. package/scripts/lib/cross-review-utils.mjs +59 -0
  129. package/scripts/lib/doctor-env-checks.mjs +121 -0
  130. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  131. package/scripts/lib/env-probe.mjs +269 -0
  132. package/scripts/lib/env.mjs +14 -0
  133. package/scripts/lib/gemini-profiles.mjs +119 -0
  134. package/scripts/lib/handoff.mjs +187 -0
  135. package/scripts/lib/hook-utils.mjs +16 -0
  136. package/scripts/lib/hub.mjs +48 -0
  137. package/scripts/lib/keyword-rules.mjs +195 -0
  138. package/scripts/lib/logger.mjs +105 -0
  139. package/scripts/lib/mcp-filter.mjs +883 -0
  140. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  141. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  142. package/scripts/lib/mcp-health.mjs +773 -0
  143. package/scripts/lib/mcp-manifest.mjs +89 -0
  144. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  145. package/scripts/lib/pid.mjs +63 -0
  146. package/scripts/lib/process-utils.mjs +26 -0
  147. package/scripts/lib/psmux-info.mjs +179 -0
  148. package/scripts/lib/quota.mjs +47 -0
  149. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  150. package/scripts/lib/skill-state.mjs +220 -0
  151. package/scripts/lib/skill-template.mjs +375 -0
  152. package/scripts/lib/team.mjs +75 -0
  153. package/scripts/lib/timeout.mjs +66 -0
  154. package/scripts/lib/tmp.mjs +26 -0
  155. package/scripts/lib/toml.mjs +113 -0
@@ -1,30 +1,40 @@
1
- export function buildTasks(subtasks, workers) {
2
- return subtasks.map((subtask, index) => ({
3
- id: `T${index + 1}`,
4
- title: subtask,
5
- owner: workers[index]?.name || null,
6
- status: "pending",
7
- depends_on: index === 0 ? [] : [`T${index}`],
8
- }));
9
- }
10
-
11
- export function normalizeTaskStatus(action) {
12
- const value = String(action || "").toLowerCase();
13
- if (value === "done" || value === "complete" || value === "completed") return "completed";
14
- if (value === "progress" || value === "in-progress" || value === "in_progress") return "in_progress";
15
- if (value === "pending") return "pending";
16
- return null;
17
- }
18
-
19
- export function updateTaskStatus(tasks = [], taskId, nextStatus) {
20
- const normalizedId = String(taskId || "").toUpperCase();
21
- const target = tasks.find((task) => String(task.id).toUpperCase() === normalizedId);
22
- if (!target) return { tasks, target: null };
23
-
24
- return {
25
- target: { ...target, status: nextStatus },
26
- tasks: tasks.map((task) => (
27
- String(task.id).toUpperCase() === normalizedId ? { ...task, status: nextStatus } : task
28
- )),
29
- };
30
- }
1
+ export function buildTasks(subtasks, workers) {
2
+ return subtasks.map((subtask, index) => ({
3
+ id: `T${index + 1}`,
4
+ title: subtask,
5
+ owner: workers[index]?.name || null,
6
+ status: "pending",
7
+ depends_on: index === 0 ? [] : [`T${index}`],
8
+ }));
9
+ }
10
+
11
+ export function normalizeTaskStatus(action) {
12
+ const value = String(action || "").toLowerCase();
13
+ if (value === "done" || value === "complete" || value === "completed")
14
+ return "completed";
15
+ if (
16
+ value === "progress" ||
17
+ value === "in-progress" ||
18
+ value === "in_progress"
19
+ )
20
+ return "in_progress";
21
+ if (value === "pending") return "pending";
22
+ return null;
23
+ }
24
+
25
+ export function updateTaskStatus(tasks = [], taskId, nextStatus) {
26
+ const normalizedId = String(taskId || "").toUpperCase();
27
+ const target = tasks.find(
28
+ (task) => String(task.id).toUpperCase() === normalizedId,
29
+ );
30
+ if (!target) return { tasks, target: null };
31
+
32
+ return {
33
+ target: { ...target, status: nextStatus },
34
+ tasks: tasks.map((task) =>
35
+ String(task.id).toUpperCase() === normalizedId
36
+ ? { ...task, status: nextStatus }
37
+ : task,
38
+ ),
39
+ };
40
+ }
@@ -0,0 +1,521 @@
1
+ // Codex headless review helper. Runs a cross-model review of a git diff
2
+ // without going through Bash-level headless-guard (the guard only inspects
3
+ // `codex exec` invoked from the Bash tool — node `spawn` bypasses it
4
+ // intentionally because `tfx` is the sanctioned entry point).
5
+ //
6
+ // Contract:
7
+ // runCodexReview({ ref = "HEAD", base?: string, timeoutMs = 180_000 })
8
+ // → { ok, code?, verdict, stdout, stderr, diffBytes, error? }
9
+ // runCodexReviewSharded({ ref, base, timeoutMs })
10
+ // → { ok, verdict, range, files, perFile: [{ file, verdict, ... }] }
11
+ // Verdict parsed from Codex output: APPROVED / REQUEST_CHANGES / COMMENT
12
+ // / UNKNOWN when none is stated.
13
+ //
14
+ // Motivation: session 12 post-mortem (BUG-J fix merged without cross-review).
15
+ // Without an ergonomic `tfx review` path, every swarm bugfix skips the
16
+ // Codex independent opinion — the exact check that caught BUG-I's path-only
17
+ // filter bypass in round 1 (session 11 PR #134). Session 13 adds --shard
18
+ // per-file for diffs that exceed the 32KB single-spawn ceiling.
19
+
20
+ import { execFileSync, spawn } from "node:child_process";
21
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+
25
+ // OS arg-list ceiling. The whole chain (bash → codex.cmd → node) fails
26
+ // with ENAMETOOLONG / "Argument list too long" well below the documented
27
+ // 128KB on Windows. Observed breakage at 62KB diff (session 12 self-dogfood).
28
+ // 32KB is a safe ceiling that covers most single-commit swarm fixes.
29
+ // Larger ranges should be reviewed per-file with --shard per-file or
30
+ // narrowed with --base.
31
+ export const MAX_PROMPT_BYTES = 32_000;
32
+
33
+ /**
34
+ * Expand a {ref, base} pair into a git range string.
35
+ * - `base` set → `base..ref`
36
+ * - `ref` already contains `..` → pass through
37
+ * - otherwise → `ref~1..ref` (single-commit review)
38
+ *
39
+ * Shared by single-mode `runCodexReview` and shard-mode
40
+ * `runCodexReviewSharded` so both interpret identical inputs identically.
41
+ *
42
+ * @param {{ ref?: string, base?: string }} opts
43
+ * @returns {string}
44
+ */
45
+ export function expandRange({ ref = "HEAD", base } = {}) {
46
+ if (base) return `${base}..${ref}`;
47
+ if (ref.includes("..")) return ref;
48
+ return `${ref}~1..${ref}`;
49
+ }
50
+
51
+ /**
52
+ * Resolve the diff payload for a given ref.
53
+ * - `a..b` ranges pass through unchanged
54
+ * - `<commit>` expands to `<commit>~1..<commit>` (single-commit review)
55
+ * - `HEAD` with no prior commit returns an empty string
56
+ * - optional `file` scopes the diff to a single path via `-- <file>`
57
+ *
58
+ * @param {{ ref?: string, base?: string, file?: string, runner?: (cmd: string, args: string[]) => string }} opts
59
+ * @returns {{ diff: string, range: string, file?: string }}
60
+ */
61
+ export function resolveReviewDiff({
62
+ ref = "HEAD",
63
+ base,
64
+ file,
65
+ runner = _defaultGitRunner,
66
+ } = {}) {
67
+ const range = expandRange({ ref, base });
68
+
69
+ const args = ["log", "-p", "--stat", "--no-color", range];
70
+ if (file) {
71
+ args.push("--", file);
72
+ }
73
+ const diff = runner("git", args);
74
+ return { diff, range, file };
75
+ }
76
+
77
+ /**
78
+ * Parse the raw `git log --name-only --pretty=format:` output into a
79
+ * de-duplicated, non-blank file path array. Pure function — extracted
80
+ * so shard enumeration can be tested against canned log output without
81
+ * requiring a temp git repo fixture.
82
+ *
83
+ * @param {string|null|undefined} rawLogOutput
84
+ * @returns {string[]}
85
+ */
86
+ export function parseChangedFilesFromLog(rawLogOutput) {
87
+ const seen = new Set();
88
+ for (const line of (rawLogOutput || "").split(/\r?\n/)) {
89
+ const trimmed = line.trim();
90
+ if (trimmed) seen.add(trimmed);
91
+ }
92
+ return [...seen];
93
+ }
94
+
95
+ /**
96
+ * List files changed in a range via `git log --name-only`, matching
97
+ * the commit-log semantics used by `resolveReviewDiff`. Using
98
+ * `git diff --name-only` would report only the net tree diff and
99
+ * silently drop files changed then reverted within a multi-commit
100
+ * range — those files would then be invisible to shard-mode review
101
+ * even though `git log -p` still contains their edits.
102
+ *
103
+ * Merge commits are included so enumeration stays aligned with
104
+ * `resolveReviewDiff`'s plain `git log -p`. Merges rarely introduce
105
+ * novel content but also rarely add noise since the dedupe Set
106
+ * collapses paths that already appear in parent commits.
107
+ *
108
+ * @param {string} range — e.g. "HEAD~1..HEAD" or "main..feature"
109
+ * @param {(cmd: string, args: string[]) => string} [runner] — injectable
110
+ * process runner. Default shells out to git; tests can pass a stub to
111
+ * verify argv contract (e.g. absence of `--no-merges`) without spawning.
112
+ * @returns {string[]}
113
+ */
114
+ export function listChangedFiles(range, runner = _defaultGitRunner) {
115
+ const out = runner("git", ["log", "--name-only", "--pretty=format:", range]);
116
+ return parseChangedFilesFromLog(out);
117
+ }
118
+
119
+ function _defaultGitRunner(cmd, args) {
120
+ return execFileSync(cmd, args, {
121
+ encoding: "utf8",
122
+ windowsHide: true,
123
+ maxBuffer: 50 * 1024 * 1024,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Build the Codex review prompt.
129
+ * @param {string} diff
130
+ * @param {{ range: string }} ctx
131
+ */
132
+ export function buildReviewPrompt(diff, { range }) {
133
+ return [
134
+ `You are acting as an independent cross-model reviewer.`,
135
+ `Review the following git log range: ${range}.`,
136
+ ``,
137
+ `Scope: correctness, regression risk, security, test coverage, code style.`,
138
+ ``,
139
+ `Classify each finding as:`,
140
+ `- HIGH: blocking — logic bug, security hole, regression`,
141
+ `- MEDIUM: should-fix — missing test, readability, maintainability`,
142
+ `- LOW: nit — style, micro-optimization`,
143
+ ``,
144
+ `Format each finding as: [SEVERITY] file:line — description. Suggested fix: ...`,
145
+ `If a category has no findings, write 'none'.`,
146
+ ``,
147
+ `End with exactly one line stating the verdict:`,
148
+ `VERDICT: APPROVED (no HIGH findings)`,
149
+ `VERDICT: REQUEST_CHANGES (any HIGH finding)`,
150
+ `VERDICT: COMMENT (observations only, no blockers)`,
151
+ ``,
152
+ "```diff",
153
+ diff,
154
+ "```",
155
+ ].join("\n");
156
+ }
157
+
158
+ /**
159
+ * Parse the verdict marker from Codex stdout.
160
+ * @param {string} stdout
161
+ */
162
+ export function parseVerdict(stdout) {
163
+ const match = stdout.match(
164
+ /VERDICT:\s*(APPROVED|REQUEST_CHANGES|COMMENT)\b/i,
165
+ );
166
+ if (match) return match[1].toUpperCase();
167
+ if (/REQUEST_CHANGES/i.test(stdout)) return "REQUEST_CHANGES";
168
+ if (/\bAPPROVED\b/i.test(stdout)) return "APPROVED";
169
+ return "UNKNOWN";
170
+ }
171
+
172
+ /**
173
+ * Aggregate per-file verdicts into an overall verdict.
174
+ * - Any REQUEST_CHANGES → REQUEST_CHANGES
175
+ * - Any COMMENT (no REQUEST_CHANGES) → COMMENT
176
+ * - All APPROVED (skipped excluded) → APPROVED
177
+ * - Mixed with UNKNOWN → UNKNOWN (conservative)
178
+ *
179
+ * @param {Array<{verdict: string|null, skipped?: boolean}>} fileResults
180
+ * @returns {string}
181
+ */
182
+ export function aggregateVerdicts(fileResults) {
183
+ const active = fileResults.filter((r) => !r.skipped);
184
+ if (active.length === 0) return "UNKNOWN";
185
+ const verdicts = active.map((r) => r.verdict);
186
+ if (verdicts.some((v) => v === "REQUEST_CHANGES")) return "REQUEST_CHANGES";
187
+ if (verdicts.some((v) => v === "COMMENT")) return "COMMENT";
188
+ if (verdicts.every((v) => v === "APPROVED")) return "APPROVED";
189
+ return "UNKNOWN";
190
+ }
191
+
192
+ /**
193
+ * Internal: run Codex on a pre-built prompt and parse the response.
194
+ * Shared by single-mode runCodexReview and shard-mode runCodexReviewSharded.
195
+ */
196
+ async function _runCodexOnPrompt({
197
+ prompt,
198
+ range,
199
+ diffBytes,
200
+ timeoutMs,
201
+ sandbox,
202
+ env,
203
+ }) {
204
+ // Prompt is a full git diff — often >10KB, regularly >60KB on swarm
205
+ // bugfixes. Windows cmd.exe caps command lines at ~8KB and node's
206
+ // `spawn(..., [promptArg])` route cannot exceed the OS arg limit
207
+ // (ENAMETOOLONG on Windows). Persist the prompt to a tmp file and let
208
+ // bash read it back — same pattern session 11 used manually
209
+ // (`codex exec "$(cat prompt.md)" ...`).
210
+ const tmpDir = mkdtempSync(join(tmpdir(), "tfx-review-"));
211
+ const promptFile = join(tmpDir, "prompt.md").replace(/\\/g, "/");
212
+ writeFileSync(promptFile, prompt);
213
+
214
+ const shellCmd = [
215
+ "codex",
216
+ "exec",
217
+ "-s",
218
+ sandbox,
219
+ "--dangerously-bypass-approvals-and-sandbox",
220
+ `"$(cat '${promptFile.replace(/'/g, "'\\''")}')"`,
221
+ ].join(" ");
222
+
223
+ return new Promise((resolve) => {
224
+ const child = spawn("bash", ["-c", shellCmd], {
225
+ stdio: ["ignore", "pipe", "pipe"],
226
+ windowsHide: true,
227
+ env,
228
+ });
229
+
230
+ let stdout = "";
231
+ let stderr = "";
232
+ let timedOut = false;
233
+
234
+ const timer = setTimeout(() => {
235
+ timedOut = true;
236
+ child.kill("SIGTERM");
237
+ }, timeoutMs);
238
+
239
+ child.stdout.on("data", (d) => {
240
+ stdout += d.toString();
241
+ });
242
+ child.stderr.on("data", (d) => {
243
+ stderr += d.toString();
244
+ });
245
+ const cleanupTmp = () => {
246
+ try {
247
+ rmSync(tmpDir, { recursive: true, force: true });
248
+ } catch {
249
+ /* best-effort */
250
+ }
251
+ };
252
+
253
+ child.on("error", (err) => {
254
+ clearTimeout(timer);
255
+ cleanupTmp();
256
+ resolve({
257
+ ok: false,
258
+ error: err.message,
259
+ verdict: null,
260
+ stdout,
261
+ stderr,
262
+ diffBytes,
263
+ range,
264
+ });
265
+ });
266
+ child.on("close", (code) => {
267
+ clearTimeout(timer);
268
+ cleanupTmp();
269
+ if (timedOut) {
270
+ resolve({
271
+ ok: false,
272
+ code,
273
+ error: `codex review timed out after ${timeoutMs}ms`,
274
+ verdict: null,
275
+ stdout,
276
+ stderr,
277
+ diffBytes,
278
+ range,
279
+ });
280
+ return;
281
+ }
282
+ resolve({
283
+ ok: code === 0,
284
+ code,
285
+ verdict: parseVerdict(stdout),
286
+ stdout,
287
+ stderr,
288
+ diffBytes,
289
+ range,
290
+ });
291
+ });
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Run Codex review of a git range (single-spawn mode).
297
+ * @param {object} opts
298
+ * @param {string} [opts.ref="HEAD"]
299
+ * @param {string} [opts.base]
300
+ * @param {number} [opts.timeoutMs=180000]
301
+ * @param {string} [opts.sandbox="read-only"]
302
+ * @param {object} [opts.env]
303
+ */
304
+ export async function runCodexReview({
305
+ ref = "HEAD",
306
+ base,
307
+ timeoutMs = 180_000,
308
+ sandbox = "read-only",
309
+ env = process.env,
310
+ _deps = {},
311
+ } = {}) {
312
+ let range;
313
+ let diff;
314
+ try {
315
+ ({ diff, range } = (_deps.resolveReviewDiff || resolveReviewDiff)({
316
+ ref,
317
+ base,
318
+ }));
319
+ } catch (err) {
320
+ return {
321
+ ok: false,
322
+ error: `git log failed: ${err.message}`,
323
+ verdict: null,
324
+ stdout: "",
325
+ stderr: "",
326
+ diffBytes: 0,
327
+ };
328
+ }
329
+
330
+ const diffBytes = Buffer.byteLength(diff, "utf8");
331
+ if (diffBytes === 0) {
332
+ return {
333
+ ok: false,
334
+ error: `empty diff for range ${range}`,
335
+ verdict: null,
336
+ stdout: "",
337
+ stderr: "",
338
+ diffBytes: 0,
339
+ range,
340
+ };
341
+ }
342
+
343
+ const prompt = buildReviewPrompt(diff, { range });
344
+ const promptBytes = Buffer.byteLength(prompt, "utf8");
345
+
346
+ if (promptBytes > MAX_PROMPT_BYTES) {
347
+ return {
348
+ ok: false,
349
+ error:
350
+ `prompt too large (${promptBytes} bytes > ${MAX_PROMPT_BYTES}). ` +
351
+ `Retry with --shard per-file or narrow with --base <sha>.`,
352
+ verdict: null,
353
+ stdout: "",
354
+ stderr: "",
355
+ diffBytes,
356
+ promptBytes,
357
+ range,
358
+ };
359
+ }
360
+
361
+ return (_deps.runCodexOnPrompt || _runCodexOnPrompt)({
362
+ prompt,
363
+ range,
364
+ diffBytes,
365
+ timeoutMs,
366
+ sandbox,
367
+ env,
368
+ });
369
+ }
370
+
371
+ /**
372
+ * Run Codex review sharded per-file. Each changed file in the range gets
373
+ * its own Codex spawn with its own 32KB budget. Results are aggregated.
374
+ *
375
+ * Sequential, not parallel — Codex headless startup is ~30-90s per call
376
+ * and parallel spawns risk auth contention. Wall-clock scales linearly
377
+ * with file count, budget accordingly (default timeout applies per-file).
378
+ *
379
+ * @param {object} opts — same shape as runCodexReview
380
+ * @returns {Promise<{ok: boolean, verdict: string, range: string, files: string[], perFile: object[], error?: string}>}
381
+ */
382
+ export async function runCodexReviewSharded({
383
+ ref = "HEAD",
384
+ base,
385
+ timeoutMs = 180_000,
386
+ sandbox = "read-only",
387
+ env = process.env,
388
+ onFileStart,
389
+ onFileDone,
390
+ } = {}) {
391
+ const range = expandRange({ ref, base });
392
+
393
+ let files;
394
+ try {
395
+ files = listChangedFiles(range);
396
+ } catch (err) {
397
+ return {
398
+ ok: false,
399
+ error: `git log --name-only failed: ${err.message}`,
400
+ verdict: "UNKNOWN",
401
+ range,
402
+ files: [],
403
+ perFile: [],
404
+ };
405
+ }
406
+
407
+ if (files.length === 0) {
408
+ return {
409
+ ok: false,
410
+ error: `no changed files in range ${range}`,
411
+ verdict: "UNKNOWN",
412
+ range,
413
+ files: [],
414
+ perFile: [],
415
+ };
416
+ }
417
+
418
+ const perFile = [];
419
+ for (const file of files) {
420
+ if (typeof onFileStart === "function") {
421
+ try {
422
+ onFileStart({ file, index: perFile.length, total: files.length });
423
+ } catch {
424
+ /* caller callback errors should not block review */
425
+ }
426
+ }
427
+
428
+ let fileDiff;
429
+ try {
430
+ ({ diff: fileDiff } = resolveReviewDiff({ ref, base, file }));
431
+ } catch (err) {
432
+ const entry = {
433
+ file,
434
+ ok: false,
435
+ error: `git log scope failed: ${err.message}`,
436
+ verdict: "UNKNOWN",
437
+ diffBytes: 0,
438
+ range: `${range} -- ${file}`,
439
+ };
440
+ perFile.push(entry);
441
+ if (typeof onFileDone === "function") {
442
+ try {
443
+ onFileDone(entry);
444
+ } catch {
445
+ /* ignore */
446
+ }
447
+ }
448
+ continue;
449
+ }
450
+
451
+ const diffBytes = Buffer.byteLength(fileDiff, "utf8");
452
+ if (diffBytes === 0) {
453
+ const entry = {
454
+ file,
455
+ ok: true,
456
+ skipped: true,
457
+ error: `empty diff for ${file} (likely rename/mode-only)`,
458
+ verdict: "APPROVED",
459
+ diffBytes: 0,
460
+ range: `${range} -- ${file}`,
461
+ };
462
+ perFile.push(entry);
463
+ if (typeof onFileDone === "function") {
464
+ try {
465
+ onFileDone(entry);
466
+ } catch {
467
+ /* ignore */
468
+ }
469
+ }
470
+ continue;
471
+ }
472
+
473
+ const scopedRange = `${range} -- ${file}`;
474
+ const prompt = buildReviewPrompt(fileDiff, { range: scopedRange });
475
+ const promptBytes = Buffer.byteLength(prompt, "utf8");
476
+ if (promptBytes > MAX_PROMPT_BYTES) {
477
+ const entry = {
478
+ file,
479
+ ok: false,
480
+ error:
481
+ `prompt too large (${promptBytes} bytes > ${MAX_PROMPT_BYTES}) ` +
482
+ `for single file. Review this file manually or split the commit.`,
483
+ verdict: "UNKNOWN",
484
+ diffBytes,
485
+ promptBytes,
486
+ range: scopedRange,
487
+ };
488
+ perFile.push(entry);
489
+ if (typeof onFileDone === "function") {
490
+ try {
491
+ onFileDone(entry);
492
+ } catch {
493
+ /* ignore */
494
+ }
495
+ }
496
+ continue;
497
+ }
498
+
499
+ const res = await _runCodexOnPrompt({
500
+ prompt,
501
+ range: scopedRange,
502
+ diffBytes,
503
+ timeoutMs,
504
+ sandbox,
505
+ env,
506
+ });
507
+ const entry = { file, ...res };
508
+ perFile.push(entry);
509
+ if (typeof onFileDone === "function") {
510
+ try {
511
+ onFileDone(entry);
512
+ } catch {
513
+ /* ignore */
514
+ }
515
+ }
516
+ }
517
+
518
+ const verdict = aggregateVerdicts(perFile);
519
+ const ok = perFile.every((r) => r.ok);
520
+ return { ok, verdict, range, files, perFile };
521
+ }
@@ -1,4 +1,4 @@
1
- import { createMessage, MSG_TYPES } from "../../mesh/mesh-protocol.mjs";
1
+ import { createMessage, MSG_TYPES } from "@triflux/core/mesh/mesh-protocol.mjs";
2
2
 
3
3
  const BRIDGE_AGENT_ID = "conductor";
4
4
 
@@ -14,10 +14,7 @@ const BRIDGE_AGENT_ID = "conductor";
14
14
  * @returns {object} Bridge API
15
15
  */
16
16
  export function createConductorMeshBridge(conductor, registry, opts = {}) {
17
- const {
18
- bridgeAgentId = BRIDGE_AGENT_ID,
19
- onMessage,
20
- } = opts;
17
+ const { bridgeAgentId = BRIDGE_AGENT_ID, onMessage } = opts;
21
18
 
22
19
  const sessionAgentMap = new Map();
23
20
  let attached = false;
@@ -49,12 +46,13 @@ export function createConductorMeshBridge(conductor, registry, opts = {}) {
49
46
  }
50
47
 
51
48
  // Emit mesh event
52
- const msg = createMessage(
53
- MSG_TYPES.EVENT,
54
- bridgeAgentId,
55
- "*",
56
- { event: "stateChange", sessionId, from, to, reason },
57
- );
49
+ const msg = createMessage(MSG_TYPES.EVENT, bridgeAgentId, "*", {
50
+ event: "stateChange",
51
+ sessionId,
52
+ from,
53
+ to,
54
+ reason,
55
+ });
58
56
  emit(msg);
59
57
 
60
58
  // Unregister on terminal states
@@ -68,12 +66,10 @@ export function createConductorMeshBridge(conductor, registry, opts = {}) {
68
66
  * Handle conductor completed events.
69
67
  */
70
68
  function handleCompleted(event) {
71
- const msg = createMessage(
72
- MSG_TYPES.EVENT,
73
- bridgeAgentId,
74
- "*",
75
- { event: "completed", sessionId: event.sessionId },
76
- );
69
+ const msg = createMessage(MSG_TYPES.EVENT, bridgeAgentId, "*", {
70
+ event: "completed",
71
+ sessionId: event.sessionId,
72
+ });
77
73
  emit(msg);
78
74
  }
79
75
 
@@ -81,12 +77,11 @@ export function createConductorMeshBridge(conductor, registry, opts = {}) {
81
77
  * Handle conductor dead events.
82
78
  */
83
79
  function handleDead(event) {
84
- const msg = createMessage(
85
- MSG_TYPES.EVENT,
86
- bridgeAgentId,
87
- "*",
88
- { event: "dead", sessionId: event.sessionId, reason: event.reason },
89
- );
80
+ const msg = createMessage(MSG_TYPES.EVENT, bridgeAgentId, "*", {
81
+ event: "dead",
82
+ sessionId: event.sessionId,
83
+ reason: event.reason,
84
+ });
90
85
  emit(msg);
91
86
  }
92
87
 
@@ -117,5 +112,11 @@ export function createConductorMeshBridge(conductor, registry, opts = {}) {
117
112
  attached = false;
118
113
  }
119
114
 
120
- return { attach, detach, get isAttached() { return attached; } };
115
+ return {
116
+ attach,
117
+ detach,
118
+ get isAttached() {
119
+ return attached;
120
+ },
121
+ };
121
122
  }