triflux 9.8.2 → 9.8.5
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 +144 -1
- package/hooks/hook-adaptive-collector.mjs +86 -0
- package/hooks/hook-registry.json +4 -4
- package/hooks/keyword-rules.json +189 -1
- package/hooks/safety-guard.mjs +17 -1
- package/hub/adaptive-diagnostic.mjs +319 -0
- package/hub/adaptive-inject.mjs +186 -0
- package/hub/adaptive-memory.mjs +322 -0
- package/hub/adaptive.mjs +143 -0
- package/hub/assign-callbacks.mjs +13 -16
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +6 -3
- package/hub/intent.mjs +2 -7
- package/hub/lib/known-errors.json +72 -0
- package/hub/lib/memory-store.mjs +748 -0
- package/hub/lib/process-utils.mjs +5 -4
- package/hub/lib/ssh-command.mjs +150 -0
- package/hub/lib/uuidv7.mjs +44 -0
- package/hub/middleware/request-logger.mjs +80 -0
- package/hub/pipe.mjs +5 -8
- package/hub/platform.mjs +225 -0
- package/hub/reflexion.mjs +303 -38
- package/hub/router.mjs +791 -791
- package/hub/schema.sql +2 -0
- package/hub/server.mjs +288 -70
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/store-adapter.mjs +118 -0
- package/hub/store.mjs +107 -57
- package/hub/team/backend.mjs +3 -5
- package/hub/team/cli/services/hub-client.mjs +38 -19
- package/hub/team/cli/services/native-control.mjs +4 -5
- package/hub/team/conductor.mjs +602 -0
- package/hub/team/event-log.mjs +76 -0
- package/hub/team/headless.mjs +368 -77
- package/hub/team/health-probe.mjs +272 -0
- package/hub/team/launcher-template.mjs +94 -0
- package/hub/team/lead-control.mjs +104 -0
- package/hub/team/nativeProxy.mjs +2 -1
- package/hub/team/notify.mjs +293 -0
- package/hub/team/pane.mjs +4 -5
- package/hub/team/process-cleanup.mjs +342 -0
- package/hub/team/psmux.mjs +3 -3
- package/hub/team/remote-probe.mjs +276 -0
- package/hub/team/remote-watcher.mjs +478 -0
- package/hub/team/session-sync.mjs +169 -0
- package/hub/team/tui-remote-adapter.mjs +393 -0
- package/hub/team/tui.mjs +206 -2
- package/hub/team-bridge.mjs +25 -0
- package/hub/tools.mjs +1 -1
- package/hub/tray.mjs +8 -7
- package/hub/workers/claude-worker.mjs +89 -37
- package/hub/workers/codex-mcp.mjs +123 -29
- package/hub/workers/gemini-worker.mjs +81 -137
- package/hub/workers/interface.mjs +12 -0
- package/hub/workers/worker-utils.mjs +78 -0
- package/hud/constants.mjs +7 -0
- package/hud/context-monitor.mjs +397 -0
- package/hud/hud-qos-status.mjs +8 -4
- package/hud/providers/claude.mjs +5 -0
- package/hud/renderers.mjs +13 -9
- package/package.json +21 -5
- package/scripts/__tests__/gen-skill-docs.test.mjs +87 -0
- package/scripts/__tests__/skill-template.test.mjs +104 -0
- package/scripts/cache-warmup.mjs +3 -3
- package/scripts/gen-skill-docs.mjs +110 -0
- package/scripts/headless-guard.mjs +7 -1
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/claudemd-scanner.mjs +218 -0
- package/scripts/lib/env-probe.mjs +95 -14
- package/scripts/lib/handoff.mjs +171 -0
- package/scripts/lib/skill-template.mjs +222 -0
- package/scripts/notion-read.mjs +5 -3
- package/scripts/pack.mjs +205 -0
- package/scripts/preflight-cache.mjs +6 -5
- package/scripts/remote-spawn.mjs +5 -5
- package/scripts/setup.mjs +300 -729
- package/scripts/templates/claudemd-tfx-section.md +54 -0
- package/scripts/test-lock.mjs +71 -0
- package/scripts/tfx-route.sh +2 -2
- package/scripts/tmp-cleanup.mjs +34 -5
- package/skills/_templates/base.md +9 -0
- package/skills/_templates/deep.md +6 -0
- package/skills/tfx-codex-swarm/SKILL.md +55 -11
- /package/hub/{team/codex-compat.mjs → codex-compat.mjs} +0 -0
package/bin/triflux.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// triflux CLI — setup, doctor, version
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, readSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
|
|
4
|
-
import { join, dirname, basename } from "path";
|
|
4
|
+
import { join, dirname, basename, resolve } from "path";
|
|
5
5
|
import { homedir, tmpdir } from "os";
|
|
6
6
|
import { execSync, execFileSync, spawn } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
@@ -11,6 +11,7 @@ import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions,
|
|
|
11
11
|
import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
|
|
12
12
|
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
13
13
|
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
14
|
+
import { serializeHandoff } from "../scripts/lib/handoff.mjs";
|
|
14
15
|
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
15
16
|
import { probePsmuxSupport, formatPsmuxInstallGuidance, formatPsmuxUpdateGuidance } from "../scripts/lib/psmux-info.mjs";
|
|
16
17
|
import {
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
extractManagedHookFilename, getManagedRegistryHooks, ensureHooksInSettings,
|
|
29
30
|
ensureCodexHubServerConfig,
|
|
30
31
|
} from "../scripts/setup.mjs";
|
|
32
|
+
import { cleanupTmpFiles } from "../scripts/tmp-cleanup.mjs";
|
|
31
33
|
|
|
32
34
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
33
35
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -97,6 +99,17 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
|
|
|
97
99
|
{ name: "--json", type: "boolean", description: "버전 정보를 JSON으로 출력" },
|
|
98
100
|
],
|
|
99
101
|
},
|
|
102
|
+
handoff: {
|
|
103
|
+
usage: "tfx handoff [--target local|remote] [--decision <text>] [--decision-file <path>] [--output <path>] [--json]",
|
|
104
|
+
description: "현재 작업 컨텍스트를 세션 핸드오프 프롬프트로 직렬화",
|
|
105
|
+
options: [
|
|
106
|
+
{ name: "--target", type: "string", description: "주입 대상 (local|remote, 기본값 remote)" },
|
|
107
|
+
{ name: "--decision", type: "string", description: "핸드오프 결정사항 (반복 지정 가능)" },
|
|
108
|
+
{ name: "--decision-file", type: "string", description: "결정사항 파일 (라인/불릿 단위)" },
|
|
109
|
+
{ name: "--output", type: "string", description: "생성한 핸드오프 프롬프트 저장 경로" },
|
|
110
|
+
{ name: "--json", type: "boolean", description: "핸드오프 결과를 JSON으로 출력" },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
100
113
|
list: {
|
|
101
114
|
usage: "tfx list [--json]",
|
|
102
115
|
description: "패키지 스킬과 사용자 스킬 목록 표시",
|
|
@@ -2782,6 +2795,128 @@ function cmdVersion(options = {}) {
|
|
|
2782
2795
|
console.log("");
|
|
2783
2796
|
}
|
|
2784
2797
|
|
|
2798
|
+
function cmdHandoff(args = [], options = {}) {
|
|
2799
|
+
const { json = false } = options;
|
|
2800
|
+
const parsed = {
|
|
2801
|
+
target: "remote",
|
|
2802
|
+
decisions: [],
|
|
2803
|
+
decisionFile: null,
|
|
2804
|
+
output: null,
|
|
2805
|
+
cwd: process.cwd(),
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2809
|
+
const arg = args[index];
|
|
2810
|
+
const next = args[index + 1];
|
|
2811
|
+
|
|
2812
|
+
if (arg === "--target") {
|
|
2813
|
+
if (!next || next.startsWith("-")) {
|
|
2814
|
+
throw createCliError("--target 값이 필요합니다 (local|remote)", {
|
|
2815
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2816
|
+
reason: "argError",
|
|
2817
|
+
fix: "tfx handoff --target remote",
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
if (!["local", "remote"].includes(next)) {
|
|
2821
|
+
throw createCliError(`지원하지 않는 --target 값: ${next}`, {
|
|
2822
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2823
|
+
reason: "argError",
|
|
2824
|
+
fix: "tfx handoff --target local|remote",
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
parsed.target = next;
|
|
2828
|
+
index += 1;
|
|
2829
|
+
continue;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
if (arg === "--decision") {
|
|
2833
|
+
if (!next || next.startsWith("-")) {
|
|
2834
|
+
throw createCliError("--decision 값이 필요합니다", {
|
|
2835
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2836
|
+
reason: "argError",
|
|
2837
|
+
fix: "tfx handoff --decision \"결정사항\"",
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
parsed.decisions.push(next);
|
|
2841
|
+
index += 1;
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
if (arg === "--decision-file") {
|
|
2846
|
+
if (!next || next.startsWith("-")) {
|
|
2847
|
+
throw createCliError("--decision-file 경로가 필요합니다", {
|
|
2848
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2849
|
+
reason: "argError",
|
|
2850
|
+
fix: "tfx handoff --decision-file .omx/notepad.md",
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
parsed.decisionFile = resolve(next);
|
|
2854
|
+
index += 1;
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
if (arg === "--output" || arg === "--out") {
|
|
2859
|
+
if (!next || next.startsWith("-")) {
|
|
2860
|
+
throw createCliError(`${arg} 경로가 필요합니다`, {
|
|
2861
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2862
|
+
reason: "argError",
|
|
2863
|
+
fix: "tfx handoff --output .omx/handoff.md",
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
parsed.output = resolve(next);
|
|
2867
|
+
index += 1;
|
|
2868
|
+
continue;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
if (arg === "--cwd") {
|
|
2872
|
+
if (!next || next.startsWith("-")) {
|
|
2873
|
+
throw createCliError("--cwd 경로가 필요합니다", {
|
|
2874
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2875
|
+
reason: "argError",
|
|
2876
|
+
fix: "tfx handoff --cwd <project-path>",
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
parsed.cwd = resolve(next);
|
|
2880
|
+
index += 1;
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
throw createCliError(`알 수 없는 handoff 옵션: ${arg}`, {
|
|
2885
|
+
exitCode: EXIT_ARG_ERROR,
|
|
2886
|
+
reason: "argError",
|
|
2887
|
+
fix: "tfx handoff --target remote --output .omx/handoff.md",
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
const result = serializeHandoff({
|
|
2892
|
+
target: parsed.target,
|
|
2893
|
+
decisions: parsed.decisions,
|
|
2894
|
+
decisionFile: parsed.decisionFile,
|
|
2895
|
+
cwd: parsed.cwd,
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
if (parsed.output) {
|
|
2899
|
+
const outputDir = dirname(parsed.output);
|
|
2900
|
+
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
|
2901
|
+
writeFileSync(parsed.output, `${result.prompt}\n`, "utf8");
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
if (json) {
|
|
2905
|
+
printJson({
|
|
2906
|
+
handoff: {
|
|
2907
|
+
...result,
|
|
2908
|
+
...(parsed.output ? { output: parsed.output } : {}),
|
|
2909
|
+
},
|
|
2910
|
+
});
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
console.log(result.prompt);
|
|
2915
|
+
if (parsed.output) {
|
|
2916
|
+
console.log(`\n${DIM}saved:${RESET} ${parsed.output}`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2785
2920
|
function cmdSchema(args = []) {
|
|
2786
2921
|
const bundle = loadDelegatorSchemaBundle();
|
|
2787
2922
|
const selector = String(args[0] || "").trim();
|
|
@@ -3053,6 +3188,7 @@ ${updateNotice}
|
|
|
3053
3188
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
3054
3189
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
3055
3190
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
3191
|
+
${WHITE_BRIGHT}tfx handoff${RESET} ${GRAY}현재 컨텍스트를 원격/로컬 핸드오프 프롬프트로 생성${RESET}
|
|
3056
3192
|
${WHITE_BRIGHT}tfx schema${RESET} ${GRAY}CLI/Hub schema JSON 출력${RESET}
|
|
3057
3193
|
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
3058
3194
|
${WHITE_BRIGHT}tfx tray${RESET} ${GRAY}Windows 시스템 트레이 실행${RESET}
|
|
@@ -3531,6 +3667,10 @@ async function main() {
|
|
|
3531
3667
|
const cmd = NORMALIZED_ARGS[0] || "help";
|
|
3532
3668
|
const cmdArgs = NORMALIZED_ARGS.slice(1);
|
|
3533
3669
|
|
|
3670
|
+
cleanupTmpFiles({
|
|
3671
|
+
protectPaths: [process.env.HOME, process.env.USERPROFILE],
|
|
3672
|
+
}).catch(() => {});
|
|
3673
|
+
|
|
3534
3674
|
switch (cmd) {
|
|
3535
3675
|
case "setup":
|
|
3536
3676
|
cmdSetup({ dryRun: cmdArgs.includes("--dry-run") });
|
|
@@ -3554,6 +3694,9 @@ async function main() {
|
|
|
3554
3694
|
case "ls":
|
|
3555
3695
|
cmdList({ json: JSON_OUTPUT });
|
|
3556
3696
|
return;
|
|
3697
|
+
case "handoff":
|
|
3698
|
+
cmdHandoff(cmdArgs, { json: JSON_OUTPUT });
|
|
3699
|
+
return;
|
|
3557
3700
|
case "hub":
|
|
3558
3701
|
await cmdHub(cmdArgs, { json: JSON_OUTPUT && (cmdArgs[0] || "status") === "status" });
|
|
3559
3702
|
return;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { createAdaptiveEngine } from '../hub/adaptive.mjs';
|
|
8
|
+
|
|
9
|
+
let engine = null;
|
|
10
|
+
let createEngine = createAdaptiveEngine;
|
|
11
|
+
|
|
12
|
+
function readStdin() {
|
|
13
|
+
try {
|
|
14
|
+
return readFileSync(0, 'utf8');
|
|
15
|
+
} catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function inferProjectSlug(cwd = process.cwd()) {
|
|
21
|
+
const packagePath = join(cwd, 'package.json');
|
|
22
|
+
if (existsSync(packagePath)) {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
25
|
+
if (typeof pkg.name === 'string' && pkg.name.trim()) return pkg.name.trim();
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
return basename(cwd) || 'default';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getEngine() {
|
|
32
|
+
if (engine) return engine;
|
|
33
|
+
engine = createEngine({
|
|
34
|
+
projectSlug: inferProjectSlug(),
|
|
35
|
+
repoRoot: process.cwd(),
|
|
36
|
+
});
|
|
37
|
+
engine.startSession?.();
|
|
38
|
+
return engine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildErrorContext(event = {}) {
|
|
42
|
+
return {
|
|
43
|
+
exitCode: event.exitCode,
|
|
44
|
+
stderr: String(event.stderr || '').slice(0, 500),
|
|
45
|
+
tool: event.tool,
|
|
46
|
+
command: String(event.command || '').slice(0, 200),
|
|
47
|
+
timestamp: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function hookAdaptiveCollector(event = {}) {
|
|
52
|
+
if (Number(event.exitCode) === 0) return null;
|
|
53
|
+
if (!event.tool || event.tool === 'Read') return null;
|
|
54
|
+
|
|
55
|
+
const result = getEngine().handleError(buildErrorContext(event));
|
|
56
|
+
if (result?.diagnosed) {
|
|
57
|
+
console.error(`[adaptive] 에러 패턴 감지: ${result.rule?.id || 'unknown'}`);
|
|
58
|
+
if (result.promoted) {
|
|
59
|
+
console.error(`[adaptive] 규칙 승격 → Tier ${result.rule?.tier ?? '?'}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function __setAdaptiveCollectorFactoryForTests(factory) {
|
|
66
|
+
createEngine = factory;
|
|
67
|
+
engine = null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function __resetAdaptiveCollectorForTests() {
|
|
71
|
+
createEngine = createAdaptiveEngine;
|
|
72
|
+
engine = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function main() {
|
|
76
|
+
const raw = readStdin();
|
|
77
|
+
if (!raw.trim()) return;
|
|
78
|
+
try {
|
|
79
|
+
hookAdaptiveCollector(JSON.parse(raw));
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const isEntrypoint = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
84
|
+
if (isEntrypoint) {
|
|
85
|
+
main();
|
|
86
|
+
}
|
package/hooks/hook-registry.json
CHANGED
|
@@ -153,9 +153,9 @@
|
|
|
153
153
|
"id": "ext-session-vault-start",
|
|
154
154
|
"source": "session-vault",
|
|
155
155
|
"matcher": "*",
|
|
156
|
-
"command": "${HOME}/Desktop/Projects/
|
|
156
|
+
"command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
|
|
157
157
|
"priority": 100,
|
|
158
|
-
"enabled":
|
|
158
|
+
"enabled": true,
|
|
159
159
|
"timeout": 10,
|
|
160
160
|
"blocking": false,
|
|
161
161
|
"description": "세션 볼트 로깅 시작"
|
|
@@ -177,9 +177,9 @@
|
|
|
177
177
|
"id": "ext-session-vault-export",
|
|
178
178
|
"source": "session-vault",
|
|
179
179
|
"matcher": "*",
|
|
180
|
-
"command": "${HOME}/Desktop/Projects/
|
|
180
|
+
"command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
|
|
181
181
|
"priority": 100,
|
|
182
|
-
"enabled":
|
|
182
|
+
"enabled": true,
|
|
183
183
|
"timeout": 30,
|
|
184
184
|
"blocking": false,
|
|
185
185
|
"description": "세션 트랜스크립트 내보내기"
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -37,6 +37,46 @@
|
|
|
37
37
|
"state": null,
|
|
38
38
|
"mcp_route": null
|
|
39
39
|
},
|
|
40
|
+
{
|
|
41
|
+
"id": "tfx-codex-swarm",
|
|
42
|
+
"patterns": [
|
|
43
|
+
{
|
|
44
|
+
"source": "\\btfx[\\s-]?codex[\\s-]?swarm\\b",
|
|
45
|
+
"flags": "i"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"source": "\\bcodex[\\s-]?swarm\\b",
|
|
49
|
+
"flags": "i"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"skill": "tfx-codex-swarm",
|
|
53
|
+
"priority": 1,
|
|
54
|
+
"supersedes": [
|
|
55
|
+
"tfx-codex"
|
|
56
|
+
],
|
|
57
|
+
"exclusive": false,
|
|
58
|
+
"state": null,
|
|
59
|
+
"mcp_route": null
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "tfx-remote-spawn",
|
|
63
|
+
"patterns": [
|
|
64
|
+
{
|
|
65
|
+
"source": "\\btfx[\\s-]?remote[\\s-]?spawn\\b",
|
|
66
|
+
"flags": "i"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"source": "\\bremote[\\s-]?spawn\\b",
|
|
70
|
+
"flags": "i"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"skill": "tfx-remote-spawn",
|
|
74
|
+
"priority": 1,
|
|
75
|
+
"supersedes": [],
|
|
76
|
+
"exclusive": false,
|
|
77
|
+
"state": null,
|
|
78
|
+
"mcp_route": null
|
|
79
|
+
},
|
|
40
80
|
{
|
|
41
81
|
"id": "tfx-auto-codex",
|
|
42
82
|
"patterns": [
|
|
@@ -74,7 +114,7 @@
|
|
|
74
114
|
"id": "tfx-codex",
|
|
75
115
|
"patterns": [
|
|
76
116
|
{
|
|
77
|
-
"source": "\\btfx[\\s-]?codex\\b",
|
|
117
|
+
"source": "\\btfx[\\s-]?codex\\b(?![\\s-]?swarm)",
|
|
78
118
|
"flags": "i"
|
|
79
119
|
}
|
|
80
120
|
],
|
|
@@ -373,6 +413,154 @@
|
|
|
373
413
|
"state": null,
|
|
374
414
|
"mcp_route": "codex"
|
|
375
415
|
},
|
|
416
|
+
{
|
|
417
|
+
"id": "gstack-checkpoint",
|
|
418
|
+
"patterns": [
|
|
419
|
+
{
|
|
420
|
+
"source": "(?:어디까지\\s*했|뭐\\s*하고\\s*있었|뭘\\s*해야\\s*하|까먹|기억이\\s*안|이어서\\s*하|중단\\s*지점)",
|
|
421
|
+
"flags": "i"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"source": "\\b(?:checkpoint|resume|pick\\s*up|where\\s*was\\s*I)\\b",
|
|
425
|
+
"flags": "i"
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
"skill": "checkpoint",
|
|
429
|
+
"priority": 5,
|
|
430
|
+
"supersedes": [],
|
|
431
|
+
"exclusive": false,
|
|
432
|
+
"state": null,
|
|
433
|
+
"mcp_route": null
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"id": "gstack-office-hours",
|
|
437
|
+
"patterns": [
|
|
438
|
+
{
|
|
439
|
+
"source": "(?:아이디어\\s*(?:있|정리|브레인)|뭘\\s*만들|제품\\s*구상)",
|
|
440
|
+
"flags": "i"
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"source": "\\boffice[\\s-]?hours\\b",
|
|
444
|
+
"flags": "i"
|
|
445
|
+
}
|
|
446
|
+
],
|
|
447
|
+
"skill": "office-hours",
|
|
448
|
+
"priority": 5,
|
|
449
|
+
"supersedes": [],
|
|
450
|
+
"exclusive": false,
|
|
451
|
+
"state": null,
|
|
452
|
+
"mcp_route": null
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
"id": "gstack-ship",
|
|
456
|
+
"patterns": [
|
|
457
|
+
{
|
|
458
|
+
"source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
|
|
459
|
+
"flags": "i"
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
"source": "\\bship\\b(?!.*tfx)",
|
|
463
|
+
"flags": "i"
|
|
464
|
+
}
|
|
465
|
+
],
|
|
466
|
+
"skill": "ship",
|
|
467
|
+
"priority": 5,
|
|
468
|
+
"supersedes": [],
|
|
469
|
+
"exclusive": false,
|
|
470
|
+
"state": null,
|
|
471
|
+
"mcp_route": null
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
"id": "gstack-investigate",
|
|
475
|
+
"patterns": [
|
|
476
|
+
{
|
|
477
|
+
"source": "(?:왜\\s*(?:안\\s*돼|터져|에러)|원인\\s*(?:찾아|분석)|root\\s*cause)",
|
|
478
|
+
"flags": "i"
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"source": "\\binvestigate\\b",
|
|
482
|
+
"flags": "i"
|
|
483
|
+
}
|
|
484
|
+
],
|
|
485
|
+
"skill": "investigate",
|
|
486
|
+
"priority": 5,
|
|
487
|
+
"supersedes": [],
|
|
488
|
+
"exclusive": false,
|
|
489
|
+
"state": null,
|
|
490
|
+
"mcp_route": null
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"id": "gstack-cso",
|
|
494
|
+
"patterns": [
|
|
495
|
+
{
|
|
496
|
+
"source": "(?:보안\\s*(?:감사|점검|리뷰|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
|
|
497
|
+
"flags": "i"
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
"skill": "cso",
|
|
501
|
+
"priority": 5,
|
|
502
|
+
"supersedes": [],
|
|
503
|
+
"exclusive": false,
|
|
504
|
+
"state": null,
|
|
505
|
+
"mcp_route": null
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
"id": "gstack-qa-browser",
|
|
509
|
+
"patterns": [
|
|
510
|
+
{
|
|
511
|
+
"source": "(?:사이트|브라우저|웹)\\s*(?:QA|테스트|확인|점검)",
|
|
512
|
+
"flags": "i"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
"source": "(?:클릭|접속).*(?:테스트|확인)",
|
|
516
|
+
"flags": "i"
|
|
517
|
+
}
|
|
518
|
+
],
|
|
519
|
+
"skill": "qa",
|
|
520
|
+
"priority": 5,
|
|
521
|
+
"supersedes": [],
|
|
522
|
+
"exclusive": false,
|
|
523
|
+
"state": null,
|
|
524
|
+
"mcp_route": null
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
"id": "gstack-retro",
|
|
528
|
+
"patterns": [
|
|
529
|
+
{
|
|
530
|
+
"source": "(?:회고|이번\\s*주\\s*뭐\\s*했|주간\\s*리뷰|뭘\\s*했지)",
|
|
531
|
+
"flags": "i"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"source": "\\bretro\\b",
|
|
535
|
+
"flags": "i"
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
"skill": "retro",
|
|
539
|
+
"priority": 5,
|
|
540
|
+
"supersedes": [],
|
|
541
|
+
"exclusive": false,
|
|
542
|
+
"state": null,
|
|
543
|
+
"mcp_route": null
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"id": "gstack-autoplan",
|
|
547
|
+
"patterns": [
|
|
548
|
+
{
|
|
549
|
+
"source": "(?:자동\\s*리뷰|리뷰\\s*파이프라인|전체\\s*리뷰)",
|
|
550
|
+
"flags": "i"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
"source": "\\bautoplan\\b",
|
|
554
|
+
"flags": "i"
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
"skill": "autoplan",
|
|
558
|
+
"priority": 5,
|
|
559
|
+
"supersedes": [],
|
|
560
|
+
"exclusive": false,
|
|
561
|
+
"state": null,
|
|
562
|
+
"mcp_route": null
|
|
563
|
+
},
|
|
376
564
|
{
|
|
377
565
|
"id": "suppress-omc-team",
|
|
378
566
|
"patterns": [
|
package/hooks/safety-guard.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
|
|
|
12
12
|
|
|
13
13
|
// ── 차단 규칙 ──────────────────────────────────────────────
|
|
14
14
|
const BLOCK_RULES = [
|
|
15
|
-
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
|
|
15
|
+
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
|
|
16
16
|
{ pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
|
|
17
17
|
{ pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
|
|
18
18
|
{ pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
|
|
@@ -22,6 +22,8 @@ const BLOCK_RULES = [
|
|
|
22
22
|
{ pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
|
|
23
23
|
{ pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
|
|
24
24
|
{ pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
|
|
25
|
+
{ pattern: /\bpsmux\s+kill-session\b/i, reason: "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>", skipIfGit: true },
|
|
26
|
+
{ pattern: /\bpsmux\s+kill-server\b/i, reason: "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용", skipIfGit: true },
|
|
25
27
|
];
|
|
26
28
|
|
|
27
29
|
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
@@ -60,8 +62,22 @@ function main() {
|
|
|
60
62
|
const command = (input.tool_input?.command || "").trim();
|
|
61
63
|
if (!command) process.exit(0);
|
|
62
64
|
|
|
65
|
+
// psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
|
|
66
|
+
// git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
|
|
67
|
+
function isPsmuxInvocation(cmd) {
|
|
68
|
+
// 명령을 세그먼트로 분할 (&&, ;, | 기준)
|
|
69
|
+
const segments = cmd.split(/[;&|]+/);
|
|
70
|
+
return segments.some((seg) => {
|
|
71
|
+
const trimmed = seg.trim();
|
|
72
|
+
if (trimmed.startsWith("#")) return false; // 주석
|
|
73
|
+
// 세그먼트의 첫 단어가 psmux인 경우만 실제 호출
|
|
74
|
+
return /^\s*psmux\s+kill-(session|server)\b/i.test(trimmed);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
// 1. BLOCK 체크 — exit 2로 차단
|
|
64
79
|
for (const rule of BLOCK_RULES) {
|
|
80
|
+
if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
|
|
65
81
|
if (rule.pattern.test(command)) {
|
|
66
82
|
process.stderr.write(
|
|
67
83
|
`[triflux safety-guard] BLOCKED: ${rule.reason}\n` +
|