triflux 10.9.17 → 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.
@@ -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.17",
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.17"
33
+ "version": "10.9.18"
34
34
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.3.4",
3
+ "version": "10.9.17",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
@@ -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,
@@ -22,7 +22,11 @@ import { getMaxSpawnPerSec } from "../lib/spawn-trace.mjs";
22
22
  import { escapePwshSingleQuoted } from "../cli-adapter-base.mjs";
23
23
  import { getBackend } from "./backend.mjs";
24
24
  import { resolveDashboardLayout } from "./dashboard-layout.mjs";
25
- import { HANDOFF_INSTRUCTION_SHORT, processHandoff } from "./handoff.mjs";
25
+ import {
26
+ formatHandoffForLead,
27
+ HANDOFF_INSTRUCTION_SHORT,
28
+ processHandoff,
29
+ } from "./handoff.mjs";
26
30
  import {
27
31
  capturePsmuxPane,
28
32
  createPsmuxSession,
@@ -317,7 +321,7 @@ export function createStallMonitor(paneId, resultFile, config, deps = {}) {
317
321
  * @param {string} [opts.command] — re-dispatch용 원본 명령
318
322
  * @param {string} [opts.token] — completion token
319
323
  * @param {(snapshot: string) => void} [opts.onPoll] — 폴링 콜백
320
- * @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 }>}
321
325
  */
322
326
  export async function waitForCompletionWithStallDetect(
323
327
  sessionName,
@@ -347,19 +351,23 @@ export async function waitForCompletionWithStallDetect(
347
351
  const _startCapture = deps.startCapture || startCapture;
348
352
 
349
353
  const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
350
- const completionPatterns = [
351
- token
352
- ? `${esc("__TRIFLUX_DONE__:")}${esc(token)}:(\\d+)`
353
- : `${esc("__TRIFLUX_DONE__:")}\\S+:(\\d+)`,
354
- token
355
- ? `${esc("TFX_DONE_")}${esc(token)}:(\\d+)`
356
- : `${esc("TFX_DONE_")}\\S+:(\\d+)`,
357
- ];
358
- const completionRe = new RegExp(completionPatterns.join("|"), "m");
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
+ };
359
365
 
360
366
  let restarts = 0;
361
367
  let currentPaneId = paneId;
362
368
  let stallDetected = false;
369
+ let currentToken = token;
370
+ let currentLogPath = opts.logPath || null;
363
371
 
364
372
  while (true) {
365
373
  let lastOutput = "";
@@ -386,6 +394,9 @@ export async function waitForCompletionWithStallDetect(
386
394
  restarts,
387
395
  stallDetected,
388
396
  timedOut: true,
397
+ paneId: currentPaneId,
398
+ token: currentToken,
399
+ logPath: currentLogPath,
389
400
  };
390
401
  }
391
402
 
@@ -400,6 +411,7 @@ export async function waitForCompletionWithStallDetect(
400
411
  }
401
412
 
402
413
  // 2) completion 토큰 감지
414
+ const completionRe = buildCompletionRegex(currentToken);
403
415
  const completionMatch = completionRe.exec(currentOutput);
404
416
  if (completionMatch) {
405
417
  return {
@@ -411,6 +423,9 @@ export async function waitForCompletionWithStallDetect(
411
423
  restarts,
412
424
  stallDetected,
413
425
  timedOut: false,
426
+ paneId: currentPaneId,
427
+ token: currentToken,
428
+ logPath: currentLogPath,
414
429
  };
415
430
  }
416
431
 
@@ -443,6 +458,9 @@ export async function waitForCompletionWithStallDetect(
443
458
  restarts,
444
459
  stallDetected,
445
460
  timedOut: false,
461
+ paneId: currentPaneId,
462
+ token: currentToken,
463
+ logPath: currentLogPath,
446
464
  };
447
465
  }
448
466
  } catch {
@@ -481,8 +499,10 @@ export async function waitForCompletionWithStallDetect(
481
499
  "#{session_name}:#{window_index}.#{pane_index}",
482
500
  ]);
483
501
  _startCapture(sessionName, newPaneId);
484
- _dispatch(sessionName, newPaneId, command);
485
- 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;
486
506
  }
487
507
 
488
508
  restarts++;
@@ -565,7 +585,11 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
565
585
  assignment.cli,
566
586
  assignment.prompt,
567
587
  resultFile,
568
- { mcp: assignment.mcp, model: assignment.model },
588
+ {
589
+ mcp: assignment.mcp,
590
+ model: assignment.model,
591
+ cwd: assignment.cwd || assignment.workdir,
592
+ },
569
593
  );
570
594
  startCapture(sessionName, newPaneId);
571
595
  // pane 간 pipe-pane EBUSY 방지 — 이벤트 루프 해방하며 순차 대기
@@ -585,6 +609,7 @@ async function dispatchProgressive(sessionName, assignments, opts = {}) {
585
609
  role: assignment.role,
586
610
  command: cmd,
587
611
  workerId,
612
+ cwd: assignment.cwd || assignment.workdir,
588
613
  });
589
614
  }
590
615
 
@@ -636,7 +661,11 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
636
661
  assignment.cli,
637
662
  assignment.prompt,
638
663
  resultFile,
639
- { mcp: assignment.mcp, model: assignment.model },
664
+ {
665
+ mcp: assignment.mcp,
666
+ model: assignment.model,
667
+ cwd: assignment.cwd || assignment.workdir,
668
+ },
640
669
  );
641
670
  const scriptDir = join(RESULT_DIR, sessionName);
642
671
  await registerHeadlessWorker(sessionName, i, assignment.cli);
@@ -661,6 +690,7 @@ async function dispatchBatch(sessionName, assignments, opts = {}) {
661
690
  role: assignment.role,
662
691
  command: cmd,
663
692
  workerId,
693
+ cwd: assignment.cwd || assignment.workdir,
664
694
  };
665
695
  }),
666
696
  );
@@ -740,6 +770,9 @@ async function awaitAll(
740
770
  onPoll: stallPollCb,
741
771
  },
742
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;
743
776
  completion = {
744
777
  matched: stallResult.matched,
745
778
  exitCode: stallResult.exitCode,
@@ -799,28 +832,27 @@ async function awaitAll(
799
832
  * @returns {Array}
800
833
  */
801
834
  async function collectResults(sessionName, results) {
802
- // B3 fix: git diff를 루프 밖에서 1회만 실행 (워커 수만큼 중복 방지)
803
- let gitDiffFiles;
804
- try {
805
- const diffOut = execSync("git diff --name-only HEAD", {
806
- encoding: "utf8",
807
- timeout: 5000,
808
- stdio: ["pipe", "pipe", "pipe"],
809
- });
810
- gitDiffFiles = diffOut.trim().split("\n").filter(Boolean);
811
- } catch {
812
- /* git 미설치 또는 non-repo — 무시 */
813
- }
814
-
815
835
  // handoff 파이프라인: parse → validate → format (각 워커 결과에 적용)
816
836
  return await Promise.all(
817
837
  results.map(async ({ d, completion, output }) => {
838
+ const workerGitDiffFiles = collectGitDiffFiles(d.cwd);
818
839
  const handoffResult = processHandoff(output, {
819
840
  exitCode: completion.exitCode,
820
841
  resultFile: d.resultFile,
821
842
  cli: d.cli,
822
- gitDiffFiles,
843
+ gitDiffFiles: workerGitDiffFiles,
823
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
+ }
824
856
  const status =
825
857
  handoffResult.handoff?.status ||
826
858
  (completion.matched && completion.exitCode === 0
@@ -843,6 +875,7 @@ async function collectResults(sessionName, results) {
843
875
  exitCode: completion.exitCode,
844
876
  output,
845
877
  resultFile: d.resultFile,
878
+ workerGitDiffFiles,
846
879
  sessionDead: completion.sessionDead || false,
847
880
  handoff: handoffResult.handoff,
848
881
  handoffFormatted: handoffResult.formatted,
@@ -853,6 +886,20 @@ async function collectResults(sessionName, results) {
853
886
  );
854
887
  }
855
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
+
856
903
  /**
857
904
  * 헤드리스 CLI 오케스트레이션 실행
858
905
  *
@@ -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