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