triflux 10.9.16 → 10.9.18
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/hub/lib/spawn-trace.mjs +31 -0
- package/hub/server.mjs +5 -0
- package/hub/team/backend.mjs +1 -1
- package/hub/team/handoff.mjs +17 -3
- package/hub/team/headless.mjs +90 -29
- package/hub/team/psmux.mjs +1 -0
- package/hub/team/swarm-hypervisor.mjs +238 -97
- package/hub/team/swarm-planner.mjs +2 -1
- package/package.json +8 -2
- package/scripts/__tests__/release-governance.test.mjs +148 -0
- package/scripts/__tests__/spawn-trace.test.mjs +126 -9
- package/scripts/headless-guard.mjs +1 -1
- package/scripts/release/bump-version.mjs +77 -0
- package/scripts/release/check-sync.mjs +51 -0
- package/scripts/release/lib.mjs +303 -0
- package/scripts/release/prepare.mjs +85 -0
- package/scripts/release/publish.mjs +87 -0
- package/scripts/release/verify.mjs +81 -0
- package/scripts/release/version-manifest.json +26 -0
- package/scripts/tfx-route.sh +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "triflux",
|
|
11
11
|
"description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
|
|
12
|
-
"version": "10.9.
|
|
12
|
+
"version": "10.9.18",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "tellang"
|
|
15
15
|
},
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
]
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
|
-
"version": "10.9.
|
|
33
|
+
"version": "10.9.18"
|
|
34
34
|
}
|
package/hub/lib/spawn-trace.mjs
CHANGED
|
@@ -331,6 +331,10 @@ function normalizeExecFileArgs(args, options, callback) {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
function wait(ms) {
|
|
335
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
336
|
+
}
|
|
337
|
+
|
|
334
338
|
export function spawn(command, args, options) {
|
|
335
339
|
const { argsList, options: normalizedOptions } = normalizeSpawnArgs(
|
|
336
340
|
args,
|
|
@@ -369,6 +373,32 @@ export function spawn(command, args, options) {
|
|
|
369
373
|
});
|
|
370
374
|
}
|
|
371
375
|
|
|
376
|
+
export async function spawnWithBackoff(command, args, options, maxRetries = 1) {
|
|
377
|
+
const retryLimit =
|
|
378
|
+
Number.isInteger(maxRetries) && maxRetries >= 0 ? maxRetries : 1;
|
|
379
|
+
let originalRateLimitError = null;
|
|
380
|
+
|
|
381
|
+
for (let attempt = 0; attempt <= retryLimit; attempt += 1) {
|
|
382
|
+
try {
|
|
383
|
+
return spawn(command, args, options);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error?.reasonCode !== "rate_limit") {
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
originalRateLimitError ??= error;
|
|
390
|
+
|
|
391
|
+
if (attempt >= retryLimit) {
|
|
392
|
+
throw originalRateLimitError;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await wait(RATE_WINDOW_MS);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
throw originalRateLimitError;
|
|
400
|
+
}
|
|
401
|
+
|
|
372
402
|
export function execFile(file, args, options, callback) {
|
|
373
403
|
const normalized = normalizeExecFileArgs(args, options, callback);
|
|
374
404
|
const traceId = nextTraceId();
|
|
@@ -515,6 +545,7 @@ export const spawnSync = childProcess.spawnSync;
|
|
|
515
545
|
export default {
|
|
516
546
|
...childProcess,
|
|
517
547
|
spawn,
|
|
548
|
+
spawnWithBackoff,
|
|
518
549
|
execFile,
|
|
519
550
|
execFileSync,
|
|
520
551
|
get MAX_SPAWN_PER_SEC() {
|
package/hub/server.mjs
CHANGED
|
@@ -719,6 +719,11 @@ export async function startHub({
|
|
|
719
719
|
pipe: pipe.getStatus(),
|
|
720
720
|
assign_callback_pipe_path: assignCallbacks.path,
|
|
721
721
|
assign_callback_pipe: assignCallbacks.getStatus(),
|
|
722
|
+
spawn_trace: {
|
|
723
|
+
max_per_sec: spawnTrace.getMaxSpawnPerSec(),
|
|
724
|
+
max_total_descendants: spawnTrace.MAX_TOTAL_DESCENDANTS,
|
|
725
|
+
},
|
|
726
|
+
version,
|
|
722
727
|
});
|
|
723
728
|
}
|
|
724
729
|
|
package/hub/team/backend.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export class GeminiBackend {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
buildArgs(prompt, resultFile, opts = {}) {
|
|
44
|
-
return
|
|
44
|
+
return `$null | gemini --prompt ${prompt} --output-format text > '${resultFile}' 2>'${resultFile}.err'`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
env() {
|
package/hub/team/handoff.mjs
CHANGED
|
@@ -219,6 +219,14 @@ export function validateHandoff(parsed, context = {}) {
|
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
const filesChangedCount = Array.isArray(h.files_changed)
|
|
223
|
+
? h.files_changed.filter(Boolean).length
|
|
224
|
+
: 0;
|
|
225
|
+
if (filesChangedCount === 0 && h.lead_action === "accept") {
|
|
226
|
+
h.lead_action = "needs_read";
|
|
227
|
+
warnings.push("files_changed empty: accept → needs_read");
|
|
228
|
+
}
|
|
229
|
+
|
|
222
230
|
const missingCore = coreRequired.filter((f) => !h[f]);
|
|
223
231
|
const missingRouting = routingRequired.filter((f) => !h[f]);
|
|
224
232
|
const valid = missingCore.length === 0 && missingRouting.length === 0;
|
|
@@ -231,15 +239,20 @@ export function validateHandoff(parsed, context = {}) {
|
|
|
231
239
|
* @param {number} exitCode
|
|
232
240
|
* @param {string} resultFile
|
|
233
241
|
* @param {string} [cli]
|
|
242
|
+
* @param {object} [context]
|
|
243
|
+
* @param {string[]} [context.filesChanged]
|
|
234
244
|
* @returns {object}
|
|
235
245
|
*/
|
|
236
|
-
export function buildFallbackHandoff(exitCode, resultFile, cli) {
|
|
246
|
+
export function buildFallbackHandoff(exitCode, resultFile, cli, context = {}) {
|
|
237
247
|
const ok = exitCode === 0;
|
|
248
|
+
const filesChanged = Array.isArray(context.filesChanged)
|
|
249
|
+
? context.filesChanged.filter(Boolean)
|
|
250
|
+
: [];
|
|
238
251
|
return {
|
|
239
252
|
status: ok ? "ok" : "failed",
|
|
240
|
-
lead_action: ok ? "accept" : "retry",
|
|
253
|
+
lead_action: ok && filesChanged.length > 0 ? "accept" : ok ? "needs_read" : "retry",
|
|
241
254
|
task: "unknown",
|
|
242
|
-
files_changed:
|
|
255
|
+
files_changed: filesChanged,
|
|
243
256
|
verdict: `${cli || "worker"} completed (exit ${exitCode})`,
|
|
244
257
|
confidence: "low",
|
|
245
258
|
risk: "low",
|
|
@@ -298,6 +311,7 @@ export function processHandoff(rawText, context = {}) {
|
|
|
298
311
|
context.exitCode ?? 1,
|
|
299
312
|
context.resultFile || "none",
|
|
300
313
|
context.cli,
|
|
314
|
+
{ filesChanged: context.gitDiffFiles },
|
|
301
315
|
);
|
|
302
316
|
return {
|
|
303
317
|
handoff: fb,
|
package/hub/team/headless.mjs
CHANGED
|
@@ -18,10 +18,15 @@ import { createRequire } from "node:module";
|
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { requestJson } from "../bridge.mjs";
|
|
21
|
+
import { getMaxSpawnPerSec } from "../lib/spawn-trace.mjs";
|
|
21
22
|
import { escapePwshSingleQuoted } from "../cli-adapter-base.mjs";
|
|
22
23
|
import { getBackend } from "./backend.mjs";
|
|
23
24
|
import { resolveDashboardLayout } from "./dashboard-layout.mjs";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
formatHandoffForLead,
|
|
27
|
+
HANDOFF_INSTRUCTION_SHORT,
|
|
28
|
+
processHandoff,
|
|
29
|
+
} from "./handoff.mjs";
|
|
25
30
|
import {
|
|
26
31
|
capturePsmuxPane,
|
|
27
32
|
createPsmuxSession,
|
|
@@ -316,7 +321,7 @@ export function createStallMonitor(paneId, resultFile, config, deps = {}) {
|
|
|
316
321
|
* @param {string} [opts.command] — re-dispatch용 원본 명령
|
|
317
322
|
* @param {string} [opts.token] — completion token
|
|
318
323
|
* @param {(snapshot: string) => void} [opts.onPoll] — 폴링 콜백
|
|
319
|
-
* @returns {Promise<{ matched: boolean, exitCode: number|null, restarts: number, stallDetected: boolean }>}
|
|
324
|
+
* @returns {Promise<{ matched: boolean, exitCode: number|null, restarts: number, stallDetected: boolean, paneId: string, token?: string, logPath?: string|null }>}
|
|
320
325
|
*/
|
|
321
326
|
export async function waitForCompletionWithStallDetect(
|
|
322
327
|
sessionName,
|
|
@@ -346,19 +351,23 @@ export async function waitForCompletionWithStallDetect(
|
|
|
346
351
|
const _startCapture = deps.startCapture || startCapture;
|
|
347
352
|
|
|
348
353
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
const buildCompletionRegex = (activeToken) => {
|
|
355
|
+
const completionPatterns = [
|
|
356
|
+
activeToken
|
|
357
|
+
? `${esc("__TRIFLUX_DONE__:")}${esc(activeToken)}:(\\d+)`
|
|
358
|
+
: `${esc("__TRIFLUX_DONE__:")}\\S+:(\\d+)`,
|
|
359
|
+
activeToken
|
|
360
|
+
? `${esc("TFX_DONE_")}${esc(activeToken)}:(\\d+)`
|
|
361
|
+
: `${esc("TFX_DONE_")}\\S+:(\\d+)`,
|
|
362
|
+
];
|
|
363
|
+
return new RegExp(completionPatterns.join("|"), "m");
|
|
364
|
+
};
|
|
358
365
|
|
|
359
366
|
let restarts = 0;
|
|
360
367
|
let currentPaneId = paneId;
|
|
361
368
|
let stallDetected = false;
|
|
369
|
+
let currentToken = token;
|
|
370
|
+
let currentLogPath = opts.logPath || null;
|
|
362
371
|
|
|
363
372
|
while (true) {
|
|
364
373
|
let lastOutput = "";
|
|
@@ -385,6 +394,9 @@ export async function waitForCompletionWithStallDetect(
|
|
|
385
394
|
restarts,
|
|
386
395
|
stallDetected,
|
|
387
396
|
timedOut: true,
|
|
397
|
+
paneId: currentPaneId,
|
|
398
|
+
token: currentToken,
|
|
399
|
+
logPath: currentLogPath,
|
|
388
400
|
};
|
|
389
401
|
}
|
|
390
402
|
|
|
@@ -399,6 +411,7 @@ export async function waitForCompletionWithStallDetect(
|
|
|
399
411
|
}
|
|
400
412
|
|
|
401
413
|
// 2) completion 토큰 감지
|
|
414
|
+
const completionRe = buildCompletionRegex(currentToken);
|
|
402
415
|
const completionMatch = completionRe.exec(currentOutput);
|
|
403
416
|
if (completionMatch) {
|
|
404
417
|
return {
|
|
@@ -410,6 +423,9 @@ export async function waitForCompletionWithStallDetect(
|
|
|
410
423
|
restarts,
|
|
411
424
|
stallDetected,
|
|
412
425
|
timedOut: false,
|
|
426
|
+
paneId: currentPaneId,
|
|
427
|
+
token: currentToken,
|
|
428
|
+
logPath: currentLogPath,
|
|
413
429
|
};
|
|
414
430
|
}
|
|
415
431
|
|
|
@@ -442,6 +458,9 @@ export async function waitForCompletionWithStallDetect(
|
|
|
442
458
|
restarts,
|
|
443
459
|
stallDetected,
|
|
444
460
|
timedOut: false,
|
|
461
|
+
paneId: currentPaneId,
|
|
462
|
+
token: currentToken,
|
|
463
|
+
logPath: currentLogPath,
|
|
445
464
|
};
|
|
446
465
|
}
|
|
447
466
|
} catch {
|
|
@@ -480,8 +499,10 @@ export async function waitForCompletionWithStallDetect(
|
|
|
480
499
|
"#{session_name}:#{window_index}.#{pane_index}",
|
|
481
500
|
]);
|
|
482
501
|
_startCapture(sessionName, newPaneId);
|
|
483
|
-
_dispatch(sessionName, newPaneId, command);
|
|
484
|
-
currentPaneId = newPaneId;
|
|
502
|
+
const redispatch = _dispatch(sessionName, newPaneId, command);
|
|
503
|
+
currentPaneId = redispatch?.paneId || newPaneId;
|
|
504
|
+
if (redispatch?.token) currentToken = redispatch.token;
|
|
505
|
+
if (redispatch?.logPath) currentLogPath = redispatch.logPath;
|
|
485
506
|
}
|
|
486
507
|
|
|
487
508
|
restarts++;
|
|
@@ -564,7 +585,11 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
564
585
|
assignment.cli,
|
|
565
586
|
assignment.prompt,
|
|
566
587
|
resultFile,
|
|
567
|
-
{
|
|
588
|
+
{
|
|
589
|
+
mcp: assignment.mcp,
|
|
590
|
+
model: assignment.model,
|
|
591
|
+
cwd: assignment.cwd || assignment.workdir,
|
|
592
|
+
},
|
|
568
593
|
);
|
|
569
594
|
startCapture(sessionName, newPaneId);
|
|
570
595
|
// pane 간 pipe-pane EBUSY 방지 — 이벤트 루프 해방하며 순차 대기
|
|
@@ -584,6 +609,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
|
|
|
584
609
|
role: assignment.role,
|
|
585
610
|
command: cmd,
|
|
586
611
|
workerId,
|
|
612
|
+
cwd: assignment.cwd || assignment.workdir,
|
|
587
613
|
});
|
|
588
614
|
}
|
|
589
615
|
|
|
@@ -635,7 +661,11 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
635
661
|
assignment.cli,
|
|
636
662
|
assignment.prompt,
|
|
637
663
|
resultFile,
|
|
638
|
-
{
|
|
664
|
+
{
|
|
665
|
+
mcp: assignment.mcp,
|
|
666
|
+
model: assignment.model,
|
|
667
|
+
cwd: assignment.cwd || assignment.workdir,
|
|
668
|
+
},
|
|
639
669
|
);
|
|
640
670
|
const scriptDir = join(RESULT_DIR, sessionName);
|
|
641
671
|
await registerHeadlessWorker(sessionName, i, assignment.cli);
|
|
@@ -660,6 +690,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
|
|
|
660
690
|
role: assignment.role,
|
|
661
691
|
command: cmd,
|
|
662
692
|
workerId,
|
|
693
|
+
cwd: assignment.cwd || assignment.workdir,
|
|
663
694
|
};
|
|
664
695
|
}),
|
|
665
696
|
);
|
|
@@ -739,6 +770,9 @@ async function awaitAll(
|
|
|
739
770
|
onPoll: stallPollCb,
|
|
740
771
|
},
|
|
741
772
|
);
|
|
773
|
+
if (stallResult.paneId) d.paneId = stallResult.paneId;
|
|
774
|
+
if (stallResult.token) d.token = stallResult.token;
|
|
775
|
+
if (stallResult.logPath) d.logPath = stallResult.logPath;
|
|
742
776
|
completion = {
|
|
743
777
|
matched: stallResult.matched,
|
|
744
778
|
exitCode: stallResult.exitCode,
|
|
@@ -798,28 +832,27 @@ async function awaitAll(
|
|
|
798
832
|
* @returns {Array}
|
|
799
833
|
*/
|
|
800
834
|
async function collectResults(sessionName, results) {
|
|
801
|
-
// B3 fix: git diff를 루프 밖에서 1회만 실행 (워커 수만큼 중복 방지)
|
|
802
|
-
let gitDiffFiles;
|
|
803
|
-
try {
|
|
804
|
-
const diffOut = execSync("git diff --name-only HEAD", {
|
|
805
|
-
encoding: "utf8",
|
|
806
|
-
timeout: 5000,
|
|
807
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
808
|
-
});
|
|
809
|
-
gitDiffFiles = diffOut.trim().split("\n").filter(Boolean);
|
|
810
|
-
} catch {
|
|
811
|
-
/* git 미설치 또는 non-repo — 무시 */
|
|
812
|
-
}
|
|
813
|
-
|
|
814
835
|
// handoff 파이프라인: parse → validate → format (각 워커 결과에 적용)
|
|
815
836
|
return await Promise.all(
|
|
816
837
|
results.map(async ({ d, completion, output }) => {
|
|
838
|
+
const workerGitDiffFiles = collectGitDiffFiles(d.cwd);
|
|
817
839
|
const handoffResult = processHandoff(output, {
|
|
818
840
|
exitCode: completion.exitCode,
|
|
819
841
|
resultFile: d.resultFile,
|
|
820
842
|
cli: d.cli,
|
|
821
|
-
gitDiffFiles,
|
|
843
|
+
gitDiffFiles: workerGitDiffFiles,
|
|
822
844
|
});
|
|
845
|
+
if (
|
|
846
|
+
completion.exitCode === 0 &&
|
|
847
|
+
workerGitDiffFiles.length === 0 &&
|
|
848
|
+
handoffResult.handoff?.lead_action === "accept"
|
|
849
|
+
) {
|
|
850
|
+
handoffResult.handoff = {
|
|
851
|
+
...handoffResult.handoff,
|
|
852
|
+
lead_action: "needs_read",
|
|
853
|
+
};
|
|
854
|
+
handoffResult.formatted = formatHandoffForLead(handoffResult.handoff);
|
|
855
|
+
}
|
|
823
856
|
const status =
|
|
824
857
|
handoffResult.handoff?.status ||
|
|
825
858
|
(completion.matched && completion.exitCode === 0
|
|
@@ -842,6 +875,7 @@ async function collectResults(sessionName, results) {
|
|
|
842
875
|
exitCode: completion.exitCode,
|
|
843
876
|
output,
|
|
844
877
|
resultFile: d.resultFile,
|
|
878
|
+
workerGitDiffFiles,
|
|
845
879
|
sessionDead: completion.sessionDead || false,
|
|
846
880
|
handoff: handoffResult.handoff,
|
|
847
881
|
handoffFormatted: handoffResult.formatted,
|
|
@@ -852,6 +886,20 @@ async function collectResults(sessionName, results) {
|
|
|
852
886
|
);
|
|
853
887
|
}
|
|
854
888
|
|
|
889
|
+
function collectGitDiffFiles(cwd) {
|
|
890
|
+
try {
|
|
891
|
+
const diffOut = execSync("git diff --name-only HEAD", {
|
|
892
|
+
cwd,
|
|
893
|
+
encoding: "utf8",
|
|
894
|
+
timeout: 5000,
|
|
895
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
896
|
+
});
|
|
897
|
+
return diffOut.trim().split("\n").filter(Boolean);
|
|
898
|
+
} catch {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
855
903
|
/**
|
|
856
904
|
* 헤드리스 CLI 오케스트레이션 실행
|
|
857
905
|
*
|
|
@@ -880,6 +928,19 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
880
928
|
|
|
881
929
|
mkdirSync(RESULT_DIR, { recursive: true });
|
|
882
930
|
|
|
931
|
+
// Hub version skew pre-flight (fail-open, best-effort)
|
|
932
|
+
requestJson("/status", { method: "GET", timeoutMs: 500 })
|
|
933
|
+
.then((status) => {
|
|
934
|
+
const hubRate = status?.spawn_trace?.max_per_sec;
|
|
935
|
+
const localRate = getMaxSpawnPerSec();
|
|
936
|
+
if (typeof hubRate === "number" && hubRate !== localRate) {
|
|
937
|
+
console.warn(
|
|
938
|
+
`[headless] Hub version skew detected: hub spawn rate=${hubRate}/s, local=${localRate}/s. Restart hub to sync.`,
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
})
|
|
942
|
+
.catch(() => {});
|
|
943
|
+
|
|
883
944
|
// Synapse: 세션 registration (fire-and-forget, hub 미응답 시 무시)
|
|
884
945
|
const synapseIds = assignments.map((_, i) => `${sessionName}-worker-${i + 1}`);
|
|
885
946
|
for (let i = 0; i < assignments.length; i++) {
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -1273,6 +1273,7 @@ export async function waitForPattern(
|
|
|
1273
1273
|
|
|
1274
1274
|
/**
|
|
1275
1275
|
* 완료 토큰이 찍힐 때까지 대기하고 exit code를 파싱한다.
|
|
1276
|
+
* NOTE: 주 채널은 headless.waitForCompletionWithStallDetect이며, 본 함수는 fallback 채널이다.
|
|
1276
1277
|
* @param {string} sessionName
|
|
1277
1278
|
* @param {string} paneNameOrTarget
|
|
1278
1279
|
* @param {string} token
|