triflux 9.2.4 → 9.4.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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +48 -217
- package/hub/pipe.mjs +1 -8
- package/hub/team/psmux.mjs +21 -1
- package/hub/workers/claude-worker.mjs +1 -24
- package/hub/workers/codex-mcp.mjs +0 -4
- package/hub/workers/gemini-worker.mjs +108 -28
- package/hub/workers/interface.mjs +0 -1
- package/hub/workers/worker-utils.mjs +26 -0
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn.test.mjs +17 -3
- package/scripts/cross-review-gate.mjs +11 -65
- package/scripts/cross-review-tracker.mjs +10 -51
- package/scripts/headless-guard.mjs +5 -17
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/mcp-filter.mjs +10 -1
- package/scripts/psmux-safety-guard.mjs +64 -0
- package/scripts/remote-spawn.mjs +77 -23
- package/scripts/session-spawn-helper.mjs +2 -1
- package/scripts/setup.mjs +36 -6
- package/scripts/tfx-route.sh +93 -10
- package/skills/tfx-auto/SKILL.md +4 -1
- package/skills/tfx-auto-codex/SKILL.md +3 -1
- package/skills/tfx-autopilot/SKILL.md +1 -1
- package/skills/tfx-autoresearch/SKILL.md +2 -0
- package/skills/tfx-codex/SKILL.md +3 -1
- package/skills/tfx-consensus/SKILL.md +5 -3
- package/skills/tfx-fullcycle/SKILL.md +1 -1
- package/skills/tfx-gemini/SKILL.md +3 -1
- package/skills/tfx-hooks/SKILL.md +216 -0
- package/skills/tfx-multi/SKILL.md +4 -1
- package/skills/tfx-plan/SKILL.md +1 -1
- package/skills/tfx-psmux-rules/SKILL.md +100 -13
- package/skills/tfx-ralph/SKILL.md +11 -66
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-review/SKILL.md +1 -1
- package/skills/tfx-setup/SKILL.md +17 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { __remoteSpawnTest } from "../remote-spawn.mjs";
|
|
5
|
+
|
|
6
|
+
const { startSpawnExitWatcher, watchSpawnSessionExit } = __remoteSpawnTest;
|
|
5
7
|
|
|
6
8
|
describe("remote-spawn watcher", () => {
|
|
7
9
|
it("lead pane dead 상태가 grace 기간 유지되면 세션을 정리한다", async () => {
|
|
@@ -51,7 +53,7 @@ describe("remote-spawn watcher", () => {
|
|
|
51
53
|
assert.equal(killCount, 0);
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
it("detached watcher를
|
|
56
|
+
it("detached watcher를 현재 cleanup watcher 인자로 실행한다", () => {
|
|
55
57
|
const calls = [];
|
|
56
58
|
const started = startSpawnExitWatcher("tfx-spawn-detached", {
|
|
57
59
|
force: true,
|
|
@@ -70,7 +72,19 @@ describe("remote-spawn watcher", () => {
|
|
|
70
72
|
assert.equal(started, true);
|
|
71
73
|
assert.equal(calls.length, 1);
|
|
72
74
|
assert.equal(calls[0].file, "node-test");
|
|
73
|
-
assert.deepEqual(calls[0].args, [
|
|
75
|
+
assert.deepEqual(calls[0].args, [
|
|
76
|
+
"C:/tmp/remote-spawn.mjs",
|
|
77
|
+
"--watch-cleanup",
|
|
78
|
+
"tfx-spawn-detached",
|
|
79
|
+
"--pane",
|
|
80
|
+
"tfx-spawn-detached:0.0",
|
|
81
|
+
"--poll-ms",
|
|
82
|
+
"1000",
|
|
83
|
+
"--grace-ms",
|
|
84
|
+
"1500",
|
|
85
|
+
"--max-ms",
|
|
86
|
+
"3600000",
|
|
87
|
+
]);
|
|
74
88
|
assert.equal(calls[0].options.detached, true);
|
|
75
89
|
assert.equal(calls[0].options.stdio, "ignore");
|
|
76
90
|
assert.equal(calls[0].unrefCalled, true);
|
|
@@ -2,56 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
process.stdin.on("end", () => resolve(raw));
|
|
17
|
-
process.stdin.on("error", () => resolve(""));
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function parseJson(raw) {
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(raw);
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function nowSec() {
|
|
30
|
-
return Math.floor(Date.now() / 1000);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolveBaseDir(payload) {
|
|
34
|
-
if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
|
|
35
|
-
if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
|
|
36
|
-
return process.cwd();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function expectedReviewer(author) {
|
|
40
|
-
if (author === "claude") return "codex";
|
|
41
|
-
if (author === "codex") return "claude";
|
|
42
|
-
if (author === "gemini") return "claude";
|
|
43
|
-
return "";
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function shouldTrackPath(filePath) {
|
|
47
|
-
if (typeof filePath !== "string" || !filePath.trim()) return false;
|
|
48
|
-
|
|
49
|
-
const lower = filePath.toLowerCase();
|
|
50
|
-
if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
|
|
51
|
-
if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
|
|
52
|
-
if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
5
|
+
import { nudge, deny } from "./lib/hook-utils.mjs";
|
|
6
|
+
import {
|
|
7
|
+
readStdin,
|
|
8
|
+
parseJson,
|
|
9
|
+
nowSec,
|
|
10
|
+
resolveBaseDir,
|
|
11
|
+
shouldTrackPath,
|
|
12
|
+
expectedReviewer,
|
|
13
|
+
SESSION_TTL_SEC,
|
|
14
|
+
STATE_REL_PATH,
|
|
15
|
+
} from "./lib/cross-review-utils.mjs";
|
|
55
16
|
|
|
56
17
|
function loadState(statePath) {
|
|
57
18
|
if (!existsSync(statePath)) return null;
|
|
@@ -77,21 +38,6 @@ function isGitCommitCommand(command) {
|
|
|
77
38
|
return /\bgit\s+commit\b/i.test(command);
|
|
78
39
|
}
|
|
79
40
|
|
|
80
|
-
function nudge(message) {
|
|
81
|
-
process.stdout.write(JSON.stringify({
|
|
82
|
-
hookSpecificOutput: {
|
|
83
|
-
hookEventName: "PreToolUse",
|
|
84
|
-
additionalContext: message,
|
|
85
|
-
},
|
|
86
|
-
}));
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function deny(message) {
|
|
91
|
-
process.stderr.write(message);
|
|
92
|
-
process.exit(2);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
41
|
function summarizePending(entries) {
|
|
96
42
|
return entries
|
|
97
43
|
.map((item) => {
|
|
@@ -2,40 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return new Promise((resolve) => {
|
|
16
|
-
let raw = "";
|
|
17
|
-
process.stdin.setEncoding("utf8");
|
|
18
|
-
process.stdin.on("data", (chunk) => {
|
|
19
|
-
raw += chunk;
|
|
20
|
-
});
|
|
21
|
-
process.stdin.on("end", () => resolve(raw));
|
|
22
|
-
process.stdin.on("error", () => resolve(""));
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseJson(raw) {
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(raw);
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function resolveBaseDir(payload) {
|
|
35
|
-
if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
|
|
36
|
-
if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
|
|
37
|
-
return process.cwd();
|
|
38
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
readStdin,
|
|
7
|
+
parseJson,
|
|
8
|
+
nowSec,
|
|
9
|
+
resolveBaseDir,
|
|
10
|
+
shouldTrackPath,
|
|
11
|
+
expectedReviewer,
|
|
12
|
+
SESSION_TTL_SEC,
|
|
13
|
+
STATE_REL_PATH,
|
|
14
|
+
} from "./lib/cross-review-utils.mjs";
|
|
39
15
|
|
|
40
16
|
function resolveStatePath(baseDir) {
|
|
41
17
|
return join(baseDir, STATE_REL_PATH);
|
|
@@ -91,16 +67,6 @@ function normalizePath(filePath, baseDir) {
|
|
|
91
67
|
return normalized.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
92
68
|
}
|
|
93
69
|
|
|
94
|
-
function shouldTrackPath(filePath) {
|
|
95
|
-
if (!filePath) return false;
|
|
96
|
-
const lower = filePath.toLowerCase();
|
|
97
|
-
|
|
98
|
-
if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
|
|
99
|
-
if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
|
|
100
|
-
if (EXCLUDED_FILE_PATTERN.test(lower)) return false;
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
70
|
function extractFilePath(toolInput) {
|
|
105
71
|
if (!toolInput || typeof toolInput !== "object") return "";
|
|
106
72
|
const candidate = toolInput.file_path ?? toolInput.path ?? toolInput.filePath ?? "";
|
|
@@ -185,13 +151,6 @@ function detectAuthor(payload) {
|
|
|
185
151
|
return "claude";
|
|
186
152
|
}
|
|
187
153
|
|
|
188
|
-
function expectedReviewer(author) {
|
|
189
|
-
if (author === "claude") return "codex";
|
|
190
|
-
if (author === "codex") return "claude";
|
|
191
|
-
if (author === "gemini") return "claude";
|
|
192
|
-
return "";
|
|
193
|
-
}
|
|
194
|
-
|
|
195
154
|
function applyReviewer(state, reviewer, ts) {
|
|
196
155
|
for (const [filePath, meta] of Object.entries(state.files)) {
|
|
197
156
|
if (!meta || typeof meta !== "object") continue;
|
|
@@ -28,6 +28,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
|
28
28
|
import { execFileSync } from "node:child_process";
|
|
29
29
|
import { tmpdir } from "node:os";
|
|
30
30
|
import { join } from "node:path";
|
|
31
|
+
import { nudge, deny } from "./lib/hook-utils.mjs";
|
|
31
32
|
|
|
32
33
|
const CACHE_FILE = join(tmpdir(), "tfx-psmux-check.json");
|
|
33
34
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
@@ -58,22 +59,14 @@ function writeMultiState(state) {
|
|
|
58
59
|
} catch { /* ignore */ }
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function nudge(message) {
|
|
62
|
-
process.stdout.write(JSON.stringify({
|
|
63
|
-
hookSpecificOutput: {
|
|
64
|
-
hookEventName: "PreToolUse",
|
|
65
|
-
additionalContext: message,
|
|
66
|
-
},
|
|
67
|
-
}));
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
62
|
function isPsmuxInstalled() {
|
|
72
|
-
// 캐시 확인
|
|
63
|
+
// 캐시 확인 (미래 타임스탬프 오염 방어)
|
|
73
64
|
try {
|
|
74
65
|
if (existsSync(CACHE_FILE)) {
|
|
75
66
|
const cache = JSON.parse(readFileSync(CACHE_FILE, "utf8"));
|
|
76
|
-
|
|
67
|
+
const age = Date.now() - cache.ts;
|
|
68
|
+
if (age >= 0 && age < CACHE_TTL_MS) return cache.ok;
|
|
69
|
+
// age < 0 → 미래 ts (오염) → 캐시 무시하고 재검사
|
|
77
70
|
}
|
|
78
71
|
} catch { /* cache miss */ }
|
|
79
72
|
|
|
@@ -147,11 +140,6 @@ function autoRoute(updatedCommand, reason) {
|
|
|
147
140
|
process.exit(0);
|
|
148
141
|
}
|
|
149
142
|
|
|
150
|
-
function deny(reason) {
|
|
151
|
-
process.stderr.write(reason);
|
|
152
|
-
process.exit(2);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
143
|
const HEADLESS_FALLBACK_COMMAND =
|
|
156
144
|
'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")';
|
|
157
145
|
const DIRECT_CLI_BYPASS_HINT =
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SESSION_TTL_SEC = 30 * 60;
|
|
4
|
+
export const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
|
|
5
|
+
|
|
6
|
+
export function readStdin() {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let raw = "";
|
|
9
|
+
process.stdin.setEncoding("utf8");
|
|
10
|
+
process.stdin.on("data", (chunk) => {
|
|
11
|
+
raw += chunk;
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on("end", () => resolve(raw));
|
|
14
|
+
process.stdin.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseJson(raw) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function nowSec() {
|
|
27
|
+
return Math.floor(Date.now() / 1000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBaseDir(payload) {
|
|
31
|
+
if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
|
|
32
|
+
if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function shouldTrackPath(filePath) {
|
|
37
|
+
if (typeof filePath !== "string" || !filePath.trim()) return false;
|
|
38
|
+
|
|
39
|
+
const lower = filePath.toLowerCase();
|
|
40
|
+
if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
|
|
41
|
+
if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
|
|
42
|
+
if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expectedReviewer(author) {
|
|
47
|
+
if (author === "claude") return "codex";
|
|
48
|
+
if (author === "codex") return "claude";
|
|
49
|
+
if (author === "gemini") return "claude";
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function nudge(message) {
|
|
2
|
+
process.stdout.write(JSON.stringify({
|
|
3
|
+
hookSpecificOutput: {
|
|
4
|
+
hookEventName: "PreToolUse",
|
|
5
|
+
additionalContext: message,
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deny(reason) {
|
|
12
|
+
process.stderr.write(reason);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
@@ -369,6 +369,13 @@ function selectContextualServers(baseServers, profile, options = {}) {
|
|
|
369
369
|
const selected = new Set(
|
|
370
370
|
(profile.alwaysOnServers || []).filter((server) => baseServers.includes(server)),
|
|
371
371
|
);
|
|
372
|
+
const wantsBrowserObservation = (
|
|
373
|
+
baseServers.includes('playwright')
|
|
374
|
+
&& /(?:browser|screenshot|layout|responsive|visual|screen|page|ui|ux|regression|캡처|스크린샷|레이아웃|반응형|화면|브라우저)/iu.test(taskText)
|
|
375
|
+
);
|
|
376
|
+
if (wantsBrowserObservation) {
|
|
377
|
+
selected.add('playwright');
|
|
378
|
+
}
|
|
372
379
|
const requestedSearchTool = typeof options.searchTool === 'string' ? options.searchTool : '';
|
|
373
380
|
|
|
374
381
|
const rankedServers = rankServers(
|
|
@@ -477,7 +484,9 @@ export function resolveAllowedServers(options = {}) {
|
|
|
477
484
|
const baseServers = availableServers.length
|
|
478
485
|
? profile.allowedServers.filter((server) => availableServers.includes(server))
|
|
479
486
|
: [...profile.allowedServers];
|
|
480
|
-
const manifestFiltered =
|
|
487
|
+
const manifestFiltered = availableServers.length
|
|
488
|
+
? baseServers
|
|
489
|
+
: applyManifestFilter(baseServers);
|
|
481
490
|
return selectContextualServers(manifestFiltered, profile, { ...options, inventory, inventoryIndex });
|
|
482
491
|
}
|
|
483
492
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* psmux-safety-guard.mjs — PreToolUse 훅
|
|
4
|
+
*
|
|
5
|
+
* psmux kill-session 직접 호출을 차단하고,
|
|
6
|
+
* psmux/wt 명령 사용 시 안전 규칙을 강제 주입한다.
|
|
7
|
+
*
|
|
8
|
+
* 동작:
|
|
9
|
+
* - kill-session 직접 호출 → deny (exit 2)
|
|
10
|
+
* - psmux/wt 명령 감지 → additionalContext에 안전 규칙 주입
|
|
11
|
+
* - 그 외 → 통과 (exit 0)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { nudge, deny } from "./lib/hook-utils.mjs";
|
|
15
|
+
|
|
16
|
+
const KILL_PATTERN = /\bpsmux\s+kill-session\b/;
|
|
17
|
+
const GRACEFUL_PATTERN = /send-keys.*exit.*Enter[\s\S]*sleep\s+\d/;
|
|
18
|
+
const PSMUX_OR_WT = /\b(psmux|wt\.exe|wt\s)/;
|
|
19
|
+
|
|
20
|
+
const SAFETY_RULES = `[psmux-safety] WT 프리징 방지 필수 규칙:
|
|
21
|
+
1) exit → sleep 2 → kill 순서 필수. 바로 kill 절대 금지.
|
|
22
|
+
2) psmux send-keys는 PowerShell 구문만 (bash 문법 직접 전달 금지).
|
|
23
|
+
3) 경로는 Windows 형식 (C:\\...). /c/... 금지.
|
|
24
|
+
4) wt.exe는 sp(split-pane)만 사용. nt(new-tab) 금지.
|
|
25
|
+
5) -p triflux 프로파일 필수.`;
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
let raw = "";
|
|
29
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
30
|
+
if (!raw.trim()) process.exit(0);
|
|
31
|
+
|
|
32
|
+
let input;
|
|
33
|
+
try { input = JSON.parse(raw); } catch { process.exit(0); }
|
|
34
|
+
|
|
35
|
+
if (input.tool_name !== "Bash") process.exit(0);
|
|
36
|
+
|
|
37
|
+
const cmd = input.tool_input?.command || "";
|
|
38
|
+
|
|
39
|
+
// kill-session 직접 호출 감지
|
|
40
|
+
if (KILL_PATTERN.test(cmd)) {
|
|
41
|
+
// 같은 명령 블록 안에 graceful exit 패턴이 있으면 허용
|
|
42
|
+
if (GRACEFUL_PATTERN.test(cmd)) {
|
|
43
|
+
nudge(SAFETY_RULES);
|
|
44
|
+
}
|
|
45
|
+
deny(
|
|
46
|
+
"[psmux-safety] psmux kill-session 직접 호출 차단.\n" +
|
|
47
|
+
"WT 프리징을 방지하려면 반드시 exit → sleep 2 → kill 순서를 따르세요.\n\n" +
|
|
48
|
+
"올바른 패턴:\n" +
|
|
49
|
+
" psmux send-keys -t SESSION \"exit\" Enter\n" +
|
|
50
|
+
" sleep 2\n" +
|
|
51
|
+
" psmux kill-session -t SESSION\n\n" +
|
|
52
|
+
"이 3줄을 하나의 명령 블록으로 실행하세요."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// psmux/wt 명령 감지 → 안전 규칙 주입
|
|
57
|
+
if (PSMUX_OR_WT.test(cmd)) {
|
|
58
|
+
nudge(SAFETY_RULES);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(() => process.exit(0));
|
package/scripts/remote-spawn.mjs
CHANGED
|
@@ -915,48 +915,100 @@ function isPrimaryPaneDead(paneId) {
|
|
|
915
915
|
}
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
-
async function
|
|
919
|
-
const
|
|
920
|
-
const
|
|
918
|
+
async function watchSpawnSessionExit(sessionName, options = {}) {
|
|
919
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
920
|
+
const pollMs = parsePositiveInt(options.pollMs, DEFAULT_CLEANUP_WATCH_POLL_MS);
|
|
921
|
+
const graceMs = parsePositiveInt(options.graceMs, DEFAULT_CLEANUP_WATCH_GRACE_MS);
|
|
922
|
+
const maxWaitMs = parsePositiveInt(options.maxWaitMs, DEFAULT_CLEANUP_WATCH_MAX_MS);
|
|
923
|
+
const sessionExists = typeof options.sessionExists === "function"
|
|
924
|
+
? options.sessionExists
|
|
925
|
+
: psmuxSessionExists;
|
|
926
|
+
const getPaneStatus = typeof options.getPaneStatus === "function"
|
|
927
|
+
? options.getPaneStatus
|
|
928
|
+
: (targetPaneId) => ({ isDead: isPrimaryPaneDead(targetPaneId), exitCode: null });
|
|
929
|
+
const killSession = typeof options.killSession === "function"
|
|
930
|
+
? options.killSession
|
|
931
|
+
: killPsmuxSession;
|
|
932
|
+
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
933
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
|
|
934
|
+
const startedAt = now();
|
|
921
935
|
let consecutiveErrors = 0;
|
|
922
936
|
|
|
923
|
-
while (
|
|
924
|
-
if (!
|
|
925
|
-
return;
|
|
937
|
+
while (now() - startedAt <= maxWaitMs) {
|
|
938
|
+
if (!sessionExists(sessionName)) {
|
|
939
|
+
return { cleaned: false, reason: "session-missing" };
|
|
926
940
|
}
|
|
927
941
|
|
|
928
|
-
const
|
|
929
|
-
if (
|
|
942
|
+
const paneStatus = getPaneStatus(paneId) || {};
|
|
943
|
+
if (paneStatus.isDead == null) {
|
|
930
944
|
consecutiveErrors += 1;
|
|
931
|
-
if (consecutiveErrors >= 10)
|
|
945
|
+
if (consecutiveErrors >= 10) {
|
|
946
|
+
return { cleaned: false, reason: "probe-failed" };
|
|
947
|
+
}
|
|
932
948
|
} else {
|
|
933
949
|
consecutiveErrors = 0;
|
|
934
950
|
}
|
|
935
|
-
if (dead === true) {
|
|
936
|
-
await sleepMsAsync(timings.graceMs);
|
|
937
951
|
|
|
938
|
-
|
|
939
|
-
|
|
952
|
+
if (paneStatus.isDead === true) {
|
|
953
|
+
await sleep(graceMs);
|
|
954
|
+
|
|
955
|
+
if (!sessionExists(sessionName)) {
|
|
956
|
+
return { cleaned: false, reason: "session-missing" };
|
|
940
957
|
}
|
|
941
958
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
959
|
+
const afterGrace = getPaneStatus(paneId) || {};
|
|
960
|
+
if (afterGrace.isDead === true) {
|
|
961
|
+
killSession(sessionName);
|
|
962
|
+
return {
|
|
963
|
+
cleaned: true,
|
|
964
|
+
reason: "pane-dead",
|
|
965
|
+
exitCode: afterGrace.exitCode ?? paneStatus.exitCode ?? null,
|
|
966
|
+
};
|
|
945
967
|
}
|
|
946
968
|
}
|
|
947
969
|
|
|
948
|
-
await
|
|
970
|
+
await sleep(pollMs);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return { cleaned: false, reason: "timeout" };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function runSpawnCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
977
|
+
await watchSpawnSessionExit(sessionName, {
|
|
978
|
+
paneId,
|
|
979
|
+
pollMs: timingOptions.pollMs,
|
|
980
|
+
graceMs: timingOptions.graceMs,
|
|
981
|
+
maxWaitMs: timingOptions.maxMs,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function startSpawnExitWatcher(sessionName, options = {}) {
|
|
986
|
+
if (!options.force && !shouldUsePsmux()) {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
991
|
+
const args = buildSpawnCleanupWatcherArgs(sessionName, paneId, {
|
|
992
|
+
graceMs: options.graceMs,
|
|
993
|
+
maxMs: options.maxMs,
|
|
994
|
+
pollMs: options.pollMs,
|
|
995
|
+
});
|
|
996
|
+
args[0] = options.scriptPath || args[0];
|
|
997
|
+
const spawnFn = typeof options.spawnFn === "function" ? options.spawnFn : spawn;
|
|
998
|
+
const child = spawnFn(options.execPath || process.execPath, args, {
|
|
999
|
+
detached: true,
|
|
1000
|
+
stdio: "ignore",
|
|
1001
|
+
windowsHide: true,
|
|
1002
|
+
});
|
|
1003
|
+
if (child && typeof child.unref === "function") {
|
|
1004
|
+
child.unref();
|
|
949
1005
|
}
|
|
1006
|
+
return true;
|
|
950
1007
|
}
|
|
951
1008
|
|
|
952
1009
|
function startSpawnSessionCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
953
|
-
const args = buildSpawnCleanupWatcherArgs(sessionName, paneId, timingOptions);
|
|
954
1010
|
try {
|
|
955
|
-
|
|
956
|
-
detached: true,
|
|
957
|
-
stdio: "ignore",
|
|
958
|
-
windowsHide: true,
|
|
959
|
-
}).unref();
|
|
1011
|
+
startSpawnExitWatcher(sessionName, { ...timingOptions, force: true, paneId });
|
|
960
1012
|
} catch {
|
|
961
1013
|
// watcher 시작 실패는 spawn 자체 실패로 보지 않는다.
|
|
962
1014
|
}
|
|
@@ -1230,6 +1282,8 @@ export const __remoteSpawnTest = {
|
|
|
1230
1282
|
buildPromptContext,
|
|
1231
1283
|
parseArgs,
|
|
1232
1284
|
rewritePromptPaths,
|
|
1285
|
+
startSpawnExitWatcher,
|
|
1233
1286
|
stageRemotePromptFiles,
|
|
1234
1287
|
validateTransferCandidate,
|
|
1288
|
+
watchSpawnSessionExit,
|
|
1235
1289
|
};
|
|
@@ -69,10 +69,11 @@ export function createIsolatedSession(options = {}) {
|
|
|
69
69
|
export function attachWithWindowsTerminal(sessionName, options = {}) {
|
|
70
70
|
const profile = options.profile || DEFAULT_ATTACH_PROFILE;
|
|
71
71
|
const title = options.title || sessionName;
|
|
72
|
+
const spawnFn = options.spawnFn || spawn;
|
|
72
73
|
|
|
73
74
|
// sp (split-pane), not new-tab
|
|
74
75
|
const wtArgs = ["sp", "-p", profile, "--title", title, "--", "psmux", "attach-session", "-t", sessionName];
|
|
75
|
-
const child =
|
|
76
|
+
const child = spawnFn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false });
|
|
76
77
|
child.unref();
|
|
77
78
|
return wtArgs;
|
|
78
79
|
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -141,6 +141,11 @@ const SYNC_MAP = [
|
|
|
141
141
|
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
|
|
142
142
|
label: "hub/workers/claude-worker.mjs",
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "worker-utils.mjs"),
|
|
146
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "worker-utils.mjs"),
|
|
147
|
+
label: "hub/workers/worker-utils.mjs",
|
|
148
|
+
},
|
|
144
149
|
{
|
|
145
150
|
src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
|
|
146
151
|
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
|
|
@@ -216,6 +221,26 @@ const SYNC_MAP = [
|
|
|
216
221
|
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
|
|
217
222
|
label: "lib/mcp-server-catalog.mjs",
|
|
218
223
|
},
|
|
224
|
+
{
|
|
225
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-manifest.mjs"),
|
|
226
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-manifest.mjs"),
|
|
227
|
+
label: "lib/mcp-manifest.mjs",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "hook-utils.mjs"),
|
|
231
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "hook-utils.mjs"),
|
|
232
|
+
label: "lib/hook-utils.mjs",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
src: join(PLUGIN_ROOT, "scripts", "psmux-safety-guard.mjs"),
|
|
236
|
+
dst: join(CLAUDE_DIR, "scripts", "psmux-safety-guard.mjs"),
|
|
237
|
+
label: "psmux-safety-guard.mjs",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "cross-review-utils.mjs"),
|
|
241
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "cross-review-utils.mjs"),
|
|
242
|
+
label: "lib/cross-review-utils.mjs",
|
|
243
|
+
},
|
|
219
244
|
{
|
|
220
245
|
src: join(PLUGIN_ROOT, "scripts", "lib", "keyword-rules.mjs"),
|
|
221
246
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
@@ -331,13 +356,18 @@ function ensureCodexProfiles() {
|
|
|
331
356
|
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
332
357
|
}
|
|
333
358
|
|
|
334
|
-
return changed;
|
|
335
|
-
} catch {
|
|
336
|
-
return 0;
|
|
359
|
+
return { ok: true, changed };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
return { ok: false, changed: 0, message: error.message };
|
|
337
362
|
}
|
|
338
363
|
}
|
|
339
364
|
|
|
340
|
-
export {
|
|
365
|
+
export {
|
|
366
|
+
replaceProfileSection, hasProfileSection, escapeRegExp, detectDevMode,
|
|
367
|
+
SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR,
|
|
368
|
+
SKILL_ALIASES, REQUIRED_CODEX_PROFILES,
|
|
369
|
+
buildAliasedSkillContent, syncAliasedSkillDir, getVersion, ensureCodexProfiles,
|
|
370
|
+
};
|
|
341
371
|
|
|
342
372
|
async function main() {
|
|
343
373
|
const isSync = process.argv.includes("--sync");
|
|
@@ -814,8 +844,8 @@ if (process.platform === "win32") {
|
|
|
814
844
|
|
|
815
845
|
// ── Codex 프로필 자동 보정 ──
|
|
816
846
|
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
847
|
+
const codexProfileResult = ensureCodexProfiles();
|
|
848
|
+
if (codexProfileResult.changed > 0) {
|
|
819
849
|
synced++;
|
|
820
850
|
}
|
|
821
851
|
|