triflux 10.9.32 → 10.11.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 (132) hide show
  1. package/CLAUDE.md +159 -0
  2. package/bin/triflux.mjs +5 -1
  3. package/hooks/keyword-rules.json +39 -0
  4. package/hub/bridge.mjs +133 -1
  5. package/hub/cli-adapter-base.mjs +0 -1
  6. package/hub/lib/tfx-route-args.mjs +222 -0
  7. package/hub/server.mjs +1 -1
  8. package/hub/team/conductor.mjs +15 -3
  9. package/hub/team/execution-mode.mjs +98 -0
  10. package/hub/team/git-preflight.mjs +2 -2
  11. package/hub/team/retry-state-machine.mjs +260 -0
  12. package/hub/team/runtime-strategy.mjs +1 -5
  13. package/hub/team/swarm-cli.mjs +11 -5
  14. package/hub/team/swarm-hypervisor.mjs +57 -4
  15. package/hub/team/swarm-planner.mjs +12 -0
  16. package/hub/team/tui-lite.mjs +0 -4
  17. package/hub/team/tui.mjs +0 -3
  18. package/hub/team/wt-manager.mjs +1 -1
  19. package/hub/workers/codex-app-server-worker.mjs +1 -2
  20. package/hud/context-monitor.mjs +2 -4
  21. package/package.json +21 -62
  22. package/references/cli-parameter-reference.md +240 -0
  23. package/references/codex-plugin-cc-analysis.md +706 -0
  24. package/references/codex-plugin-cc-code-patterns.md +468 -0
  25. package/references/hosts.json +46 -0
  26. package/scripts/__tests__/release-governance.test.mjs +56 -0
  27. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +0 -2
  28. package/scripts/claudemd-sync.mjs +33 -5
  29. package/scripts/config-audit.mjs +4 -4
  30. package/scripts/preinstall.mjs +1 -1
  31. package/scripts/release/lib.mjs +39 -4
  32. package/scripts/release/prepare.mjs +68 -7
  33. package/scripts/remote-spawn.mjs +1 -1
  34. package/scripts/session-spawn-helper.mjs +0 -1
  35. package/scripts/setup.mjs +4 -1
  36. package/skills/tfx-analysis/SKILL.md +153 -60
  37. package/skills/tfx-auto/SKILL.md +151 -4
  38. package/skills/tfx-auto-codex/SKILL.md +27 -88
  39. package/skills/tfx-autopilot/SKILL.md +26 -97
  40. package/skills/tfx-autoresearch/SKILL.md +5 -117
  41. package/skills/tfx-autoroute/SKILL.md +35 -167
  42. package/skills/tfx-codex/SKILL.md +21 -66
  43. package/skills/tfx-deep-analysis/SKILL.md +12 -216
  44. package/skills/tfx-deep-interview/SKILL.md +7 -187
  45. package/skills/tfx-deep-plan/SKILL.md +6 -271
  46. package/skills/tfx-deep-qa/SKILL.md +8 -149
  47. package/skills/tfx-deep-research/SKILL.md +8 -199
  48. package/skills/tfx-deep-review/SKILL.md +16 -162
  49. package/skills/tfx-fullcycle/SKILL.md +23 -268
  50. package/skills/tfx-gemini/SKILL.md +21 -74
  51. package/skills/tfx-interview/SKILL.md +37 -2
  52. package/skills/tfx-multi/SKILL.md +23 -167
  53. package/skills/tfx-persist/SKILL.md +33 -252
  54. package/skills/tfx-plan/SKILL.md +156 -42
  55. package/skills/tfx-qa/SKILL.md +118 -83
  56. package/skills/tfx-research/SKILL.md +154 -93
  57. package/skills/tfx-review/SKILL.md +181 -13
  58. package/skills/tfx-ship/SKILL.md +324 -0
  59. package/skills/tfx-swarm/SKILL.md +25 -210
  60. package/skills/tfx-workspace/async-tests/run-tests.sh +203 -0
  61. package/skills/tfx-workspace/evals/evals.json +79 -0
  62. package/skills/tfx-workspace/iteration-1/benchmark.json +524 -0
  63. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +11 -0
  64. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +25 -0
  65. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +154 -0
  66. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +5 -0
  67. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +25 -0
  68. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +126 -0
  69. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +5 -0
  70. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +11 -0
  71. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +25 -0
  72. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +119 -0
  73. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +5 -0
  74. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +25 -0
  75. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +115 -0
  76. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +5 -0
  77. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +10 -0
  78. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +20 -0
  79. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +86 -0
  80. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +5 -0
  81. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +20 -0
  82. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +81 -0
  83. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +5 -0
  84. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +12 -0
  85. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +30 -0
  86. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +316 -0
  87. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +5 -0
  88. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +30 -0
  89. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +352 -0
  90. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +5 -0
  91. package/skills/tfx-workspace/iteration-1/review.html +1325 -0
  92. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +12 -0
  93. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +30 -0
  94. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +97 -0
  95. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +5 -0
  96. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +30 -0
  97. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +94 -0
  98. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +5 -0
  99. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +12 -0
  100. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +30 -0
  101. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +209 -0
  102. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +5 -0
  103. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +30 -0
  104. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +193 -0
  105. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +5 -0
  106. package/skills/tfx-workspace/iteration-2/benchmark.json +144 -0
  107. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +13 -0
  108. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +35 -0
  109. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +382 -0
  110. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +5 -0
  111. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +35 -0
  112. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +333 -0
  113. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +5 -0
  114. package/skills/tfx-workspace/iteration-2/review.html +1325 -0
  115. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +217 -0
  116. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +77 -0
  117. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +65 -0
  118. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +94 -0
  119. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +82 -0
  120. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +133 -0
  121. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +426 -0
  122. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +101 -0
  123. package/.claude-plugin/marketplace.json +0 -34
  124. package/.claude-plugin/plugin.json +0 -22
  125. package/config/mcp-registry.json +0 -29
  126. package/tui/codex-profile.mjs +0 -457
  127. package/tui/core.mjs +0 -266
  128. package/tui/doctor.mjs +0 -375
  129. package/tui/gemini-profile.mjs +0 -299
  130. package/tui/monitor-data.mjs +0 -152
  131. package/tui/monitor.mjs +0 -330
  132. 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` 를 독립 유지한다.
package/bin/triflux.mjs CHANGED
@@ -882,7 +882,11 @@ function getClaudeRoutingSyncSummary(results) {
882
882
  (summary, result) => ({
883
883
  changed:
884
884
  summary.changed +
885
- (result.action === "created" || result.action === "updated" ? 1 : 0),
885
+ (result.action === "created" ||
886
+ result.action === "updated" ||
887
+ result.action === "removed"
888
+ ? 1
889
+ : 0),
886
890
  skipped: summary.skipped + (result.skipped ? 1 : 0),
887
891
  }),
888
892
  { changed: 0, skipped: 0 },
@@ -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
  }
package/hub/bridge.mjs CHANGED
@@ -21,6 +21,12 @@ import { fileURLToPath } from "node:url";
21
21
  import { parseArgs as nodeParseArgs } from "node:util";
22
22
 
23
23
  import { getPipelineStateDbPath } from "./pipeline/state.mjs";
24
+ import {
25
+ createRetryStateMachine,
26
+ DEFAULT_ESCALATION_CHAIN,
27
+ loadSnapshot,
28
+ saveSnapshot,
29
+ } from "./team/retry-state-machine.mjs";
24
30
 
25
31
  const HUB_PID_FILE = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
26
32
  const HUB_TOKEN_FILE = join(homedir(), ".claude", ".tfx-hub-token");
@@ -409,6 +415,10 @@ export function parseArgs(argv) {
409
415
  done: { type: "boolean" },
410
416
  "mcp-profile": { type: "string" },
411
417
  "session-key": { type: "string" },
418
+ snapshot: { type: "string" },
419
+ "snapshot-file": { type: "string" },
420
+ event: { type: "string" },
421
+ "max-iterations": { type: "string" },
412
422
  },
413
423
  allowPositionals: true,
414
424
  strict: false,
@@ -1079,6 +1089,124 @@ async function cmdHitlPending() {
1079
1089
  return emitJson(outcome?.result || unavailableResult());
1080
1090
  }
1081
1091
 
1092
+ // ---------------------------------------------------------------------------
1093
+ // retry-run / retry-status — Phase 3 Step C2 bridge 서브커맨드.
1094
+ // retry-state-machine.mjs 를 multi-process safe 하게 외부 호출용 wrap.
1095
+ // 사용자 워크플로우:
1096
+ // 1) 첫 호출: retry-run --snapshot X --mode ralph --event start
1097
+ // → 새 SM 생성, PLANNING → EXECUTING transition, snapshot 저장
1098
+ // 2) verify 성공 시: retry-run --snapshot X --event verify-success
1099
+ // → DONE, 종료 판단 반환
1100
+ // 3) verify 실패 시: retry-run --snapshot X --event verify-fail --reason R
1101
+ // → DIAGNOSING 또는 STUCK/BUDGET_EXCEEDED, 종료 판단 반환
1102
+ // 4) 다음 iter 시작: retry-run --snapshot X --event start
1103
+ // 출력: {ok, current, iterations, done, shouldStop, reason?, cli?} JSON.
1104
+ // ---------------------------------------------------------------------------
1105
+
1106
+ function buildRetrySmFromArgs(args, snapshot) {
1107
+ const mode = args.mode || snapshot?.mode || "bounded";
1108
+ const maxIterations =
1109
+ args["max-iterations"] !== undefined
1110
+ ? Number(args["max-iterations"])
1111
+ : snapshot?.maxIterations;
1112
+ const sessionId = args["session-id"] || snapshot?.sessionId || null;
1113
+ const cliChain = snapshot?.cliChain;
1114
+
1115
+ const sm = createRetryStateMachine({
1116
+ mode,
1117
+ maxIterations,
1118
+ sessionId,
1119
+ cliChain,
1120
+ });
1121
+ if (snapshot) sm.applySnapshot(snapshot);
1122
+ return sm;
1123
+ }
1124
+
1125
+ async function cmdRetryRun(args) {
1126
+ const snapshotFile = args.snapshot || args["snapshot-file"];
1127
+ const event = args.event;
1128
+ const reason = args.reason || "";
1129
+
1130
+ if (!snapshotFile) {
1131
+ console.error("--snapshot <path> required");
1132
+ return false;
1133
+ }
1134
+ if (!event) {
1135
+ console.error("--event <start|verify-success|verify-fail> required");
1136
+ return false;
1137
+ }
1138
+
1139
+ const existing = loadSnapshot(snapshotFile);
1140
+ const sm = buildRetrySmFromArgs(args, existing);
1141
+
1142
+ let result;
1143
+ switch (event) {
1144
+ case "start":
1145
+ result = sm.startIteration();
1146
+ break;
1147
+ case "verify-success":
1148
+ result = sm.reportVerifySuccess();
1149
+ break;
1150
+ case "verify-fail":
1151
+ result = sm.reportVerifyFail(reason || "unspecified");
1152
+ break;
1153
+ default:
1154
+ console.error(`unknown --event: ${event}`);
1155
+ return false;
1156
+ }
1157
+
1158
+ const snap = sm.serialize();
1159
+ saveSnapshot(snapshotFile, snap);
1160
+
1161
+ const terminal = ["DONE", "STUCK", "BUDGET_EXCEEDED"].includes(snap.current);
1162
+ const cli = snap.cliChain?.[snap.cliIndex] || null;
1163
+ const out = {
1164
+ ok: true,
1165
+ current: snap.current,
1166
+ iterations: snap.iterations,
1167
+ cliIndex: snap.cliIndex,
1168
+ cli,
1169
+ done: snap.current === "DONE",
1170
+ shouldStop: terminal,
1171
+ stuckCounter: snap.stuckCounter,
1172
+ lastFailureReason: snap.lastFailureReason,
1173
+ transition: result,
1174
+ };
1175
+ console.log(JSON.stringify(out));
1176
+ return true;
1177
+ }
1178
+
1179
+ async function cmdRetryStatus(args) {
1180
+ const snapshotFile = args.snapshot || args["snapshot-file"];
1181
+ if (!snapshotFile) {
1182
+ console.error("--snapshot <path> required");
1183
+ return false;
1184
+ }
1185
+ const snap = loadSnapshot(snapshotFile);
1186
+ if (!snap) {
1187
+ console.log(JSON.stringify({ ok: true, exists: false }));
1188
+ return true;
1189
+ }
1190
+ const terminal = ["DONE", "STUCK", "BUDGET_EXCEEDED"].includes(snap.current);
1191
+ const cli = snap.cliChain?.[snap.cliIndex] || null;
1192
+ console.log(
1193
+ JSON.stringify({
1194
+ ok: true,
1195
+ exists: true,
1196
+ current: snap.current,
1197
+ iterations: snap.iterations,
1198
+ maxIterations: snap.maxIterations,
1199
+ cliIndex: snap.cliIndex,
1200
+ cli,
1201
+ mode: snap.mode,
1202
+ shouldStop: terminal,
1203
+ stuckCounter: snap.stuckCounter,
1204
+ lastFailureReason: snap.lastFailureReason,
1205
+ }),
1206
+ );
1207
+ return true;
1208
+ }
1209
+
1082
1210
  export async function main(argv = process.argv.slice(2)) {
1083
1211
  const cmd = argv[0];
1084
1212
  const args = parseArgs(argv.slice(1));
@@ -1138,9 +1266,13 @@ export async function main(argv = process.argv.slice(2)) {
1138
1266
  return await cmdHitlSubmit(args);
1139
1267
  case "hitl-pending":
1140
1268
  return await cmdHitlPending(args);
1269
+ case "retry-run":
1270
+ return await cmdRetryRun(args);
1271
+ case "retry-status":
1272
+ return await cmdRetryStatus(args);
1141
1273
  default:
1142
1274
  console.error(
1143
- "사용법: bridge.mjs <register|result|control|handoff|publish|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending> [--옵션]",
1275
+ "사용법: bridge.mjs <register|result|control|handoff|publish|send-input|context|deregister|assign-async|assign-result|assign-status|assign-retry|team-info|team-task-list|team-task-update|team-send-message|pipeline-state|pipeline-advance|pipeline-init|pipeline-list|ping|delegator-delegate|delegator-reply|delegator-status|hitl-request|hitl-submit|hitl-pending|retry-run|retry-status> [--옵션]",
1144
1276
  );
1145
1277
  process.exit(1);
1146
1278
  }
@@ -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
 
@@ -0,0 +1,222 @@
1
+ // hub/lib/tfx-route-args.mjs
2
+ // Phase 3 Step B — tfx-auto / tfx-route 플래그 파서.
3
+ // 설계 문서: .triflux/plans/phase3-lead-codex-ralph-escalate.md
4
+ //
5
+ // 입력: ARGUMENTS 문자열 또는 토큰 배열.
6
+ // 출력: {cli, mode, parallel, retry, isolation, remote, lead, noClaudeNative,
7
+ // maxIterations, task, warnings}.
8
+ //
9
+ // 기존 플래그 (Phase 2 v10.9.33+):
10
+ // --cli {auto|codex|gemini|claude}
11
+ // --mode {quick|deep|consensus}
12
+ // --parallel {1|N|swarm}
13
+ // --retry {0|1|ralph|auto-escalate} (Phase 3 에서 ralph/auto-escalate 신규)
14
+ // --isolation {none|worktree}
15
+ // --remote {none|<host>}
16
+ //
17
+ // Phase 3 신규:
18
+ // --lead {claude|codex} (tfx-auto-codex 의미 흡수)
19
+ // --no-claude-native (Claude native sub-agent 경로 disable)
20
+ // --max-iterations <N> (ralph / auto-escalate 상한, 0=unlimited)
21
+
22
+ export const DEFAULT_OPTIONS = Object.freeze({
23
+ cli: "auto",
24
+ mode: "quick",
25
+ parallel: "1",
26
+ retry: "1",
27
+ isolation: "none",
28
+ remote: "none",
29
+ lead: "claude",
30
+ noClaudeNative: false,
31
+ maxIterations: 0,
32
+ });
33
+
34
+ const VALID_VALUES = Object.freeze({
35
+ cli: ["auto", "codex", "gemini", "claude"],
36
+ mode: ["quick", "deep", "consensus"],
37
+ retry: ["0", "1", "ralph", "auto-escalate"],
38
+ isolation: ["none", "worktree"],
39
+ lead: ["claude", "codex"],
40
+ });
41
+
42
+ const VALUE_FLAGS = new Set([
43
+ "--cli",
44
+ "--mode",
45
+ "--parallel",
46
+ "--retry",
47
+ "--isolation",
48
+ "--remote",
49
+ "--lead",
50
+ "--max-iterations",
51
+ ]);
52
+
53
+ const BOOL_FLAGS = new Set(["--no-claude-native"]);
54
+
55
+ export function parseArgs(input) {
56
+ const tokens = Array.isArray(input)
57
+ ? input.slice()
58
+ : tokenize(String(input || ""));
59
+ const opts = { ...DEFAULT_OPTIONS };
60
+ const warnings = [];
61
+ const taskTokens = [];
62
+
63
+ let i = 0;
64
+ while (i < tokens.length) {
65
+ const raw = tokens[i];
66
+
67
+ if (BOOL_FLAGS.has(raw)) {
68
+ applyBool(opts, raw);
69
+ i += 1;
70
+ continue;
71
+ }
72
+
73
+ // --flag=value 지원
74
+ const eqIdx = raw.indexOf("=");
75
+ let flag = raw;
76
+ let value = null;
77
+ if (eqIdx > 0 && raw.startsWith("--")) {
78
+ flag = raw.slice(0, eqIdx);
79
+ value = raw.slice(eqIdx + 1);
80
+ }
81
+
82
+ if (VALUE_FLAGS.has(flag)) {
83
+ if (value === null) {
84
+ const next = tokens[i + 1];
85
+ if (next === undefined || next.startsWith("--")) {
86
+ warnings.push(`${flag} needs a value`);
87
+ i += 1;
88
+ continue;
89
+ }
90
+ value = next;
91
+ i += 2;
92
+ } else {
93
+ i += 1;
94
+ }
95
+ applyValue(opts, flag, value, warnings);
96
+ continue;
97
+ }
98
+
99
+ if (raw.startsWith("--")) {
100
+ warnings.push(`unknown flag: ${raw}`);
101
+ i += 1;
102
+ continue;
103
+ }
104
+
105
+ taskTokens.push(raw);
106
+ i += 1;
107
+ }
108
+
109
+ validate(opts, warnings);
110
+
111
+ return {
112
+ ...opts,
113
+ task: taskTokens.join(" ").trim(),
114
+ warnings,
115
+ };
116
+ }
117
+
118
+ function tokenize(str) {
119
+ const tokens = [];
120
+ let cur = "";
121
+ let quote = null;
122
+ for (const ch of str) {
123
+ if (quote) {
124
+ if (ch === quote) {
125
+ quote = null;
126
+ } else {
127
+ cur += ch;
128
+ }
129
+ continue;
130
+ }
131
+ if (ch === '"' || ch === "'") {
132
+ quote = ch;
133
+ continue;
134
+ }
135
+ if (/\s/.test(ch)) {
136
+ if (cur) {
137
+ tokens.push(cur);
138
+ cur = "";
139
+ }
140
+ continue;
141
+ }
142
+ cur += ch;
143
+ }
144
+ if (cur) tokens.push(cur);
145
+ return tokens;
146
+ }
147
+
148
+ function applyBool(opts, flag) {
149
+ switch (flag) {
150
+ case "--no-claude-native":
151
+ opts.noClaudeNative = true;
152
+ break;
153
+ }
154
+ }
155
+
156
+ function applyValue(opts, flag, value, warnings) {
157
+ switch (flag) {
158
+ case "--cli":
159
+ opts.cli = value;
160
+ break;
161
+ case "--mode":
162
+ opts.mode = value;
163
+ break;
164
+ case "--parallel":
165
+ opts.parallel = value;
166
+ break;
167
+ case "--retry":
168
+ opts.retry = value;
169
+ break;
170
+ case "--isolation":
171
+ opts.isolation = value;
172
+ break;
173
+ case "--remote":
174
+ opts.remote = value;
175
+ break;
176
+ case "--lead":
177
+ opts.lead = value;
178
+ break;
179
+ case "--max-iterations": {
180
+ const n = Number.parseInt(value, 10);
181
+ if (Number.isNaN(n) || n < 0) {
182
+ warnings.push(
183
+ `invalid --max-iterations=${value}, expected non-negative integer (0=unlimited)`,
184
+ );
185
+ } else {
186
+ opts.maxIterations = n;
187
+ }
188
+ break;
189
+ }
190
+ }
191
+ }
192
+
193
+ function validate(opts, warnings) {
194
+ for (const key of Object.keys(VALID_VALUES)) {
195
+ if (!VALID_VALUES[key].includes(opts[key])) {
196
+ warnings.push(
197
+ `invalid --${key}=${opts[key]}, expected one of ${VALID_VALUES[key].join("|")}`,
198
+ );
199
+ }
200
+ }
201
+ if (
202
+ opts.parallel !== "1" &&
203
+ opts.parallel !== "swarm" &&
204
+ !/^\d+$/.test(opts.parallel)
205
+ ) {
206
+ warnings.push(`invalid --parallel=${opts.parallel}, expected 1|N|swarm`);
207
+ }
208
+ const parallelOne = opts.parallel === "1" || opts.parallel === 1;
209
+ if (parallelOne && opts.isolation === "worktree") {
210
+ warnings.push(
211
+ "--isolation worktree requires --parallel >=2 or swarm; forcing isolation=none",
212
+ );
213
+ opts.isolation = "none";
214
+ }
215
+ if (opts.remote !== "none" && opts.parallel !== "swarm") {
216
+ warnings.push(
217
+ `--remote ${opts.remote} ignored (requires --parallel swarm)`,
218
+ );
219
+ }
220
+ }
221
+
222
+ export { VALID_VALUES };
package/hub/server.mjs CHANGED
@@ -1057,7 +1057,7 @@ export async function startHub({
1057
1057
  if (path === "/synapse/heartbeat" && req.method === "POST") {
1058
1058
  try {
1059
1059
  const body = await parseBody(req);
1060
- const { sessionId, ...partial } = body || {};
1060
+ const { sessionId, partial } = body || {};
1061
1061
  const ok = synapseRegistry.heartbeat(sessionId, partial);
1062
1062
  if (!ok) {
1063
1063
  throw new Error("heartbeat failed");
@@ -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