triflux 10.13.6 → 10.13.8
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.
- package/bin/triflux.mjs +22 -1
- package/hub/team/swarm-cli.mjs +56 -0
- package/hud/context-monitor.mjs +7 -6
- package/package.json +1 -1
- package/scripts/release/prepare.mjs +13 -1
package/bin/triflux.mjs
CHANGED
|
@@ -4760,6 +4760,7 @@ ${updateNotice}
|
|
|
4760
4760
|
${WHITE_BRIGHT}tfx tray${RESET} ${GRAY}Windows 시스템 트레이 실행${RESET}
|
|
4761
4761
|
${DIM} --detach${RESET} ${GRAY}백그라운드 트레이 프로세스로 분리${RESET}
|
|
4762
4762
|
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
4763
|
+
${WHITE_BRIGHT}tfx swarm${RESET} ${GRAY}PRD 기반 worktree 격리 병렬 실행${RESET}
|
|
4763
4764
|
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
4764
4765
|
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
4765
4766
|
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
@@ -5816,11 +5817,31 @@ async function main() {
|
|
|
5816
5817
|
return;
|
|
5817
5818
|
}
|
|
5818
5819
|
case "swarm": {
|
|
5820
|
+
const sub = cmdArgs[0] || "";
|
|
5821
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
5822
|
+
const s = CLI_COMMAND_SCHEMAS.swarm;
|
|
5823
|
+
console.log(`
|
|
5824
|
+
${AMBER}${BOLD}⬡ tfx swarm${RESET}
|
|
5825
|
+
|
|
5826
|
+
${GRAY}${s.description}${RESET}
|
|
5827
|
+
|
|
5828
|
+
${BOLD}Usage${RESET}
|
|
5829
|
+
${WHITE_BRIGHT}${s.usage}${RESET}
|
|
5830
|
+
|
|
5831
|
+
${BOLD}Subcommands${RESET}
|
|
5832
|
+
${WHITE_BRIGHT}tfx swarm plan <prd>${RESET} ${GRAY}${s.subcommands.plan}${RESET}
|
|
5833
|
+
${WHITE_BRIGHT}tfx swarm list${RESET} ${GRAY}${s.subcommands.list}${RESET}
|
|
5834
|
+
${WHITE_BRIGHT}tfx swarm status${RESET} ${GRAY}${s.subcommands.status}${RESET}
|
|
5835
|
+
|
|
5836
|
+
${BOLD}Options${RESET}
|
|
5837
|
+
${s.options.map((o) => ` ${DIM}${o.name.padEnd(16)}${RESET} ${GRAY}${o.description}${RESET}`).join("\n")}
|
|
5838
|
+
`);
|
|
5839
|
+
return;
|
|
5840
|
+
}
|
|
5819
5841
|
await checkHubRunning();
|
|
5820
5842
|
const { cmdSwarmRun, cmdSwarmPlan, cmdSwarmList } = await import(
|
|
5821
5843
|
"../hub/team/swarm-cli.mjs"
|
|
5822
5844
|
);
|
|
5823
|
-
const sub = cmdArgs[0] || "";
|
|
5824
5845
|
if (sub === "list" || sub === "status") {
|
|
5825
5846
|
await cmdSwarmList(cmdArgs.slice(1), { json: JSON_OUTPUT });
|
|
5826
5847
|
return;
|
package/hub/team/swarm-cli.mjs
CHANGED
|
@@ -13,6 +13,54 @@ const RED = "\u001b[91m";
|
|
|
13
13
|
const YELLOW = "\u001b[93m";
|
|
14
14
|
const GRAY = "\u001b[90m";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* #116-C: non-TTY background 환경에서 `tfx swarm` 실행은 codex worker spawn 이
|
|
18
|
+
* 무한 hang 한다 (stdin TTY 대기 또는 hub MCP lease race).
|
|
19
|
+
*
|
|
20
|
+
* - stdout/stdin 모두 non-TTY 이면 fail-fast — 명시 복구 경로 안내.
|
|
21
|
+
* - `TFX_ALLOW_NON_TTY_SWARM=1` opt-in 시 경고만 남기고 통과 (테스트/CI).
|
|
22
|
+
* - pure function — 테스트하기 쉽게 deps 주입 가능.
|
|
23
|
+
*
|
|
24
|
+
* @param {{
|
|
25
|
+
* stdoutIsTTY?: boolean,
|
|
26
|
+
* stdinIsTTY?: boolean,
|
|
27
|
+
* env?: Record<string,string|undefined>,
|
|
28
|
+
* }} [deps]
|
|
29
|
+
* @returns {{ ok: boolean, optIn: boolean, warnings: string[], reason?: string }}
|
|
30
|
+
*/
|
|
31
|
+
export function assertTtyForSwarm(deps = {}) {
|
|
32
|
+
const stdoutIsTTY =
|
|
33
|
+
typeof deps.stdoutIsTTY === "boolean"
|
|
34
|
+
? deps.stdoutIsTTY
|
|
35
|
+
: Boolean(process.stdout.isTTY);
|
|
36
|
+
const stdinIsTTY =
|
|
37
|
+
typeof deps.stdinIsTTY === "boolean"
|
|
38
|
+
? deps.stdinIsTTY
|
|
39
|
+
: Boolean(process.stdin.isTTY);
|
|
40
|
+
const env = deps.env || process.env;
|
|
41
|
+
const warnings = [];
|
|
42
|
+
|
|
43
|
+
if (stdoutIsTTY || stdinIsTTY) {
|
|
44
|
+
return { ok: true, optIn: false, warnings };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (env.TFX_ALLOW_NON_TTY_SWARM === "1") {
|
|
48
|
+
warnings.push(
|
|
49
|
+
"non-TTY 환경 감지 — TFX_ALLOW_NON_TTY_SWARM=1 opt-in 으로 진행합니다. codex worker spawn hang 가능성 존재 (#116-C).",
|
|
50
|
+
);
|
|
51
|
+
return { ok: true, optIn: true, warnings };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const reason =
|
|
55
|
+
"tfx swarm 은 TTY 가 필요합니다 — non-TTY 환경 (run_in_background, nohup 등) 에서 codex worker spawn 이 hang 합니다 (#116-C).\n" +
|
|
56
|
+
" 복구 경로:\n" +
|
|
57
|
+
" 1) 터미널에서 직접 실행: tfx swarm <prd>\n" +
|
|
58
|
+
" 2) tmux 경로: tfx multi --teammate-mode tmux --auto-attach --dashboard --assign ...\n" +
|
|
59
|
+
" 3) opt-in (위험): TFX_ALLOW_NON_TTY_SWARM=1 tfx swarm <prd>";
|
|
60
|
+
|
|
61
|
+
return { ok: false, optIn: false, warnings, reason };
|
|
62
|
+
}
|
|
63
|
+
|
|
16
64
|
export function parseFlags(args) {
|
|
17
65
|
const flags = {
|
|
18
66
|
dryRun: false,
|
|
@@ -121,6 +169,14 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
121
169
|
return;
|
|
122
170
|
}
|
|
123
171
|
|
|
172
|
+
const ttyGate = assertTtyForSwarm();
|
|
173
|
+
for (const w of ttyGate.warnings) {
|
|
174
|
+
console.error(` ${YELLOW}⚠${RESET} ${w}`);
|
|
175
|
+
}
|
|
176
|
+
if (!ttyGate.ok) {
|
|
177
|
+
throw new Error(ttyGate.reason);
|
|
178
|
+
}
|
|
179
|
+
|
|
124
180
|
const logsDir =
|
|
125
181
|
flags.logsDir ||
|
|
126
182
|
join(process.cwd(), ".triflux", "swarm-logs", `run-${Date.now()}`);
|
package/hud/context-monitor.mjs
CHANGED
|
@@ -282,12 +282,13 @@ export function buildContextUsageView(stdin, snapshot = null) {
|
|
|
282
282
|
const modelId = stdin?.model?.id ?? stdin?.model;
|
|
283
283
|
const modelHintLimit = resolveModelLimit(modelId);
|
|
284
284
|
const monitorLimit = Number(monitor?.limitTokens || 0);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
const stdinLimit = stdinUsage?.limitTokens;
|
|
286
|
+
const limitTokens =
|
|
287
|
+
stdinLimit != null && stdinLimit > 0
|
|
288
|
+
? Math.max(1, stdinLimit)
|
|
289
|
+
: modelId
|
|
290
|
+
? Math.max(1, monitorLimit, modelHintLimit)
|
|
291
|
+
: Math.max(1, monitorLimit || modelHintLimit);
|
|
291
292
|
|
|
292
293
|
const usedTokens = stdinUsage?.usedTokens ?? Number(monitor?.usedTokens || 0);
|
|
293
294
|
const percent =
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import {
|
|
@@ -13,6 +13,17 @@ import {
|
|
|
13
13
|
} from "./lib.mjs";
|
|
14
14
|
|
|
15
15
|
const TEST_TIMEOUT_MS = 10 * 60 * 1000;
|
|
16
|
+
const STALE_LOCK = join(ROOT, ".test-lock", "pid.lock");
|
|
17
|
+
|
|
18
|
+
export function cleanupStaleTestLock() {
|
|
19
|
+
if (!existsSync(STALE_LOCK)) return;
|
|
20
|
+
try {
|
|
21
|
+
rmSync(STALE_LOCK, { force: true });
|
|
22
|
+
console.log("[prepare] cleaned stale .test-lock/pid.lock");
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn(`[prepare] failed to clean test-lock: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
function createStepLogger() {
|
|
18
29
|
const startedAt = Date.now();
|
|
@@ -30,6 +41,7 @@ export async function prepareRelease({
|
|
|
30
41
|
skipTests = false,
|
|
31
42
|
execFileSyncFn,
|
|
32
43
|
} = {}) {
|
|
44
|
+
cleanupStaleTestLock();
|
|
33
45
|
const logStep = createStepLogger();
|
|
34
46
|
logStep("version-sync");
|
|
35
47
|
const sync = assertVersionSync({ rootDir });
|