triflux 7.1.4 → 7.2.2

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
@@ -1,28 +1,29 @@
1
- export const TEAM_COMMANDS = [
2
- { name: "status", usage: "tfx multi status", desc: "현재 팀 상태" },
3
- { name: "debug", usage: "tfx multi debug [--lines 30]", desc: "강화 디버그 출력" },
4
- { name: "tasks", usage: "tfx multi tasks", desc: "공유 태스크 목록" },
5
- { name: "task", usage: "tfx multi task <pending|progress|done> <T1>", desc: "태스크 상태 갱신" },
6
- { name: "attach", usage: "tfx multi attach [--wt]", desc: "세션 재연결" },
7
- { name: "focus", usage: "tfx multi focus <lead|이름|번호> [--wt]", desc: "특정 팀메이트 포커스" },
8
- { name: "send", usage: "tfx multi send <lead|이름|번호> \"msg\"", desc: "팀메이트에 메시지 주입" },
9
- { name: "interrupt", usage: "tfx multi interrupt <대상>", desc: "팀메이트 인터럽트(C-c)" },
10
- { name: "control", usage: "tfx multi control <대상> <cmd> [사유]", desc: "리드 제어명령 전송" },
11
- { name: "stop", usage: "tfx multi stop", desc: "graceful 종료" },
12
- { name: "kill", usage: "tfx multi kill", desc: "모든 팀 세션 강제 종료" },
13
- { name: "list", usage: "tfx multi list", desc: "활성 세션 목록" },
14
- { name: "help", usage: "tfx multi help", desc: "도움말" },
15
- ];
16
-
17
- export const TEAM_COMMAND_ALIASES = new Map([
18
- ["-h", "help"],
19
- ["--help", "help"],
20
- ]);
21
-
22
- export const TEAM_SUBCOMMANDS = new Set(TEAM_COMMANDS.map(({ name }) => name));
23
-
24
- export function resolveTeamCommand(raw) {
25
- if (typeof raw !== "string") return null;
26
- const command = raw.toLowerCase();
27
- return TEAM_SUBCOMMANDS.has(command) ? command : (TEAM_COMMAND_ALIASES.get(command) || null);
28
- }
1
+ export const TEAM_COMMANDS = [
2
+ { name: "status", usage: "tfx multi status", desc: "현재 팀 상태" },
3
+ { name: "debug", usage: "tfx multi debug [--lines 30]", desc: "강화 디버그 출력" },
4
+ { name: "tasks", usage: "tfx multi tasks", desc: "공유 태스크 목록" },
5
+ { name: "task", usage: "tfx multi task <pending|progress|done> <T1>", desc: "태스크 상태 갱신" },
6
+ { name: "attach", usage: "tfx multi attach [--wt]", desc: "세션 재연결" },
7
+ { name: "focus", usage: "tfx multi focus <lead|이름|번호> [--wt]", desc: "특정 팀메이트 포커스" },
8
+ { name: "send", usage: "tfx multi send <lead|이름|번호> \"msg\"", desc: "팀메이트에 메시지 주입" },
9
+ { name: "interrupt", usage: "tfx multi interrupt <대상>", desc: "팀메이트 인터럽트(C-c)" },
10
+ { name: "control", usage: "tfx multi control <대상> <cmd> [사유]", desc: "리드 제어명령 전송" },
11
+ { name: "stop", usage: "tfx multi stop", desc: "graceful 종료" },
12
+ { name: "kill", usage: "tfx multi kill", desc: "모든 팀 세션 강제 종료" },
13
+ { name: "list", usage: "tfx multi list", desc: "활성 세션 목록" },
14
+ { name: "help", usage: "tfx multi help", desc: "도움말" },
15
+ { name: "start", usage: "tfx multi start [options]", desc: "새 팀 세션 시작 (기본 커맨드)" },
16
+ ];
17
+
18
+ export const TEAM_COMMAND_ALIASES = new Map([
19
+ ["-h", "help"],
20
+ ["--help", "help"],
21
+ ]);
22
+
23
+ export const TEAM_SUBCOMMANDS = new Set(TEAM_COMMANDS.map(({ name }) => name));
24
+
25
+ export function resolveTeamCommand(raw) {
26
+ if (typeof raw !== "string") return null;
27
+ const command = raw.toLowerCase();
28
+ return TEAM_SUBCOMMANDS.has(command) ? command : (TEAM_COMMAND_ALIASES.get(command) || null);
29
+ }
@@ -126,6 +126,43 @@ export async function startHubDaemon() {
126
126
  return null;
127
127
  }
128
128
 
129
+ /**
130
+ * Hub가 살아있는지 확인하고, 죽어있으면 재시작을 시도한다.
131
+ * exponential backoff: 1초, 2초, 4초
132
+ * 모든 재시작 실패 시 에러를 throw한다 (silent fail 아님).
133
+ * @param {number} [maxRetries=3]
134
+ * @returns {Promise<object>} Hub 정보
135
+ * @throws {Error} 모든 재시작 시도 실패 시
136
+ */
137
+ export async function ensureHubAlive(maxRetries = 3) {
138
+ const hub = await getHubInfo();
139
+ if (hub && !hub.degraded) return hub;
140
+
141
+ let lastError = null;
142
+ for (let i = 0; i < maxRetries; i++) {
143
+ try {
144
+ const restarted = await startHubDaemon();
145
+ if (restarted) {
146
+ // 재시작 후 연결 복구 확인
147
+ const recovered = await getHubInfo();
148
+ if (recovered) return recovered;
149
+ }
150
+ } catch (err) {
151
+ lastError = err;
152
+ }
153
+ // 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
154
+ if (i < maxRetries - 1) {
155
+ const backoffMs = Math.pow(2, i) * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
156
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
157
+ }
158
+ }
159
+
160
+ const error = new Error(`Hub 재시작 ${maxRetries}회 모두 실패${lastError ? `: ${lastError.message}` : ""}`);
161
+ error.code = "HUB_RESTART_FAILED";
162
+ error.cause = lastError;
163
+ throw error;
164
+ }
165
+
129
166
  export async function fetchHubTaskList(state) {
130
167
  const hubBase = (state?.hubUrl || getDefaultHubUrl()).replace(/\/mcp$/, "");
131
168
  const teamName = state?.native?.teamName || state?.sessionName || null;
@@ -1,4 +1,4 @@
1
- import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -11,24 +11,38 @@ export const TEAM_PROFILE = (() => {
11
11
  return raw === "codex-team" ? "codex-team" : "team";
12
12
  })();
13
13
 
14
- const TEAM_STATE_FILE = join(
15
- HUB_PID_DIR,
16
- TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json",
17
- );
14
+ export const SESSION_ID = process.env.CLAUDE_SESSION_ID || `s${Date.now()}`;
18
15
 
19
- export function loadTeamState() {
16
+ function getStatePath(sessionId) {
17
+ if (sessionId) return join(HUB_PID_DIR, `team-state-${sessionId}.json`);
18
+ return join(HUB_PID_DIR, TEAM_PROFILE === "codex-team" ? "team-state-codex-team.json" : "team-state.json");
19
+ }
20
+
21
+ export function loadTeamState(sessionId) {
22
+ const resolvedId = sessionId || SESSION_ID;
23
+ const sessionPath = getStatePath(resolvedId);
24
+ try {
25
+ if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
26
+ } catch {
27
+ return null;
28
+ }
29
+ // 세션별 파일 없으면 기존 team-state.json fallback
30
+ const legacyPath = getStatePath(null);
20
31
  try {
21
- return JSON.parse(readFileSync(TEAM_STATE_FILE, "utf8"));
32
+ if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
22
33
  } catch {
23
34
  return null;
24
35
  }
36
+ return null;
25
37
  }
26
38
 
27
- export function saveTeamState(state) {
28
- mkdirSync(dirname(TEAM_STATE_FILE), { recursive: true });
29
- writeFileSync(TEAM_STATE_FILE, JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n");
39
+ export function saveTeamState(state, sessionId) {
40
+ const path = getStatePath(sessionId || state.sessionId || SESSION_ID);
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ writeFileSync(path, JSON.stringify({ ...state, profile: TEAM_PROFILE }, null, 2) + "\n");
30
43
  }
31
44
 
32
- export function clearTeamState() {
33
- try { unlinkSync(TEAM_STATE_FILE); } catch {}
45
+ export function clearTeamState(sessionId) {
46
+ const path = getStatePath(sessionId || SESSION_ID);
47
+ if (existsSync(path)) unlinkSync(path);
34
48
  }
@@ -218,14 +218,21 @@ export async function renderDashboard(sessionName, opts = {}) {
218
218
  console.log(`${AMBER}└${GRAY}${border}${AMBER}┘${RESET}`);
219
219
  }
220
220
 
221
- /** team-state.json 로드 */
221
+ /** team-state.json 로드 (세션별 파일 우선, fallback: team-state.json) */
222
222
  async function loadTeamState() {
223
223
  try {
224
- const { readFileSync } = await import("node:fs");
224
+ const { existsSync, readFileSync } = await import("node:fs");
225
225
  const { join } = await import("node:path");
226
226
  const { homedir } = await import("node:os");
227
- const statePath = join(homedir(), ".claude", "cache", "tfx-hub", "team-state.json");
228
- return JSON.parse(readFileSync(statePath, "utf8"));
227
+ const hubDir = join(homedir(), ".claude", "cache", "tfx-hub");
228
+ const sessionId = process.env.CLAUDE_SESSION_ID;
229
+ if (sessionId) {
230
+ const sessionPath = join(hubDir, `team-state-${sessionId}.json`);
231
+ if (existsSync(sessionPath)) return JSON.parse(readFileSync(sessionPath, "utf8"));
232
+ }
233
+ const legacyPath = join(hubDir, "team-state.json");
234
+ if (existsSync(legacyPath)) return JSON.parse(readFileSync(legacyPath, "utf8"));
235
+ return {};
229
236
  } catch {
230
237
  return {};
231
238
  }
@@ -41,6 +41,18 @@ Rules:
41
41
  - Do not skip any required field
42
42
  `.trim();
43
43
 
44
+ /**
45
+ * CLI 프롬프트 길이 제한을 고려한 축약 HANDOFF 지시
46
+ */
47
+ export const HANDOFF_INSTRUCTION_SHORT =
48
+ `After completing, output this block at the end:
49
+ --- HANDOFF ---
50
+ status: ok | partial | failed
51
+ lead_action: accept | needs_read | retry | reassign
52
+ verdict: <one sentence>
53
+ files_changed: <comma-separated paths or "none">
54
+ confidence: high | medium | low`;
55
+
44
56
  /**
45
57
  * raw 텍스트에서 HANDOFF 블록을 파싱한다.
46
58
  * @param {string} rawText