triflux 3.3.0-dev.7 → 4.0.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 (91) hide show
  1. package/README.ko.md +108 -199
  2. package/README.md +108 -199
  3. package/bin/triflux.mjs +2415 -1762
  4. package/hooks/keyword-rules.json +361 -354
  5. package/hooks/pipeline-stop.mjs +5 -2
  6. package/hub/assign-callbacks.mjs +136 -136
  7. package/hub/bridge.mjs +734 -708
  8. package/hub/delegator/contracts.mjs +38 -0
  9. package/hub/delegator/index.mjs +14 -0
  10. package/hub/delegator/schema/delegator-tools.schema.json +250 -0
  11. package/hub/delegator/service.mjs +302 -0
  12. package/hub/delegator/tool-definitions.mjs +35 -0
  13. package/hub/hitl.mjs +67 -67
  14. package/hub/paths.mjs +28 -0
  15. package/hub/pipe.mjs +589 -561
  16. package/hub/pipeline/state.mjs +23 -0
  17. package/hub/public/dashboard.html +349 -0
  18. package/hub/public/tray-icon.ico +0 -0
  19. package/hub/public/tray-icon.png +0 -0
  20. package/hub/router.mjs +782 -782
  21. package/hub/schema.sql +40 -40
  22. package/hub/server.mjs +810 -637
  23. package/hub/store.mjs +706 -706
  24. package/hub/team/cli/commands/attach.mjs +37 -0
  25. package/hub/team/cli/commands/control.mjs +43 -0
  26. package/hub/team/cli/commands/debug.mjs +74 -0
  27. package/hub/team/cli/commands/focus.mjs +53 -0
  28. package/hub/team/cli/commands/interrupt.mjs +36 -0
  29. package/hub/team/cli/commands/kill.mjs +37 -0
  30. package/hub/team/cli/commands/list.mjs +24 -0
  31. package/hub/team/cli/commands/send.mjs +37 -0
  32. package/hub/team/cli/commands/start/index.mjs +87 -0
  33. package/hub/team/cli/commands/start/parse-args.mjs +32 -0
  34. package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
  35. package/hub/team/cli/commands/start/start-mux.mjs +73 -0
  36. package/hub/team/cli/commands/start/start-wt.mjs +69 -0
  37. package/hub/team/cli/commands/status.mjs +87 -0
  38. package/hub/team/cli/commands/stop.mjs +31 -0
  39. package/hub/team/cli/commands/task.mjs +30 -0
  40. package/hub/team/cli/commands/tasks.mjs +13 -0
  41. package/hub/team/{cli.mjs → cli/help.mjs} +38 -99
  42. package/hub/team/cli/index.mjs +39 -0
  43. package/hub/team/cli/manifest.mjs +28 -0
  44. package/hub/team/cli/render.mjs +30 -0
  45. package/hub/team/cli/services/attach-fallback.mjs +54 -0
  46. package/hub/team/cli/services/hub-client.mjs +171 -0
  47. package/hub/team/cli/services/member-selector.mjs +30 -0
  48. package/hub/team/cli/services/native-control.mjs +115 -0
  49. package/hub/team/cli/services/runtime-mode.mjs +60 -0
  50. package/hub/team/cli/services/state-store.mjs +34 -0
  51. package/hub/team/cli/services/task-model.mjs +30 -0
  52. package/hub/team/native-supervisor.mjs +69 -63
  53. package/hub/team/native.mjs +367 -266
  54. package/hub/team/nativeProxy.mjs +217 -173
  55. package/hub/team/pane.mjs +149 -149
  56. package/hub/team/psmux.mjs +946 -946
  57. package/hub/team/session.mjs +608 -608
  58. package/hub/team/staleState.mjs +369 -299
  59. package/hub/tools.mjs +107 -107
  60. package/hub/tray.mjs +332 -0
  61. package/hub/workers/claude-worker.mjs +446 -446
  62. package/hub/workers/codex-mcp.mjs +414 -414
  63. package/hub/workers/delegator-mcp.mjs +1045 -1045
  64. package/hub/workers/factory.mjs +21 -21
  65. package/hub/workers/gemini-worker.mjs +349 -349
  66. package/hub/workers/interface.mjs +41 -41
  67. package/package.json +61 -60
  68. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  69. package/scripts/hub-ensure.mjs +102 -101
  70. package/scripts/keyword-detector.mjs +272 -272
  71. package/scripts/keyword-rules-expander.mjs +521 -521
  72. package/scripts/lib/keyword-rules.mjs +168 -168
  73. package/scripts/lib/mcp-filter.mjs +642 -642
  74. package/scripts/lib/mcp-server-catalog.mjs +118 -118
  75. package/scripts/mcp-check.mjs +126 -126
  76. package/scripts/preflight-cache.mjs +19 -0
  77. package/scripts/run.cjs +62 -62
  78. package/scripts/setup.mjs +68 -31
  79. package/scripts/test-tfx-route-no-claude-native.mjs +57 -57
  80. package/scripts/tfx-route-worker.mjs +161 -161
  81. package/scripts/tfx-route.sh +1360 -1326
  82. package/skills/tfx-auto/SKILL.md +196 -196
  83. package/skills/tfx-auto-codex/SKILL.md +77 -77
  84. package/skills/tfx-multi/SKILL.md +378 -378
  85. package/hub/team/cli-team-common.mjs +0 -348
  86. package/hub/team/cli-team-control.mjs +0 -393
  87. package/hub/team/cli-team-start.mjs +0 -516
  88. package/hub/team/cli-team-status.mjs +0 -283
  89. package/skills/auto-verify/SKILL.md +0 -145
  90. package/skills/manage-skills/SKILL.md +0 -192
  91. package/skills/verify-implementation/SKILL.md +0 -138
@@ -0,0 +1,115 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { spawn } from "node:child_process";
4
+
5
+ import { buildLeadPrompt, buildPrompt } from "../../orchestrator.mjs";
6
+ import { HUB_PID_DIR, PKG_ROOT } from "./state-store.mjs";
7
+
8
+ export function buildNativeCliCommand(cli) {
9
+ switch (cli) {
10
+ case "codex":
11
+ return "codex --dangerously-bypass-approvals-and-sandbox --no-alt-screen";
12
+ case "gemini":
13
+ return "gemini";
14
+ case "claude":
15
+ return "claude";
16
+ default:
17
+ return cli;
18
+ }
19
+ }
20
+
21
+ export async function startNativeSupervisor({ sessionId, task, lead, agents, subtasks, hubUrl }) {
22
+ const configPath = join(HUB_PID_DIR, `team-native-${sessionId}.config.json`);
23
+ const runtimePath = join(HUB_PID_DIR, `team-native-${sessionId}.runtime.json`);
24
+ const logsDir = join(HUB_PID_DIR, "team-logs", sessionId);
25
+ mkdirSync(logsDir, { recursive: true });
26
+
27
+ const leadMember = {
28
+ role: "lead",
29
+ name: "lead",
30
+ cli: lead,
31
+ agentId: `${lead}-lead`,
32
+ command: buildNativeCliCommand(lead),
33
+ };
34
+ const workers = agents.map((cli, index) => ({
35
+ role: "worker",
36
+ name: `${cli}-${index + 1}`,
37
+ cli,
38
+ agentId: `${cli}-w${index + 1}`,
39
+ command: buildNativeCliCommand(cli),
40
+ subtask: subtasks[index],
41
+ }));
42
+ const members = [
43
+ {
44
+ ...leadMember,
45
+ prompt: buildLeadPrompt(task, {
46
+ agentId: leadMember.agentId,
47
+ hubUrl,
48
+ teammateMode: "in-process",
49
+ workers: workers.map((worker) => ({
50
+ agentId: worker.agentId,
51
+ cli: worker.cli,
52
+ subtask: worker.subtask,
53
+ })),
54
+ }),
55
+ },
56
+ ...workers.map((worker) => ({
57
+ ...worker,
58
+ prompt: buildPrompt(worker.subtask, { cli: worker.cli, agentId: worker.agentId, hubUrl }),
59
+ })),
60
+ ];
61
+
62
+ writeFileSync(configPath, JSON.stringify({
63
+ sessionName: sessionId,
64
+ hubUrl,
65
+ startupDelayMs: 3000,
66
+ logsDir,
67
+ runtimeFile: runtimePath,
68
+ members,
69
+ }, null, 2) + "\n");
70
+
71
+ const child = spawn(process.execPath, [join(PKG_ROOT, "hub", "team", "native-supervisor.mjs"), "--config", configPath], {
72
+ detached: true,
73
+ stdio: "ignore",
74
+ env: { ...process.env },
75
+ windowsHide: true,
76
+ });
77
+ child.unref();
78
+
79
+ const deadline = Date.now() + 5000;
80
+ while (Date.now() < deadline) {
81
+ if (existsSync(runtimePath)) {
82
+ try {
83
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf8"));
84
+ return { runtime, members };
85
+ } catch {}
86
+ }
87
+ await new Promise((resolve) => setTimeout(resolve, 100));
88
+ }
89
+
90
+ return { runtime: null, members };
91
+ }
92
+
93
+ export async function nativeRequest(state, path, body = {}) {
94
+ if (!state?.native?.controlUrl) return null;
95
+ try {
96
+ const res = await fetch(`${state.native.controlUrl}${path}`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify(body),
100
+ });
101
+ return await res.json();
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ export async function nativeGetStatus(state) {
108
+ if (!state?.native?.controlUrl) return null;
109
+ try {
110
+ const res = await fetch(`${state.native.controlUrl}/status`);
111
+ return await res.json();
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ detectMultiplexer,
3
+ hasWindowsTerminal,
4
+ hasWindowsTerminalSession,
5
+ sessionExists,
6
+ } from "../../session.mjs";
7
+
8
+ export function normalizeTeammateMode(mode = "auto") {
9
+ const raw = String(mode).toLowerCase();
10
+ if (raw === "inline" || raw === "native") return "in-process";
11
+ if (raw === "in-process" || raw === "tmux" || raw === "wt" || raw === "psmux") return raw;
12
+ if (raw === "windows-terminal" || raw === "windows_terminal") return "wt";
13
+ if (raw === "auto") {
14
+ if (process.env.TMUX) return "tmux";
15
+ return detectMultiplexer() === "psmux" ? "psmux" : "in-process";
16
+ }
17
+ return "in-process";
18
+ }
19
+
20
+ export function normalizeLayout(layout = "2x2") {
21
+ const raw = String(layout).toLowerCase();
22
+ if (raw === "2x2" || raw === "grid") return "2x2";
23
+ if (raw === "1xn" || raw === "1x3" || raw === "vertical" || raw === "columns") return "1xN";
24
+ if (raw === "nx1" || raw === "horizontal" || raw === "rows") return "Nx1";
25
+ return "2x2";
26
+ }
27
+
28
+ export function isNativeMode(state) {
29
+ return state?.teammateMode === "in-process" && !!state?.native?.controlUrl;
30
+ }
31
+
32
+ export function isWtMode(state) {
33
+ return state?.teammateMode === "wt";
34
+ }
35
+
36
+ export function isTeamAlive(state) {
37
+ if (!state) return false;
38
+ if (isNativeMode(state)) {
39
+ try {
40
+ process.kill(state.native.supervisorPid, 0);
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ if (isWtMode(state)) {
47
+ if (!hasWindowsTerminal()) return false;
48
+ if (hasWindowsTerminalSession()) return true;
49
+ return Array.isArray(state.members) && state.members.length > 0;
50
+ }
51
+ return sessionExists(state.sessionName);
52
+ }
53
+
54
+ export function ensureTmuxOrExit() {
55
+ const mux = detectMultiplexer();
56
+ if (mux) return mux;
57
+ const error = new Error("tmux 미발견");
58
+ error.code = "TMUX_REQUIRED";
59
+ throw error;
60
+ }
@@ -0,0 +1,34 @@
1
+ import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const PKG_ROOT = fileURLToPath(new URL("../../../../", import.meta.url));
8
+ export const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
9
+ export const TEAM_PROFILE = (() => {
10
+ const raw = String(process.env.TFX_TEAM_PROFILE || "team").trim().toLowerCase();
11
+ return raw === "codex-team" ? "codex-team" : "team";
12
+ })();
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
+ );
18
+
19
+ export function loadTeamState() {
20
+ try {
21
+ return JSON.parse(readFileSync(TEAM_STATE_FILE, "utf8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
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");
30
+ }
31
+
32
+ export function clearTeamState() {
33
+ try { unlinkSync(TEAM_STATE_FILE); } catch {}
34
+ }
@@ -0,0 +1,30 @@
1
+ export function buildTasks(subtasks, workers) {
2
+ return subtasks.map((subtask, index) => ({
3
+ id: `T${index + 1}`,
4
+ title: subtask,
5
+ owner: workers[index]?.name || null,
6
+ status: "pending",
7
+ depends_on: index === 0 ? [] : [`T${index}`],
8
+ }));
9
+ }
10
+
11
+ export function normalizeTaskStatus(action) {
12
+ const value = String(action || "").toLowerCase();
13
+ if (value === "done" || value === "complete" || value === "completed") return "completed";
14
+ if (value === "progress" || value === "in-progress" || value === "in_progress") return "in_progress";
15
+ if (value === "pending") return "pending";
16
+ return null;
17
+ }
18
+
19
+ export function updateTaskStatus(tasks = [], taskId, nextStatus) {
20
+ const normalizedId = String(taskId || "").toUpperCase();
21
+ const target = tasks.find((task) => String(task.id).toUpperCase() === normalizedId);
22
+ if (!target) return { tasks, target: null };
23
+
24
+ return {
25
+ target: { ...target, status: nextStatus },
26
+ tasks: tasks.map((task) => (
27
+ String(task.id).toUpperCase() === normalizedId ? { ...task, status: nextStatus } : task
28
+ )),
29
+ };
30
+ }
@@ -1,11 +1,12 @@
1
1
  // hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
2
- import { createServer } from "node:http";
3
- import { spawn } from "node:child_process";
4
- import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
- import { dirname, join } from "node:path";
6
- import { verifySlimWrapperRouteExecution } from "./native.mjs";
7
-
8
- const ROUTE_LOG_TAIL_BYTES = 65536;
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { verifySlimWrapperRouteExecution } from "./native.mjs";
7
+ import { forceCleanupTeam } from "./nativeProxy.mjs";
8
+
9
+ const ROUTE_LOG_TAIL_BYTES = 65536;
9
10
 
10
11
  function parseArgs(argv) {
11
12
  const out = {};
@@ -22,43 +23,43 @@ async function readJson(path) {
22
23
  return JSON.parse(readFileSync(path, "utf8"));
23
24
  }
24
25
 
25
- function safeText(v, fallback = "") {
26
- if (v == null) return fallback;
27
- return String(v);
28
- }
29
-
30
- function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
31
- try {
32
- const raw = readFileSync(path, "utf8");
33
- if (raw.length <= maxBytes) return raw;
34
- return raw.slice(-maxBytes);
35
- } catch {
36
- return "";
37
- }
38
- }
39
-
40
- function finalizeRouteVerification(state) {
41
- if (state?.member?.role !== "worker") return;
42
-
43
- const verification = verifySlimWrapperRouteExecution({
44
- promptText: safeText(state.member?.prompt),
45
- stdoutText: readTailText(state.logFile),
46
- stderrText: readTailText(state.errFile),
47
- });
48
-
49
- state.routeVerification = verification;
50
- if (!verification.expectedRouteInvocation) {
51
- state.completionStatus = "unchecked";
52
- state.completionReason = null;
53
- return;
54
- }
55
-
56
- state.completionStatus = verification.abnormal ? "abnormal" : "normal";
57
- state.completionReason = verification.reason;
58
- if (verification.abnormal) {
59
- state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
60
- }
61
- }
26
+ function safeText(v, fallback = "") {
27
+ if (v == null) return fallback;
28
+ return String(v);
29
+ }
30
+
31
+ function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
32
+ try {
33
+ const raw = readFileSync(path, "utf8");
34
+ if (raw.length <= maxBytes) return raw;
35
+ return raw.slice(-maxBytes);
36
+ } catch {
37
+ return "";
38
+ }
39
+ }
40
+
41
+ function finalizeRouteVerification(state) {
42
+ if (state?.member?.role !== "worker") return;
43
+
44
+ const verification = verifySlimWrapperRouteExecution({
45
+ promptText: safeText(state.member?.prompt),
46
+ stdoutText: readTailText(state.logFile),
47
+ stderrText: readTailText(state.errFile),
48
+ });
49
+
50
+ state.routeVerification = verification;
51
+ if (!verification.expectedRouteInvocation) {
52
+ state.completionStatus = "unchecked";
53
+ state.completionReason = null;
54
+ return;
55
+ }
56
+
57
+ state.completionStatus = verification.abnormal ? "abnormal" : "normal";
58
+ state.completionReason = verification.reason;
59
+ if (verification.abnormal) {
60
+ state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
61
+ }
62
+ }
62
63
 
63
64
  function nowMs() {
64
65
  return Date.now();
@@ -73,6 +74,7 @@ if (!args.config) {
73
74
  const config = await readJson(args.config);
74
75
  const {
75
76
  sessionName,
77
+ teamName = sessionName,
76
78
  runtimeFile,
77
79
  logsDir,
78
80
  startupDelayMs = 3000,
@@ -96,16 +98,16 @@ function memberStateSnapshot() {
96
98
  agentId: m.agentId,
97
99
  command: m.command,
98
100
  pid: state?.child?.pid || null,
99
- status: state?.status || "unknown",
100
- exitCode: state?.exitCode ?? null,
101
- lastPreview: state?.lastPreview || "",
102
- completionStatus: state?.completionStatus || null,
103
- completionReason: state?.completionReason || null,
104
- routeVerification: state?.routeVerification || null,
105
- logFile: state?.logFile || null,
106
- errFile: state?.errFile || null,
107
- });
108
- }
101
+ status: state?.status || "unknown",
102
+ exitCode: state?.exitCode ?? null,
103
+ lastPreview: state?.lastPreview || "",
104
+ completionStatus: state?.completionStatus || null,
105
+ completionReason: state?.completionReason || null,
106
+ routeVerification: state?.routeVerification || null,
107
+ logFile: state?.logFile || null,
108
+ errFile: state?.errFile || null,
109
+ });
110
+ }
109
111
  return states;
110
112
  }
111
113
 
@@ -167,14 +169,14 @@ function spawnMember(member) {
167
169
  }
168
170
  });
169
171
 
170
- child.on("exit", (code) => {
171
- state.status = "exited";
172
- state.exitCode = code;
173
- finalizeRouteVerification(state);
174
- try { outWs.end(); } catch {}
175
- try { errWs.end(); } catch {}
176
- maybeAutoShutdown();
177
- });
172
+ child.on("exit", (code) => {
173
+ state.status = "exited";
174
+ state.exitCode = code;
175
+ finalizeRouteVerification(state);
176
+ try { outWs.end(); } catch {}
177
+ try { errWs.end(); } catch {}
178
+ maybeAutoShutdown();
179
+ });
178
180
 
179
181
  processMap.set(member.name, state);
180
182
  }
@@ -224,7 +226,7 @@ function maybeAutoShutdown() {
224
226
  shutdown();
225
227
  }
226
228
 
227
- function shutdown() {
229
+ async function shutdown() {
228
230
  if (isShuttingDown) return;
229
231
  isShuttingDown = true;
230
232
 
@@ -237,6 +239,10 @@ function shutdown() {
237
239
  try { state.errWs.end(); } catch {}
238
240
  }
239
241
 
242
+ try {
243
+ await forceCleanupTeam(teamName);
244
+ } catch {}
245
+
240
246
  setTimeout(() => {
241
247
  for (const state of processMap.values()) {
242
248
  if (state.status === "running") {