@triflux/remote 10.1.0 → 10.26.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/hub/pipe.mjs +239 -121
- package/hub/public/dashboard.html +61 -0
- package/hub/server.mjs +2179 -709
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +26 -7
- package/hub/team/ansi.mjs +245 -146
- package/hub/team/backend.mjs +78 -17
- package/hub/team/build-worker-prompt.mjs +73 -0
- package/hub/team/check-mcp-hub.mjs +49 -0
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/claude-native-bridge.mjs +1011 -0
- package/hub/team/claude-session-projection.mjs +74 -0
- package/hub/team/cli/commands/attach.mjs +57 -37
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +95 -74
- package/hub/team/cli/commands/focus.mjs +80 -53
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +31 -24
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +151 -33
- package/hub/team/cli/commands/start/parse-args.mjs +97 -13
- package/hub/team/cli/commands/start/start-headless.mjs +155 -37
- package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
- package/hub/team/cli/commands/start/start-mux.mjs +122 -73
- package/hub/team/cli/commands/start/start-wt.mjs +86 -69
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +13 -13
- package/hub/team/cli/help.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +52 -30
- package/hub/team/cli/services/attach-fallback.mjs +74 -54
- package/hub/team/cli/services/hub-client.mjs +44 -16
- package/hub/team/cli/services/member-selector.mjs +37 -30
- package/hub/team/cli/services/native-control.mjs +144 -116
- package/hub/team/cli/services/runtime-mode.mjs +61 -4
- package/hub/team/cli/services/state-store.mjs +66 -9
- package/hub/team/cli/services/task-model.mjs +40 -30
- package/hub/team/codex-review.mjs +521 -0
- package/hub/team/conductor-mesh-bridge.mjs +25 -24
- package/hub/team/conductor-registry.mjs +121 -0
- package/hub/team/conductor.mjs +1379 -671
- package/hub/team/consensus-meta.mjs +176 -0
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +20 -118
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/execution-mode.mjs +256 -0
- package/hub/team/extract-completion-payload.mjs +110 -0
- package/hub/team/git-preflight.mjs +286 -0
- package/hub/team/handoff.mjs +49 -18
- package/hub/team/headless-bridge-session.mjs +17 -0
- package/hub/team/headless.mjs +2213 -1156
- package/hub/team/health-probe.mjs +232 -69
- package/hub/team/interactive-native-launcher.mjs +106 -0
- package/hub/team/interactive-native-worker.mjs +80 -0
- package/hub/team/interactive-tui-transport.mjs +465 -0
- package/hub/team/launcher-template.mjs +49 -17
- package/hub/team/lead-control.mjs +91 -4
- package/hub/team/mcp-selector.mjs +145 -0
- package/hub/team/native-supervisor.mjs +90 -26
- package/hub/team/native.mjs +120 -42
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +147 -78
- package/hub/team/orchestrator.mjs +173 -161
- package/hub/team/pane.mjs +43 -20
- package/hub/team/process-cleanup.mjs +69 -25
- package/hub/team/psmux.mjs +1022 -281
- package/hub/team/recovery-store.mjs +149 -0
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +131 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +200 -0
- package/hub/team/schemas/worker-completion.json +39 -0
- package/hub/team/sentinel-capture.mjs +127 -0
- package/hub/team/session-sync.mjs +153 -6
- package/hub/team/session.mjs +574 -611
- package/hub/team/shared.mjs +13 -13
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-cli.mjs +390 -0
- package/hub/team/swarm-hypervisor.mjs +1544 -193
- package/hub/team/swarm-intent.mjs +216 -0
- package/hub/team/swarm-locks.mjs +140 -25
- package/hub/team/swarm-planner.mjs +121 -21
- package/hub/team/swarm-preflight.mjs +319 -0
- package/hub/team/swarm-reconciler.mjs +128 -23
- package/hub/team/synapse-cli.mjs +458 -0
- package/hub/team/synapse-http.mjs +60 -0
- package/hub/team/synapse-registry.mjs +329 -0
- package/hub/team/terminal-opener.mjs +216 -0
- package/hub/team/tui-core.mjs +292 -0
- package/hub/team/tui-lite.mjs +237 -148
- package/hub/team/tui-remote-adapter.mjs +18 -34
- package/hub/team/tui-synapse.mjs +223 -0
- package/hub/team/tui-viewer.mjs +108 -48
- package/hub/team/tui-widgets.mjs +262 -0
- package/hub/team/tui.mjs +931 -439
- package/hub/team/worker-completion-validator.mjs +84 -0
- package/hub/team/worker-sandbox.mjs +97 -0
- package/hub/team/worker-signal.mjs +263 -0
- package/hub/team/worker-signal.types.d.ts +52 -0
- package/hub/team/worktree-lifecycle.mjs +517 -63
- package/hub/team/wt-manager.mjs +297 -26
- package/hub/tools.mjs +477 -252
- package/hub/tray.mjs +411 -368
- package/hub/workers/antigravity-route-worker.mjs +169 -0
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-app-server-worker.mjs +1015 -0
- package/hub/workers/codex-mcp.mjs +724 -507
- package/hub/workers/delegator-mcp.mjs +570 -499
- package/hub/workers/factory.mjs +103 -21
- package/hub/workers/gemini-worker.mjs +126 -284
- package/hub/workers/interface.mjs +24 -2
- package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
- package/hub/workers/worker-utils.mjs +23 -17
- package/package.json +4 -3
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +223 -0
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +37 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +59 -0
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +269 -0
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +119 -0
- package/scripts/lib/handoff.mjs +187 -0
- package/scripts/lib/hook-utils.mjs +16 -0
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +195 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +883 -0
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +2120 -0
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +89 -0
- package/scripts/lib/mcp-server-catalog.mjs +355 -0
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +224 -0
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +226 -0
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +375 -0
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- 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")
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 "
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 {
|
|
115
|
+
return {
|
|
116
|
+
attach,
|
|
117
|
+
detach,
|
|
118
|
+
get isAttached() {
|
|
119
|
+
return attached;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
121
122
|
}
|