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