triflux 10.9.32 → 10.10.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 (113) hide show
  1. package/CLAUDE.md +159 -0
  2. package/hooks/keyword-rules.json +39 -0
  3. package/hub/cli-adapter-base.mjs +0 -1
  4. package/hub/team/conductor.mjs +15 -3
  5. package/hub/team/execution-mode.mjs +98 -0
  6. package/hub/team/swarm-cli.mjs +11 -5
  7. package/hub/team/swarm-hypervisor.mjs +57 -4
  8. package/hub/team/swarm-planner.mjs +12 -0
  9. package/hub/workers/codex-app-server-worker.mjs +0 -1
  10. package/package.json +21 -62
  11. package/references/cli-parameter-reference.md +240 -0
  12. package/references/codex-plugin-cc-analysis.md +706 -0
  13. package/references/codex-plugin-cc-code-patterns.md +468 -0
  14. package/references/hosts.json +46 -0
  15. package/scripts/__tests__/release-governance.test.mjs +56 -0
  16. package/scripts/release/lib.mjs +39 -4
  17. package/scripts/release/prepare.mjs +68 -7
  18. package/skills/tfx-analysis/SKILL.md +153 -60
  19. package/skills/tfx-auto/SKILL.md +82 -4
  20. package/skills/tfx-auto-codex/SKILL.md +25 -90
  21. package/skills/tfx-autopilot/SKILL.md +26 -97
  22. package/skills/tfx-autoresearch/SKILL.md +5 -117
  23. package/skills/tfx-autoroute/SKILL.md +23 -170
  24. package/skills/tfx-codex/SKILL.md +21 -66
  25. package/skills/tfx-deep-analysis/SKILL.md +12 -216
  26. package/skills/tfx-deep-interview/SKILL.md +7 -187
  27. package/skills/tfx-deep-plan/SKILL.md +6 -271
  28. package/skills/tfx-deep-qa/SKILL.md +8 -149
  29. package/skills/tfx-deep-research/SKILL.md +8 -199
  30. package/skills/tfx-deep-review/SKILL.md +16 -162
  31. package/skills/tfx-fullcycle/SKILL.md +23 -268
  32. package/skills/tfx-gemini/SKILL.md +21 -74
  33. package/skills/tfx-multi/SKILL.md +23 -167
  34. package/skills/tfx-persist/SKILL.md +25 -257
  35. package/skills/tfx-plan/SKILL.md +156 -42
  36. package/skills/tfx-qa/SKILL.md +118 -83
  37. package/skills/tfx-research/SKILL.md +154 -93
  38. package/skills/tfx-review/SKILL.md +181 -13
  39. package/skills/tfx-ship/SKILL.md +324 -0
  40. package/skills/tfx-swarm/SKILL.md +25 -210
  41. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  42. package/skills/tfx-workspace/evals/evals.json +79 -0
  43. package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  47. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  48. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
  49. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  50. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  54. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  55. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
  56. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  57. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  61. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  62. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
  63. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  64. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  68. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  69. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
  70. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  71. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  72. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  76. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  77. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
  78. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  79. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  83. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  84. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
  85. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  86. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  87. package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  91. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  92. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
  93. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  94. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  95. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  96. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  97. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
  98. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  99. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  100. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
  101. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  102. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  103. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  104. package/.claude-plugin/marketplace.json +0 -34
  105. package/.claude-plugin/plugin.json +0 -22
  106. package/config/mcp-registry.json +0 -29
  107. package/tui/codex-profile.mjs +0 -457
  108. package/tui/core.mjs +0 -266
  109. package/tui/doctor.mjs +0 -375
  110. package/tui/gemini-profile.mjs +0 -299
  111. package/tui/monitor-data.mjs +0 -152
  112. package/tui/monitor.mjs +0 -330
  113. package/tui/setup.mjs +0 -598
package/CLAUDE.md ADDED
@@ -0,0 +1,159 @@
1
+ # triflux — Claude Code 운영 가이드
2
+
3
+ <core-systems>
4
+ ## 핵심 스킬 시스템 (항상 인지)
5
+
6
+ 이 프로젝트는 3개의 스킬 시스템을 동시에 사용한다. 어떤 작업이든 해당 시스템의 스킬이 있는지 먼저 확인한다.
7
+
8
+ | 시스템 | 접두사 | 용도 | 스킬 수 |
9
+ |--------|--------|------|---------|
10
+ | **triflux** | `/tfx-*` | CLI 라우팅, 멀티모델 오케스트레이션, 스웜, 원격 실행 | ~40개 |
11
+ | **gstack** | `/` (접두사 없음) | QA, ship, investigate, design, review, checkpoint | ~35개 |
12
+ | **omc** | `/oh-my-claudecode:*` | autopilot, ralph, team, ultrawork, ccg | ~25개 |
13
+
14
+ 스킬을 모르면 자연어 라우팅(`.claude/rules/tfx-routing.md`)으로 자동 매핑된다.
15
+ 세션 종료 전 메모리 파일이 3개+ 변경됐으면 `/memory-hygiene` 제안을 검토한다.
16
+ </core-systems>
17
+
18
+ <psmux-wt>
19
+ ## psmux/WT 규칙
20
+
21
+ psmux 세션·WT 패인을 생성/조작/정리할 때 `tfx-psmux-rules` 스킬을 참조한다.
22
+ WT 프리징 방지: exit → sleep 2 → kill 순서. 바로 kill하지 않는다.
23
+
24
+ ### wt.exe → wt-manager 경유
25
+
26
+ safety-guard가 `wt.exe`, `wt new-tab`, `wt split-pane`, `Start-Process wt`를 차단한다.
27
+ `hub/team/wt-manager.mjs`의 API를 사용한다.
28
+
29
+ | 용도 | API |
30
+ |------|-----|
31
+ | 새 탭 | `createTab({ title, command, profile, cwd })` |
32
+ | 패인 분할 | `splitPane({ direction: 'H'\|'V', title, command })` |
33
+ | 다중 배치 | `applySplitLayout([{ title, command, direction }])` |
34
+ | 탭 정리 | `closeTab(title)` / `closeStale({ olderThanMs, titlePattern })` |
35
+
36
+ 차단과 대안은 항상 쌍으로 존재해야 한다. 차단만 추가하고 대안을 안 만들면 데드락.
37
+
38
+ ### raw `psmux kill-session` → psmux wrapper 경유
39
+
40
+ safety-guard가 raw `psmux kill-session`을 차단한다.
41
+ 세션 정리는 `hub/team/psmux.mjs` 공개 API 또는 internal wrapper로 우회한다.
42
+
43
+ | 용도 | API / 래퍼 |
44
+ |------|------------|
45
+ | 세션 조회 | `listSessions({ filterTitle?, olderThanMs? })` |
46
+ | title prefix / regex kill | `killSessionByTitle(titlePattern)` |
47
+ | stale idle 세션 정리 | `pruneStale({ olderThanMs, dryRun })` |
48
+ | Bash 훅 우회용 래퍼 | `node hub/team/psmux.mjs --internal kill-by-title <prefix\|/regex/>` |
49
+
50
+ ### psmux에서 Codex 실행
51
+
52
+ | 방식 | 동작 | 이유 |
53
+ |------|------|------|
54
+ | `codex` (interactive) | 불가 | psmux에서 TTY를 못 잡음 |
55
+ | `codex < prompt.md` | 불가 | "stdin is not a terminal" |
56
+ | `codex exec "$(cat prompt.md)" -s danger-full-access --dangerously-bypass-approvals-and-sandbox` | 사용 | 유일한 안전 경로 |
57
+
58
+ `codex exec`는 config.toml `approval_mode`를 무시하므로 `--dangerously-bypass-approvals-and-sandbox` 필수.
59
+ `-s` 유효값: read-only, workspace-write, danger-full-access.
60
+ </psmux-wt>
61
+
62
+ <codex-config>
63
+ ## Codex config.toml
64
+
65
+ config.toml에 이미 설정된 값은 CLI 플래그로 중복 지정하지 않는다.
66
+
67
+ | config.toml에 있으면 | CLI에서 생략 |
68
+ |---------------------|-------------|
69
+ | `approval_mode = "auto"` | `-a`, `--full-auto` |
70
+ | `sandbox = "workspace-write"` | `-s`, `--full-auto` |
71
+
72
+ 안전 패턴: config.toml에 기본값을 두고, CLI에서는 `--profile` 선택만 한다.
73
+ </codex-config>
74
+
75
+ <account-broker>
76
+ ## AccountBroker (계정 브로커)
77
+
78
+ conductor, headless, swarm-hypervisor가 하나의 AccountBroker 싱글턴을 공유한다.
79
+
80
+ | 항목 | 설명 |
81
+ |------|------|
82
+ | 계정별 CircuitBreaker | 장애 격리 — 한 계정 오류가 다른 계정에 전파되지 않음 |
83
+ | busy 플래그 | 동일 계정 이중 임대(double-lease) 방지 |
84
+ | `/broker/reload` | 장시간 세션 중 accounts.json 핫리로드 |
85
+ | EventEmitter 이벤트 | `lease`, `release`, `cooldown`, `tierFallback`, `circuitOpen`, `circuitClose`, `noAvailableAccounts` — HUD 연동용 |
86
+ </account-broker>
87
+
88
+ <remote>
89
+ ## 원격 실행
90
+
91
+ ### 스킬 구분
92
+
93
+ | 스킬 | 대상 | 방식 |
94
+ |------|------|------|
95
+ | tfx-codex-swarm | 로컬 전용 | 로컬 worktree + psmux |
96
+ | tfx-remote-spawn | Claude Code 원격 | SSH → Claude Code 세션 → 내부 tfx 라우팅 |
97
+
98
+ codex를 SSH 너머로 직접 실행하지 않는다. config.toml 충돌 + TTY 문제.
99
+ 원격에서 codex가 필요하면: remote-spawn → Claude Code → Claude가 내부에서 codex 호출.
100
+
101
+ ### SSH 패턴
102
+
103
+ hosts.json `os` 필드로 대상 셸을 판단한다. safety-guard도 이 필드를 참조.
104
+
105
+ | 대상 OS | 셸 | 패턴 |
106
+ |---------|-----|------|
107
+ | windows | PowerShell | scp + `pwsh -File` 필수. `$var` → `$env:VAR`, `2>/dev/null` → `2>$null` |
108
+ | darwin | zsh | 인라인 가능. brew PATH 주의 (`/opt/homebrew/bin`) |
109
+ | linux | bash | 인라인 가능. 표준 POSIX |
110
+
111
+ - `~` → `$HOME` 변환은 모든 OS 공통
112
+ </remote>
113
+
114
+ <headless-retrieval>
115
+ ## Headless 결과 회수
116
+
117
+ background로 실행한 headless 결과는 **반드시 task-notification 완료 후** 읽는다.
118
+
119
+ | 패턴 | 올바름 | 이유 |
120
+ |------|--------|------|
121
+ | task-notification 후 output 파일 읽기 | YES | 프로세스 종료 = 워커 전부 완료 |
122
+ | task-notification 전 output 파일 tail | NO | 시작 메시지만 보이고 "실패"로 오진 |
123
+ | psmux capture-pane으로 중간 체크 | NO | 워커 진행 중이면 빈 화면일 수 있음 |
124
+
125
+ 완료 마커: `=== HEADLESS_COMPLETE succeeded=N failed=N total=N ===`
126
+ 워커 상세: `$TMPDIR/tfx-headless/{sessionName}-worker-N.txt`
127
+ </headless-retrieval>
128
+
129
+ <cross-review>
130
+ ## 교차 검증
131
+
132
+ - Claude 작성 코드 → Codex 리뷰
133
+ - Codex 작성 코드 → Claude 리뷰
134
+ - 동일 모델 self-approve 하지 않는다
135
+ - git commit 전 미검증 파일 감지 시 nudge
136
+ </cross-review>
137
+
138
+ <session-context>
139
+ ## 맥락 이탈 판단
140
+
141
+ 현재 세션 맥락과 무관한 요청이 감지되면 psmux 격리를 제안한다.
142
+
143
+ | 확신도 | 신호 | 행동 |
144
+ |--------|------|------|
145
+ | 확실 | "새 탭", "별도로", "새 세션" | 바로 psmux spawn |
146
+ | 높음 | 다른 프로젝트/스택 언급 | 분리 제안 |
147
+ | 중간 | 작업 유형 전환 | 분리 제안 + 현재 세션 옵션 |
148
+ | 낮음 | 현재 작업 연장 | 세션 유지 |
149
+ </session-context>
150
+
151
+ ## 세부 규칙은 `.claude/rules/` 참조
152
+
153
+ | 파일 | 내용 |
154
+ |------|------|
155
+ | `.claude/rules/tfx-routing.md` | 자연어 → 스킬 라우팅, CLI 라우팅 Layer 1~3, 충돌 해소 |
156
+ | `.claude/rules/tfx-execution-skill-map.md` | tfx-auto / multi / swarm 실행 엔진 매핑, 격리 기준, 안티패턴 |
157
+ | `.claude/rules/tfx-update-logic.md` | triflux / OMC / gstack / Codex / Gemini 업데이트 로직 |
158
+
159
+ Claude Code는 `.claude/rules/*.md` 를 자동 로드한다. Codex CLI는 `@import` 미지원이므로 필요 시 `AGENTS.md` 를 독립 유지한다.
@@ -664,6 +664,45 @@
664
664
  "exclusive": false,
665
665
  "state": null,
666
666
  "mcp_route": null
667
+ },
668
+ {
669
+ "id": "tfx-ship",
670
+ "patterns": [
671
+ {
672
+ "source": "\\btfx[\\s-]?ship\\b",
673
+ "flags": "i"
674
+ },
675
+ {
676
+ "source": "/ship\\b",
677
+ "flags": "i"
678
+ },
679
+ {
680
+ "source": "\\brelease\\b",
681
+ "flags": "i"
682
+ },
683
+ {
684
+ "source": "\\bpublish\\b",
685
+ "flags": "i"
686
+ },
687
+ {
688
+ "source": "배포(?!자|사|장|처)",
689
+ "flags": ""
690
+ },
691
+ {
692
+ "source": "릴리[즈스]",
693
+ "flags": ""
694
+ },
695
+ {
696
+ "source": "쉽\\s*(?:해|하자|가자|쳐)",
697
+ "flags": ""
698
+ }
699
+ ],
700
+ "skill": "tfx-ship",
701
+ "priority": 3,
702
+ "supersedes": ["gstack-ship"],
703
+ "exclusive": false,
704
+ "state": null,
705
+ "mcp_route": null
667
706
  }
668
707
  ]
669
708
  }
@@ -147,7 +147,6 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
147
147
  profile,
148
148
  skipGitRepoCheck = true,
149
149
  sandboxBypass = true,
150
- cwd,
151
150
  mcpServers,
152
151
  } = opts;
153
152
 
@@ -28,6 +28,7 @@ import {
28
28
  getConductorRegistry,
29
29
  } from "./conductor-registry.mjs";
30
30
  import { createEventLog } from "./event-log.mjs";
31
+ import { buildSpawnSpecForMode, MODES } from "./execution-mode.mjs";
31
32
  import { createHealthProbe } from "./health-probe.mjs";
32
33
  import { buildLauncher } from "./launcher-template.mjs";
33
34
  import {
@@ -490,6 +491,14 @@ export function createConductor(opts = {}) {
490
491
  let recentOutput = "";
491
492
 
492
493
  const spawnCwd = session.config.workdir || launcher.cwd || undefined;
494
+ const spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
495
+ cli: session.config.agent,
496
+ prompt: session.config.prompt,
497
+ profile: session.config.profile,
498
+ model: session.config.model,
499
+ mcpServers: session.config.mcpServers,
500
+ resolveCommand: opts.deps?.resolveCliExecutable,
501
+ });
493
502
 
494
503
  // #90 branch guard: shard spawn cwd가 main 브랜치면 즉시 abort.
495
504
  // swarm shard는 반드시 shard 전용 worktree 브랜치에서 실행되어야 한다.
@@ -524,8 +533,8 @@ export function createConductor(opts = {}) {
524
533
 
525
534
  let child;
526
535
  try {
527
- child = spawnFn(launcher.command, {
528
- shell: true,
536
+ child = spawnFn(spawnSpec.command, spawnSpec.args, {
537
+ shell: false,
529
538
  cwd: spawnCwd,
530
539
  env: {
531
540
  ...process.env,
@@ -544,6 +553,7 @@ export function createConductor(opts = {}) {
544
553
  session: session.id,
545
554
  error: err.message,
546
555
  cwd: spawnCwd || null,
556
+ command: spawnSpec.command,
547
557
  });
548
558
  handleFailure(session, `spawn_error:${err.message}`);
549
559
  return;
@@ -561,7 +571,9 @@ export function createConductor(opts = {}) {
561
571
  session: session.id,
562
572
  agent: session.config.agent,
563
573
  pid: child.pid,
564
- command: launcher.command,
574
+ command: spawnSpec.command,
575
+ args: spawnSpec.args,
576
+ legacyCommand: launcher.command,
565
577
  restart: session.restarts,
566
578
  });
567
579
 
@@ -1,5 +1,11 @@
1
1
  // hub/team/execution-mode.mjs — headless vs interactive execution mode selection
2
2
 
3
+ import { existsSync } from "node:fs";
4
+
5
+ import { whichCommand } from "../platform.mjs";
6
+
7
+ const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
8
+
3
9
  export const MODES = Object.freeze({
4
10
  HEADLESS: "headless",
5
11
  INTERACTIVE: "interactive",
@@ -10,6 +16,98 @@ function quotePrompt(prompt) {
10
16
  return JSON.stringify(typeof prompt === "string" ? prompt : "");
11
17
  }
12
18
 
19
+ function asPrompt(prompt) {
20
+ return typeof prompt === "string" ? prompt : "";
21
+ }
22
+
23
+ function pushFlag(args, flag, value) {
24
+ if (typeof value === "string" && value.length > 0) {
25
+ args.push(flag, value);
26
+ }
27
+ }
28
+
29
+ export function resolveCliExecutable(cli, opts = {}) {
30
+ const name = String(cli || "codex");
31
+ const resolveCommand = opts.resolveCommand || whichCommand;
32
+ const resolved = resolveCommand(name) || name;
33
+
34
+ // Windows: Node spawn({ shell: false }) calls CreateProcess directly and does NOT
35
+ // search PATHEXT. npm-installed CLIs (codex, gemini) live at `<npm>/codex` (Git Bash
36
+ // shell script) alongside `<npm>/codex.cmd` (Windows batch wrapper). whichCommand
37
+ // returns the extensionless path, which Windows cannot execute → ENOENT "The system
38
+ // cannot find the file specified." Append the correct extension when the resolved
39
+ // path has none.
40
+ const platform = opts.platform || process.platform;
41
+ if (platform === "win32" && resolved) {
42
+ const hasExt = /\.[^\\/.]+$/.test(resolved);
43
+ if (!hasExt) {
44
+ const existsFn = opts.existsSyncFn || existsSync;
45
+ for (const ext of WIN32_EXT_PRECEDENCE) {
46
+ const candidate = `${resolved}${ext}`;
47
+ try {
48
+ if (existsFn(candidate)) return candidate;
49
+ } catch {
50
+ // ignore stat failures, try next extension
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ return resolved;
57
+ }
58
+
59
+ export function buildSpawnSpecForMode(mode, opts = {}) {
60
+ const cli = opts.cli || "codex";
61
+ const prompt = asPrompt(opts.prompt);
62
+ const resolvedCommand = resolveCliExecutable(cli, opts);
63
+ const platform = opts.platform || process.platform;
64
+
65
+ // Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
66
+ // (EINVAL). npm-installed Windows wrappers (e.g. codex.cmd) hit this. Wrap via
67
+ // cmd.exe /c to keep shell:false while still launching the batch wrapper.
68
+ const needsCmdWrap =
69
+ platform === "win32" && /\.(cmd|bat)$/i.test(resolvedCommand);
70
+ const wrap = (args) =>
71
+ needsCmdWrap
72
+ ? { command: "cmd", args: ["/c", resolvedCommand, ...args] }
73
+ : { command: resolvedCommand, args };
74
+
75
+ if (cli === "gemini") {
76
+ const args = [];
77
+ pushFlag(args, "--model", opts.model);
78
+ args.push("--yolo", "--prompt", prompt, "--output-format", "text");
79
+ return { ...wrap(args), useExec: true, shell: false };
80
+ }
81
+
82
+ if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
83
+ return { ...wrap([]), useExec: false, shell: false };
84
+ }
85
+
86
+ if (cli === "claude") {
87
+ const args = [];
88
+ pushFlag(args, "--model", opts.model);
89
+ args.push("-p", prompt);
90
+ return { ...wrap(args), useExec: true, shell: false };
91
+ }
92
+
93
+ const args = [];
94
+ pushFlag(args, "--profile", opts.profile);
95
+ args.push(
96
+ "exec",
97
+ "--dangerously-bypass-approvals-and-sandbox",
98
+ "--skip-git-repo-check",
99
+ "--color",
100
+ "never",
101
+ );
102
+ if (Array.isArray(opts.mcpServers)) {
103
+ for (const server of opts.mcpServers) {
104
+ args.push("-c", `mcp_servers.${server}.enabled=true`);
105
+ }
106
+ }
107
+ args.push(prompt);
108
+ return { ...wrap(args), useExec: true, shell: false };
109
+ }
110
+
13
111
  /**
14
112
  * @param {{
15
113
  * cli: "codex"|"gemini"|"claude",
@@ -131,14 +131,20 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
131
131
  baseBranch: flags.baseBranch,
132
132
  });
133
133
 
134
- hyper.on("shardLaunched", ({ shard, sessionId, remote }) => {
134
+ hyper.on("shardLaunched", ({ shardName, sessionId, remote }) => {
135
135
  const tag = remote ? ` ${GRAY}(remote)${RESET}` : "";
136
- console.log(` ${GREEN}▸${RESET} launched: ${shard}${tag} [${sessionId}]`);
136
+ console.log(
137
+ ` ${GREEN}▸${RESET} launched: ${shardName}${tag} [${sessionId}]`,
138
+ );
139
+ });
140
+ hyper.on("shardCompleted", ({ shardName, sessionId, isRedundant }) => {
141
+ const tag = isRedundant ? ` ${GRAY}(redundant)${RESET}` : "";
142
+ console.log(` ${GREEN}✓${RESET} ${shardName}${tag} [${sessionId}]`);
137
143
  });
138
- hyper.on("shardCompleted", ({ shard, success, reason }) => {
139
- const mark = success ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
144
+ hyper.on("shardFailed", ({ shardName, failureMode, reason }) => {
140
145
  const reasonStr = reason ? ` ${GRAY}(${reason})${RESET}` : "";
141
- console.log(` ${mark} ${shard}${reasonStr}`);
146
+ const modeStr = failureMode ? ` ${GRAY}[${failureMode}]${RESET}` : "";
147
+ console.log(` ${RED}✗${RESET} ${shardName}${modeStr}${reasonStr}`);
142
148
  });
143
149
  hyper.on("warning", ({ type, ...rest }) => {
144
150
  console.error(
@@ -535,6 +535,15 @@ export function createSwarmHypervisor(opts) {
535
535
  branchName: sessionConfig.branchName,
536
536
  });
537
537
 
538
+ emitter.emit("shardLaunched", {
539
+ shardName: shard.name,
540
+ sessionId: sessionConfig.id,
541
+ agent: shard.agent,
542
+ isRedundant,
543
+ remote: Boolean(shard.host),
544
+ host: shard.host || null,
545
+ });
546
+
538
547
  const entry = {
539
548
  conductor,
540
549
  shardConfig: shard,
@@ -661,9 +670,47 @@ export function createSwarmHypervisor(opts) {
661
670
  lockManager.release(shardName);
662
671
 
663
672
  emitter.emit("shardFailed", { shardName, failureMode, reason });
673
+
674
+ // Cascade failure to dependents so they do not hold integration hostage
675
+ // (BFS over plan.shards). Without this, a dead shard keeps dependents in
676
+ // `pending` forever and integrateResults is never reachable.
677
+ cascadeDependencyFailure(shardName);
678
+
664
679
  checkAllShardsCompleted();
665
680
  }
666
681
 
682
+ function cascadeDependencyFailure(rootFailedShard) {
683
+ const queue = [rootFailedShard];
684
+ const visited = new Set([rootFailedShard]);
685
+ while (queue.length > 0) {
686
+ const failedName = queue.shift();
687
+ for (const candidate of plan.shards) {
688
+ if (visited.has(candidate.name)) continue;
689
+ if (failures.has(candidate.name)) continue;
690
+ if (completedShards.has(candidate.name)) continue;
691
+ if (!candidate.depends?.includes(failedName)) continue;
692
+
693
+ visited.add(candidate.name);
694
+ failures.set(candidate.name, {
695
+ mode: FAILURE_MODES.F1_CRASH,
696
+ reason: `dep_failed:${failedName}`,
697
+ sessionId: null,
698
+ });
699
+ lockManager.release(candidate.name);
700
+ eventLog.append("shard_blocked", {
701
+ shard: candidate.name,
702
+ failedDep: failedName,
703
+ });
704
+ emitter.emit("shardFailed", {
705
+ shardName: candidate.name,
706
+ failureMode: FAILURE_MODES.F1_CRASH,
707
+ reason: `dep_failed:${failedName}`,
708
+ });
709
+ queue.push(candidate.name);
710
+ }
711
+ }
712
+ }
713
+
667
714
  function classifyFailure(reason) {
668
715
  if (!reason) return FAILURE_MODES.F1_CRASH;
669
716
  const r = String(reason).toLowerCase();
@@ -687,10 +734,16 @@ export function createSwarmHypervisor(opts) {
687
734
  function checkAllShardsCompleted() {
688
735
  if (state !== SWARM_STATES.RUNNING) return;
689
736
 
690
- const allDone = plan.mergeOrder.every((name) => {
691
- const w = workers.get(name);
692
- return (w && isTerminal(w)) || failures.has(name);
693
- });
737
+ // `completedShards` + `failures` are set synchronously inside
738
+ // handleShardCompleted / handleShardFailed, so they are the authoritative
739
+ // terminal signal. `isTerminal(w)` queries conductor.getSnapshot() which
740
+ // lags because the snapshot state transition is applied asynchronously
741
+ // after the onCompleted callback chain that invoked this check. Using the
742
+ // snapshot here races with its own producer and leaves integrateResults
743
+ // unreachable (2026-04-17 swarm hang — required manual cherry-pick).
744
+ const allDone = plan.mergeOrder.every(
745
+ (name) => completedShards.has(name) || failures.has(name),
746
+ );
694
747
 
695
748
  if (allDone) {
696
749
  void integrateResults();
@@ -256,6 +256,18 @@ export function planSwarm(prdPath, opts = {}) {
256
256
  );
257
257
  }
258
258
 
259
+ const emptyPromptShards = shards.filter(
260
+ (s) => !s.prompt || s.prompt.trim() === "",
261
+ );
262
+ if (emptyPromptShards.length > 0) {
263
+ const names = emptyPromptShards.map((s) => s.name).join(", ");
264
+ throw new Error(
265
+ `Shard(s) with empty prompt: ${names}. ` +
266
+ `Each shard must include a non-empty "- prompt: |" block. ` +
267
+ `See docs/prd/_template.md for PRD format.`,
268
+ );
269
+ }
270
+
259
271
  const { leaseMap, conflicts } = buildFileLeaseMap(shards);
260
272
  const mcpManifest = buildMcpManifest(shards);
261
273
  const { order: mergeOrder, cycles } = computeMergeOrder(shards);
@@ -361,7 +361,6 @@ export class CodexAppServerWorker {
361
361
  typeof options.warn === "function"
362
362
  ? options.warn
363
363
  : (label, payload) => {
364
- // biome-ignore lint/suspicious/noConsole: defensive warn-once path
365
364
  console.warn(`[codex-app-server] ${label}`, payload);
366
365
  };
367
366
  this._onUnknownMethod =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.9.32",
3
+ "version": "10.10.0",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,75 +13,26 @@
13
13
  "tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
14
14
  "tfx-setup-tui": "bin/tfx-setup-tui.mjs"
15
15
  },
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "dependencies": {
20
+ "@triflux/core": "10.0.1",
21
+ "@triflux/remote": "^10.0.0-alpha.1"
22
+ },
16
23
  "files": [
17
24
  "bin",
18
- "tui",
19
- "hub",
20
- "config",
21
25
  "skills",
22
- "!skills/tfx-workspace",
23
- "!**/failure-reports",
24
- "scripts",
25
26
  "hooks",
26
27
  "hud",
28
+ "scripts",
29
+ "hub",
27
30
  "mesh",
28
- ".claude-plugin",
31
+ "references",
32
+ "CLAUDE.md",
29
33
  "README.md",
30
- "README.ko.md",
31
34
  "LICENSE"
32
35
  ],
33
- "workspaces": [
34
- "packages/core",
35
- "packages/remote",
36
- "packages/triflux"
37
- ],
38
- "scripts": {
39
- "pack": "node scripts/pack.mjs all",
40
- "pack:core": "node scripts/pack.mjs core",
41
- "pack:remote": "node scripts/pack.mjs remote",
42
- "setup": "node scripts/setup.mjs",
43
- "preinstall": "node scripts/preinstall.mjs",
44
- "postinstall": "node scripts/setup.mjs",
45
- "lint": "biome check bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
46
- "lint:fix": "biome check --write bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
47
- "health": "npm test && npm run lint",
48
- "test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
49
- "test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
50
- "test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
51
- "test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
52
- "test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
53
- "test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
54
- "gen:skill-docs": "node scripts/gen-skill-docs.mjs",
55
- "gen:skill-manifest": "node scripts/gen-skill-manifest.mjs",
56
- "release:check-sync": "node scripts/release/check-sync.mjs",
57
- "release:check-sync:fix": "node scripts/release/check-sync.mjs --fix",
58
- "release:bump": "node scripts/release/bump-version.mjs",
59
- "release:prepare": "node scripts/release/prepare.mjs",
60
- "release:publish": "node scripts/release/publish.mjs",
61
- "release:verify": "node scripts/release/verify.mjs"
62
- },
63
- "engines": {
64
- "node": ">=18.0.0"
65
- },
66
- "repository": {
67
- "type": "git",
68
- "url": "git+https://github.com/tellang/triflux.git"
69
- },
70
- "homepage": "https://github.com/tellang/triflux#readme",
71
- "author": "tellang",
72
- "license": "MIT",
73
- "dependencies": {
74
- "@modelcontextprotocol/sdk": "^1.29.0",
75
- "better-sqlite3": "^12.6.2",
76
- "pino": "^10.3.1",
77
- "pino-pretty": "^13.1.3",
78
- "systray2": "^2.1.4",
79
- "zod": "^4.0.0"
80
- },
81
- "devDependencies": {
82
- "@biomejs/biome": "^2.0.0",
83
- "knip": "^6.3.0"
84
- },
85
36
  "keywords": [
86
37
  "claude-code",
87
38
  "plugin",
@@ -92,5 +43,13 @@
92
43
  "multi-model",
93
44
  "triflux",
94
45
  "tfx"
95
- ]
46
+ ],
47
+ "author": "tellang",
48
+ "license": "MIT",
49
+ "homepage": "https://github.com/tellang/triflux#readme",
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/tellang/triflux.git",
53
+ "directory": "packages/triflux"
54
+ }
96
55
  }