triflux 10.3.4 → 10.7.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/LICENSE +21 -21
- package/bin/tfx-doctor-tui.mjs +1 -1
- package/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-profile.mjs +1 -1
- package/bin/tfx-setup-tui.mjs +1 -1
- package/bin/tfx-setup.mjs +6 -1
- package/bin/triflux.mjs +2396 -1140
- package/hooks/agent-route-guard.mjs +12 -8
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +19 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +108 -24
- package/hooks/hook-registry.json +11 -0
- package/hooks/keyword-rules.json +6 -10
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/pipeline-stop.mjs +3 -6
- package/hooks/safety-guard.mjs +99 -28
- package/hooks/session-start-fast.mjs +143 -0
- package/hooks/subagent-verifier.mjs +5 -4
- package/hub/account-broker.mjs +256 -60
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +95 -57
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +67 -30
- package/hub/bridge.mjs +0 -1
- package/hub/cli-adapter-base.mjs +200 -48
- package/hub/codex-adapter.mjs +76 -96
- package/hub/codex-compat.mjs +3 -3
- package/hub/codex-preflight.mjs +63 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +3 -3
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +5 -5
- package/hub/fullcycle.mjs +33 -17
- package/hub/gemini-adapter.mjs +69 -94
- package/hub/hitl.mjs +89 -30
- package/hub/intent.mjs +161 -38
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/mcp-response-cache.mjs +66 -32
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +35 -37
- package/hub/lib/process-utils.mjs +106 -37
- package/hub/lib/spawn-trace.mjs +527 -0
- package/hub/lib/ssh-command.mjs +34 -4
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +57 -48
- package/hub/promote-penalties.mjs +25 -7
- package/hub/quality/deslop.mjs +70 -49
- package/hub/research.mjs +32 -25
- package/hub/router.mjs +240 -107
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/server.mjs +4 -4
- package/hub/session-fingerprint.mjs +126 -60
- package/hub/state.mjs +84 -43
- package/hub/store-adapter.mjs +59 -26
- package/hub/store.mjs +356 -153
- package/hub/team/agent-map.json +22 -7
- package/hub/team/ansi.mjs +186 -122
- package/hub/team/backend.mjs +28 -10
- package/hub/team/cli/commands/attach.mjs +29 -9
- package/hub/team/cli/commands/control.mjs +29 -8
- package/hub/team/cli/commands/debug.mjs +32 -11
- package/hub/team/cli/commands/focus.mjs +38 -11
- package/hub/team/cli/commands/interrupt.mjs +18 -6
- package/hub/team/cli/commands/kill.mjs +16 -5
- package/hub/team/cli/commands/list.mjs +11 -4
- package/hub/team/cli/commands/send.mjs +19 -6
- package/hub/team/cli/commands/start/index.mjs +154 -31
- package/hub/team/cli/commands/start/parse-args.mjs +38 -11
- package/hub/team/cli/commands/start/start-headless.mjs +112 -36
- package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
- package/hub/team/cli/commands/start/start-mux.mjs +70 -21
- package/hub/team/cli/commands/start/start-wt.mjs +29 -12
- package/hub/team/cli/commands/status.mjs +43 -14
- package/hub/team/cli/commands/stop.mjs +11 -4
- package/hub/team/cli/commands/task.mjs +8 -3
- package/hub/team/cli/commands/tasks.mjs +1 -1
- package/hub/team/cli/index.mjs +2 -2
- package/hub/team/cli/manifest.mjs +38 -8
- package/hub/team/cli/render.mjs +30 -8
- package/hub/team/cli/services/attach-fallback.mjs +31 -11
- package/hub/team/cli/services/hub-client.mjs +42 -14
- package/hub/team/cli/services/member-selector.mjs +11 -4
- package/hub/team/cli/services/native-control.mjs +48 -21
- package/hub/team/cli/services/runtime-mode.mjs +2 -1
- package/hub/team/cli/services/state-store.mjs +25 -8
- package/hub/team/cli/services/task-model.mjs +16 -6
- package/hub/team/conductor-mesh-bridge.mjs +24 -23
- package/hub/team/conductor.mjs +8 -4
- package/hub/team/dashboard-anchor.mjs +4 -5
- package/hub/team/dashboard-layout.mjs +3 -1
- package/hub/team/dashboard-open.mjs +41 -21
- package/hub/team/dashboard.mjs +76 -28
- package/hub/team/event-log.mjs +18 -10
- package/hub/team/handoff.mjs +31 -15
- package/hub/team/headless.mjs +2 -1
- package/hub/team/health-probe.mjs +69 -54
- package/hub/team/launcher-template.mjs +16 -13
- package/hub/team/native-supervisor.mjs +65 -21
- package/hub/team/native.mjs +74 -35
- package/hub/team/nativeProxy.mjs +184 -113
- package/hub/team/notify.mjs +119 -76
- package/hub/team/orchestrator.mjs +9 -4
- package/hub/team/pane.mjs +12 -7
- package/hub/team/process-cleanup.mjs +25 -16
- package/hub/team/psmux.mjs +491 -201
- package/hub/team/remote-probe.mjs +68 -52
- package/hub/team/remote-session.mjs +117 -59
- package/hub/team/remote-watcher.mjs +61 -33
- package/hub/team/routing.mjs +51 -25
- package/hub/team/runtime-strategy.mjs +3 -1
- package/hub/team/session.mjs +98 -34
- package/hub/team/staleState.mjs +72 -30
- package/hub/team/swarm-locks.mjs +15 -13
- package/hub/team/swarm-planner.mjs +32 -21
- package/hub/team/swarm-reconciler.mjs +48 -23
- package/hub/team/tui-lite.mjs +266 -68
- package/hub/team/tui-remote-adapter.mjs +14 -10
- package/hub/team/tui-viewer.mjs +99 -43
- package/hub/team/tui.mjs +708 -271
- package/hub/team/worktree-lifecycle.mjs +152 -58
- package/hub/team/wt-manager.mjs +24 -14
- package/hub/token-mode.mjs +71 -71
- package/hub/tray.mjs +66 -23
- package/hub/workers/claude-worker.mjs +162 -118
- package/hub/workers/codex-mcp.mjs +192 -141
- package/hub/workers/delegator-mcp.mjs +507 -333
- package/hub/workers/factory.mjs +8 -8
- package/hub/workers/gemini-worker.mjs +115 -84
- package/hub/workers/interface.mjs +6 -1
- package/hub/workers/worker-utils.mjs +21 -14
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +162 -26
- package/hud/context-monitor.mjs +82 -41
- package/hud/hud-qos-status.mjs +129 -49
- package/hud/mission-board.mjs +6 -3
- package/hud/providers/claude.mjs +226 -115
- package/hud/providers/codex.mjs +62 -22
- package/hud/providers/gemini.mjs +168 -56
- package/hud/renderers.mjs +384 -119
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +78 -38
- package/mesh/index.mjs +11 -5
- package/mesh/mesh-budget.mjs +18 -9
- package/mesh/mesh-heartbeat.mjs +1 -1
- package/mesh/mesh-queue.mjs +3 -5
- package/mesh/mesh-router.mjs +5 -4
- package/package.json +2 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
- package/scripts/__tests__/keyword-detector.test.mjs +77 -28
- package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
- package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
- package/scripts/__tests__/remote-spawn.test.mjs +10 -4
- package/scripts/__tests__/session-start-fast.test.mjs +36 -0
- package/scripts/__tests__/skill-template.test.mjs +98 -50
- package/scripts/__tests__/smoke.test.mjs +1 -1
- package/scripts/__tests__/spawn-trace.test.mjs +102 -0
- package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
- package/scripts/cache-doctor.mjs +11 -4
- package/scripts/cache-warmup.mjs +96 -37
- package/scripts/claudemd-sync.mjs +27 -17
- package/scripts/codex-gateway-preflight.mjs +52 -37
- package/scripts/codex-mcp-gateway-sync.mjs +59 -39
- package/scripts/completions/tfx.bash +47 -47
- package/scripts/completions/tfx.fish +44 -44
- package/scripts/completions/tfx.zsh +83 -83
- package/scripts/config-audit.mjs +232 -0
- package/scripts/convert-to-tmpl.mjs +54 -0
- package/scripts/cross-review-gate.mjs +35 -12
- package/scripts/cross-review-tracker.mjs +21 -8
- package/scripts/demo.mjs +35 -17
- package/scripts/doctor-diagnose.mjs +284 -0
- package/scripts/gen-skill-docs.mjs +7 -2
- package/scripts/gen-skill-manifest.mjs +2 -1
- package/scripts/headless-guard.mjs +86 -48
- package/scripts/hub-ensure.mjs +45 -26
- package/scripts/keyword-detector.mjs +41 -20
- package/scripts/keyword-rules-expander.mjs +47 -30
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +6 -3
- package/scripts/lib/env-probe.mjs +47 -28
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/keyword-rules.mjs +43 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +377 -239
- package/scripts/lib/mcp-guard-engine.mjs +194 -79
- package/scripts/lib/mcp-manifest.mjs +23 -13
- package/scripts/lib/mcp-server-catalog.mjs +300 -63
- package/scripts/lib/psmux-info.mjs +11 -6
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-template.mjs +30 -7
- package/scripts/mcp-check.mjs +58 -39
- package/scripts/mcp-gateway-config.mjs +83 -39
- package/scripts/mcp-gateway-ensure.mjs +43 -35
- package/scripts/mcp-gateway-integration-test.mjs +70 -58
- package/scripts/mcp-gateway-start.mjs +126 -60
- package/scripts/mcp-gateway-verify.mjs +24 -22
- package/scripts/mcp-safety-guard.mjs +44 -11
- package/scripts/notion-read.mjs +199 -84
- package/scripts/pack.mjs +94 -89
- package/scripts/preflight-cache.mjs +27 -10
- package/scripts/preinstall.mjs +42 -13
- package/scripts/remote-spawn.mjs +309 -94
- package/scripts/run.cjs +8 -5
- package/scripts/session-spawn-helper.mjs +130 -39
- package/scripts/session-stale-cleanup.mjs +123 -0
- package/scripts/setup.mjs +941 -492
- package/scripts/test-lock.mjs +20 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
- package/scripts/tfx-batch-stats.mjs +32 -11
- package/scripts/tfx-gate-activate.mjs +11 -4
- package/scripts/tfx-route-post.mjs +87 -20
- package/scripts/tfx-route-worker.mjs +57 -51
- package/scripts/tfx-route.sh +41 -124
- package/scripts/tmp-cleanup.mjs +21 -7
- package/scripts/token-snapshot.mjs +204 -85
- package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
- package/skills/.omc/state/idle-notif-cooldown.json +3 -0
- package/skills/.omc/state/last-tool-error.json +7 -0
- package/skills/.omc/state/subagent-tracking.json +7 -0
- package/skills/_templates/base.md +1 -6
- package/skills/merge-worktree/SKILL.md.tmpl +144 -0
- package/skills/shared/telemetry-segment.md +6 -0
- package/skills/star-prompt/SKILL.md.tmpl +222 -0
- package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
- package/skills/tfx-analysis/skill.json +1 -6
- package/skills/tfx-auto/SKILL.md +1 -0
- package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
- package/skills/tfx-auto-codex/skill.json +1 -3
- package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
- package/skills/tfx-autopilot/skill.json +1 -5
- package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
- package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
- package/skills/tfx-autoroute/skill.json +1 -7
- package/skills/tfx-codex/SKILL.md +1 -0
- package/skills/tfx-codex/skill.json +1 -3
- package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
- package/skills/tfx-codex-swarm/evals/evals.json +1 -1
- package/skills/tfx-codex-swarm/skill.json +1 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
- package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
- package/skills/tfx-debate/SKILL.md.tmpl +192 -0
- package/skills/tfx-debate/skill.json +1 -7
- package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
- package/skills/tfx-deep-analysis/skill.json +1 -5
- package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
- package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
- package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
- package/skills/tfx-deep-qa/skill.json +1 -6
- package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
- package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
- package/skills/tfx-doctor/SKILL.md +21 -0
- package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
- package/skills/tfx-doctor/skill.json +1 -3
- package/skills/tfx-find/SKILL.md +1 -0
- package/skills/tfx-forge/SKILL.md.tmpl +187 -0
- package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
- package/skills/tfx-fullcycle/skill.json +1 -6
- package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
- package/skills/tfx-gemini/skill.json +1 -3
- package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
- package/skills/tfx-hooks/skill.json +1 -3
- package/skills/tfx-hub/SKILL.md.tmpl +212 -0
- package/skills/tfx-hub/skill.json +1 -3
- package/skills/tfx-index/SKILL.md +1 -0
- package/skills/tfx-index/skill.json +1 -6
- package/skills/tfx-interview/SKILL.md.tmpl +285 -0
- package/skills/tfx-multi/SKILL.md.tmpl +183 -0
- package/skills/tfx-multi/skill.json +1 -3
- package/skills/tfx-panel/SKILL.md.tmpl +189 -0
- package/skills/tfx-panel/skill.json +1 -7
- package/skills/tfx-persist/SKILL.md.tmpl +270 -0
- package/skills/tfx-persist/skill.json +1 -7
- package/skills/tfx-plan/SKILL.md +1 -0
- package/skills/tfx-plan/skill.json +1 -6
- package/skills/tfx-profile/SKILL.md.tmpl +239 -0
- package/skills/tfx-profile/skill.json +1 -3
- package/skills/tfx-prune/SKILL.md.tmpl +200 -0
- package/skills/tfx-prune/skill.json +1 -7
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
- package/skills/tfx-psmux-rules/skill.json +1 -4
- package/skills/tfx-qa/SKILL.md +1 -0
- package/skills/tfx-qa/skill.json +1 -6
- package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
- package/skills/tfx-ralph/skill.json +1 -4
- package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
- package/skills/tfx-remote-setup/skill.json +1 -3
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
- package/skills/tfx-remote-spawn/references/hosts.json +16 -0
- package/skills/tfx-remote-spawn/skill.json +1 -4
- package/skills/tfx-research/SKILL.md +1 -0
- package/skills/tfx-review/SKILL.md +1 -0
- package/skills/tfx-review/skill.json +1 -6
- package/skills/tfx-setup/SKILL.md.tmpl +504 -0
- package/skills/tfx-setup/skill.json +1 -3
- package/skills/tfx-swarm/SKILL.md +22 -0
- package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
- package/tui/codex-profile.mjs +88 -33
- package/tui/core.mjs +45 -15
- package/tui/doctor.mjs +75 -28
- package/tui/gemini-profile.mjs +74 -29
- package/tui/monitor-data.mjs +8 -4
- package/tui/monitor.mjs +71 -27
- package/tui/setup.mjs +133 -42
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
|
|
2
3
|
// hooks/hook-orchestrator.mjs — 범용 훅 체이닝 엔진
|
|
3
4
|
//
|
|
4
5
|
// settings.json에 이벤트당 하나만 등록. stdin JSON에서 이벤트명+툴명을 읽고
|
|
@@ -19,10 +20,11 @@
|
|
|
19
20
|
// CLAUDE_PLUGIN_ROOT — ${PLUGIN_ROOT} 치환용
|
|
20
21
|
// HOME / USERPROFILE — ${HOME} 치환용
|
|
21
22
|
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
24
27
|
import { fileURLToPath } from "node:url";
|
|
25
|
-
import { execFileSync, execFile } from "node:child_process";
|
|
26
28
|
import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
|
|
27
29
|
|
|
28
30
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -84,7 +86,8 @@ function executeHook(hook, stdinData) {
|
|
|
84
86
|
// "bash script.sh" → ["bash", ["script.sh"]]
|
|
85
87
|
// 따옴표 처리 포함
|
|
86
88
|
const parts = parseCommand(cmd);
|
|
87
|
-
if (parts.length === 0)
|
|
89
|
+
if (parts.length === 0)
|
|
90
|
+
return { code: 1, stdout: "", stderr: "empty command" };
|
|
88
91
|
|
|
89
92
|
const [executable, ...args] = parts;
|
|
90
93
|
|
|
@@ -114,20 +117,35 @@ function executeHookAsync(hook, stdinData) {
|
|
|
114
117
|
const cmd = resolveCommand(hook.command);
|
|
115
118
|
const timeout = (hook.timeout || 10) * 1000;
|
|
116
119
|
const parts = parseCommand(cmd);
|
|
117
|
-
if (parts.length === 0) {
|
|
120
|
+
if (parts.length === 0) {
|
|
121
|
+
resolve({ code: 1, stdout: "", stderr: "empty command" });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
118
124
|
const [executable, ...args] = parts;
|
|
119
|
-
const child = execFile(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
const child = execFile(
|
|
126
|
+
executable,
|
|
127
|
+
args,
|
|
128
|
+
{
|
|
129
|
+
timeout,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
cwd: process.cwd(),
|
|
133
|
+
env: { ...process.env },
|
|
134
|
+
},
|
|
135
|
+
(err, stdout, stderr) => {
|
|
136
|
+
if (err)
|
|
137
|
+
resolve({
|
|
138
|
+
code: err.status ?? 1,
|
|
139
|
+
stdout: stdout || "",
|
|
140
|
+
stderr: stderr || "",
|
|
141
|
+
});
|
|
142
|
+
else resolve({ code: 0, stdout: stdout || "", stderr: "" });
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
if (stdinData) {
|
|
146
|
+
child.stdin.write(stdinData);
|
|
147
|
+
child.stdin.end();
|
|
148
|
+
} else child.stdin.end();
|
|
131
149
|
});
|
|
132
150
|
}
|
|
133
151
|
|
|
@@ -170,11 +188,19 @@ function mergeOutputs(accumulated, newOutput) {
|
|
|
170
188
|
|
|
171
189
|
// hookSpecificOutput 머지 — additionalContext는 누적, 나머지는 덮어쓰기
|
|
172
190
|
if (parsed.hookSpecificOutput) {
|
|
173
|
-
if (
|
|
191
|
+
if (
|
|
192
|
+
accumulated.hookSpecificOutput?.additionalContext &&
|
|
193
|
+
parsed.hookSpecificOutput.additionalContext
|
|
194
|
+
) {
|
|
174
195
|
parsed.hookSpecificOutput.additionalContext =
|
|
175
|
-
accumulated.hookSpecificOutput.additionalContext +
|
|
196
|
+
accumulated.hookSpecificOutput.additionalContext +
|
|
197
|
+
"\n" +
|
|
198
|
+
parsed.hookSpecificOutput.additionalContext;
|
|
176
199
|
}
|
|
177
|
-
accumulated.hookSpecificOutput = {
|
|
200
|
+
accumulated.hookSpecificOutput = {
|
|
201
|
+
...accumulated.hookSpecificOutput,
|
|
202
|
+
...parsed.hookSpecificOutput,
|
|
203
|
+
};
|
|
178
204
|
}
|
|
179
205
|
// systemMessage는 누적
|
|
180
206
|
if (parsed.systemMessage) {
|
|
@@ -220,7 +246,12 @@ function recordRouteOutcome(slug, mode, outcome) {
|
|
|
220
246
|
|
|
221
247
|
const weights = existsSync(weightsPath)
|
|
222
248
|
? JSON.parse(readFileSync(weightsPath, "utf8"))
|
|
223
|
-
: {
|
|
249
|
+
: {
|
|
250
|
+
updated_at: null,
|
|
251
|
+
total_routes: 0,
|
|
252
|
+
overrides: 0,
|
|
253
|
+
weights: { mode_bias: {}, profile_bias: {}, depth_bias: {} },
|
|
254
|
+
};
|
|
224
255
|
|
|
225
256
|
weights.total_routes++;
|
|
226
257
|
weights.updated_at = new Date().toISOString();
|
|
@@ -284,6 +315,32 @@ async function main() {
|
|
|
284
315
|
|
|
285
316
|
if (!eventName) process.exit(0);
|
|
286
317
|
|
|
318
|
+
// ── SessionStart fast-path ──
|
|
319
|
+
// TRIFLUX_HOOK_FAST_PATH=false로 비활성화 가능 (rollback)
|
|
320
|
+
if (eventName === "SessionStart" && process.env.TRIFLUX_HOOK_FAST_PATH !== "false") {
|
|
321
|
+
try {
|
|
322
|
+
const { execute } = await import("./session-start-fast.mjs");
|
|
323
|
+
const result = await execute(stdinRaw);
|
|
324
|
+
if (result.stdout?.trim()) {
|
|
325
|
+
const output = { additionalContext: result.stdout.trim() };
|
|
326
|
+
process.stdout.write(JSON.stringify(output));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// external source 훅 (session-vault 등)은 기존 방식으로 실행
|
|
330
|
+
const allHooks = registry.events.SessionStart || [];
|
|
331
|
+
const externalHooks = allHooks.filter((h) => h.enabled !== false && h.source !== "triflux");
|
|
332
|
+
for (const hook of externalHooks) {
|
|
333
|
+
const hookResult = executeHookAsync(hook, stdinRaw);
|
|
334
|
+
hookResult.catch(() => {}); // fire-and-forget for external hooks
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
process.exit(0);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
// fast-path 실패 시 기존 방식으로 폴백
|
|
340
|
+
process.stderr.write(`[orchestrator] fast-path failed, falling back: ${err.message}\n`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
287
344
|
// 이벤트에 해당하는 훅 목록
|
|
288
345
|
const hooks = registry.events[eventName];
|
|
289
346
|
if (!hooks || hooks.length === 0) process.exit(0);
|
|
@@ -326,7 +383,9 @@ async function main() {
|
|
|
326
383
|
}
|
|
327
384
|
} else {
|
|
328
385
|
// 같은 priority 다중 훅 — 비동기 병렬 실행
|
|
329
|
-
const results = await Promise.all(
|
|
386
|
+
const results = await Promise.all(
|
|
387
|
+
group.hooks.map((h) => executeHookAsync(h, stdinRaw)),
|
|
388
|
+
);
|
|
330
389
|
for (const result of results) {
|
|
331
390
|
if (result.code === 2) {
|
|
332
391
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
@@ -340,18 +399,43 @@ async function main() {
|
|
|
340
399
|
}
|
|
341
400
|
}
|
|
342
401
|
|
|
402
|
+
// ── PostToolUse: 컨텍스트 압축 nudge (인라인, 프로세스 추가 없음) ──
|
|
403
|
+
if (eventName === "PostToolUse" && !blocked) {
|
|
404
|
+
try {
|
|
405
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
406
|
+
const snapshotPath = join(home, ".claude", "cache", "tfx-hub", "context-monitor.json");
|
|
407
|
+
const nudgeMarker = join(tmpdir(), "tfx-compact-nudge-sent");
|
|
408
|
+
if (existsSync(snapshotPath) && !existsSync(nudgeMarker)) {
|
|
409
|
+
const snap = JSON.parse(readFileSync(snapshotPath, "utf8"));
|
|
410
|
+
const percent = Number(snap.percent || 0);
|
|
411
|
+
if (percent >= 80) {
|
|
412
|
+
const level = percent >= 90 ? "critical" : "warn";
|
|
413
|
+
const msg = level === "critical"
|
|
414
|
+
? `[context ${percent}%] 컨텍스트 ${percent}% 사용. /compact 또는 에이전트 분할을 강력 권장합니다.`
|
|
415
|
+
: `[context ${percent}%] 컨텍스트 ${percent}% 사용. 마일스톤이면 /compact를 권장합니다.`;
|
|
416
|
+
mergedOutput = mergeOutputs(mergedOutput, JSON.stringify({ systemMessage: msg }));
|
|
417
|
+
if (level === "warn") {
|
|
418
|
+
writeFileSync(nudgeMarker, new Date().toISOString());
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch { /* 컨텍스트 모니터 읽기 실패 무시 */ }
|
|
423
|
+
}
|
|
424
|
+
|
|
343
425
|
// ── PostToolUse:Skill 완료 시 라우팅 가중치 기록 ──
|
|
344
426
|
if (eventName === "PostToolUse" && toolName === "Skill" && !blocked) {
|
|
345
427
|
try {
|
|
346
428
|
const input = JSON.parse(stdinRaw);
|
|
347
429
|
const skillName = input.tool_input?.skill || "";
|
|
348
|
-
if (skillName
|
|
430
|
+
if (skillName?.startsWith("tfx-")) {
|
|
349
431
|
const mode = skillName.replace(/^tfx-/, "");
|
|
350
432
|
const gitRoot = process.env.GIT_WORK_TREE || process.cwd();
|
|
351
433
|
const slug = gitRoot.split(/[\\/]/).pop() || "unknown";
|
|
352
434
|
recordRouteOutcome(slug, mode, "completion");
|
|
353
435
|
}
|
|
354
|
-
} catch {
|
|
436
|
+
} catch {
|
|
437
|
+
/* 가중치 기록 실패 무시 */
|
|
438
|
+
}
|
|
355
439
|
}
|
|
356
440
|
|
|
357
441
|
// 결과 출력
|
package/hooks/hook-registry.json
CHANGED
|
@@ -192,6 +192,17 @@
|
|
|
192
192
|
"blocking": false,
|
|
193
193
|
"description": "supergateway MCP 서비스 헬스체크 및 자동 기동"
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
"id": "tfx-session-stale-cleanup",
|
|
197
|
+
"source": "triflux",
|
|
198
|
+
"matcher": "*",
|
|
199
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/session-stale-cleanup.mjs\"",
|
|
200
|
+
"priority": 5,
|
|
201
|
+
"enabled": true,
|
|
202
|
+
"timeout": 3,
|
|
203
|
+
"blocking": false,
|
|
204
|
+
"description": "이전 세션의 stale tfx-multi 상태 파일 정리 (#62)"
|
|
205
|
+
},
|
|
195
206
|
{
|
|
196
207
|
"id": "ext-session-vault-start",
|
|
197
208
|
"source": "session-vault",
|
package/hooks/keyword-rules.json
CHANGED
|
@@ -11,12 +11,7 @@
|
|
|
11
11
|
"skill": null,
|
|
12
12
|
"action": "suppress_all",
|
|
13
13
|
"priority": 0,
|
|
14
|
-
"supersedes": [
|
|
15
|
-
"tfx-multi",
|
|
16
|
-
"tfx-unified",
|
|
17
|
-
"tfx-codex",
|
|
18
|
-
"tfx-gemini"
|
|
19
|
-
],
|
|
14
|
+
"supersedes": ["tfx-multi", "tfx-unified", "tfx-codex", "tfx-gemini"],
|
|
20
15
|
"exclusive": true,
|
|
21
16
|
"state": null,
|
|
22
17
|
"mcp_route": null
|
|
@@ -62,9 +57,7 @@
|
|
|
62
57
|
],
|
|
63
58
|
"skill": "tfx-swarm",
|
|
64
59
|
"priority": 1,
|
|
65
|
-
"supersedes": [
|
|
66
|
-
"tfx-codex"
|
|
67
|
-
],
|
|
60
|
+
"supersedes": ["tfx-codex"],
|
|
68
61
|
"exclusive": false,
|
|
69
62
|
"state": null,
|
|
70
63
|
"mcp_route": null
|
|
@@ -80,7 +73,10 @@
|
|
|
80
73
|
{ "source": "(?:분석해|계획|설계해)", "flags": "i" },
|
|
81
74
|
{ "source": "(?:찾아봐|조사해|검색해)", "flags": "i" },
|
|
82
75
|
{ "source": "(?:정리해|슬롭|클린업)", "flags": "i" },
|
|
83
|
-
{
|
|
76
|
+
{
|
|
77
|
+
"source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
|
|
78
|
+
"flags": "i"
|
|
79
|
+
}
|
|
84
80
|
],
|
|
85
81
|
"skill": "tfx-auto",
|
|
86
82
|
"priority": 2,
|
|
@@ -51,7 +51,7 @@ export function resolvePluginRoot(callerUrl) {
|
|
|
51
51
|
|
|
52
52
|
const moduleFallback = toPluginRootFromUrl(import.meta.url) || process.cwd();
|
|
53
53
|
process.stderr.write(
|
|
54
|
-
`[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n
|
|
54
|
+
`[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`,
|
|
55
55
|
);
|
|
56
56
|
return moduleFallback;
|
|
57
57
|
}
|
|
@@ -25,10 +25,14 @@ function buildSystemMessage(filePath, stdioServers, result) {
|
|
|
25
25
|
|
|
26
26
|
if (result.modified) {
|
|
27
27
|
const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
|
|
28
|
-
lines.push(
|
|
28
|
+
lines.push(
|
|
29
|
+
`[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`,
|
|
30
|
+
);
|
|
29
31
|
|
|
30
32
|
if (result.replacement?.name && result.replacement?.url) {
|
|
31
|
-
lines.push(
|
|
33
|
+
lines.push(
|
|
34
|
+
`[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`,
|
|
35
|
+
);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (result.backupPath) {
|
package/hooks/pipeline-stop.mjs
CHANGED
|
@@ -11,11 +11,8 @@ let getPipelineStateDbPath;
|
|
|
11
11
|
let ensurePipelineTable;
|
|
12
12
|
let listPipelineStates;
|
|
13
13
|
try {
|
|
14
|
-
({
|
|
15
|
-
|
|
16
|
-
ensurePipelineTable,
|
|
17
|
-
listPipelineStates,
|
|
18
|
-
} = await import("../hub/pipeline/state.mjs"));
|
|
14
|
+
({ getPipelineStateDbPath, ensurePipelineTable, listPipelineStates } =
|
|
15
|
+
await import("../hub/pipeline/state.mjs"));
|
|
19
16
|
} catch {
|
|
20
17
|
// hub/pipeline 모듈 없으면 훅 무동작
|
|
21
18
|
process.exit(0);
|
|
@@ -53,7 +50,7 @@ try {
|
|
|
53
50
|
// 활성 파이프라인 발견 → 구조화 decision으로 block
|
|
54
51
|
const lines = active.map(
|
|
55
52
|
(s) =>
|
|
56
|
-
` - 팀 ${s.team_name}: ${s.phase} 단계 (fix: ${s.fix_attempt}/${s.fix_max}, ralph: ${s.ralph_iteration}/${s.ralph_max})
|
|
53
|
+
` - 팀 ${s.team_name}: ${s.phase} 단계 (fix: ${s.fix_attempt}/${s.fix_max}, ralph: ${s.ralph_iteration}/${s.ralph_max})`,
|
|
57
54
|
);
|
|
58
55
|
|
|
59
56
|
const reason =
|
package/hooks/safety-guard.mjs
CHANGED
|
@@ -8,23 +8,51 @@
|
|
|
8
8
|
// BLOCK (exit 2) — 복구 불가능한 파괴적 명령
|
|
9
9
|
// WARN (allow + context) — 주의가 필요한 명령
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
|
|
14
14
|
// ── 차단 규칙 ──────────────────────────────────────────────
|
|
15
15
|
const BLOCK_RULES = [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
16
|
+
{
|
|
17
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i,
|
|
18
|
+
reason: "루트/홈 디렉토리 rm -rf 차단",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i,
|
|
22
|
+
reason: "현재 디렉토리 rm -rf . 차단",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i,
|
|
26
|
+
reason: "main/master force push 차단",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\bgit\s+push\s+--force\s*$/i,
|
|
30
|
+
reason: "대상 미지정 force push 차단",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /\bgit\s+reset\s+--hard\s+origin\//i,
|
|
34
|
+
reason: "remote reset --hard 차단 — 로컬 작업 소실 위험",
|
|
35
|
+
},
|
|
21
36
|
{ pattern: /\bdrop\s+(table|database|schema)\b/i, reason: "SQL DROP 차단" },
|
|
22
37
|
{ pattern: /\btruncate\s+table\b/i, reason: "SQL TRUNCATE 차단" },
|
|
23
38
|
{ pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
|
|
24
39
|
{ pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
{
|
|
41
|
+
pattern: /\bgit\s+clean\s+.*-fd/i,
|
|
42
|
+
reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern: /\bpsmux\s+kill-session\b/i,
|
|
46
|
+
reason:
|
|
47
|
+
"raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>",
|
|
48
|
+
skipIfGit: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /\bpsmux\s+kill-server\b/i,
|
|
52
|
+
reason:
|
|
53
|
+
"psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용",
|
|
54
|
+
skipIfGit: true,
|
|
55
|
+
},
|
|
28
56
|
];
|
|
29
57
|
|
|
30
58
|
const WT_DIRECT_PATTERNS = [
|
|
@@ -42,12 +70,12 @@ const WT_DIRECT_BLOCK_MESSAGE =
|
|
|
42
70
|
// ── SSH+PowerShell bash 문법 차단 ────────────────────────────
|
|
43
71
|
// 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
|
|
44
72
|
const BASH_SYNTAX_IN_SSH = [
|
|
45
|
-
/2>\/dev\/null/,
|
|
46
|
-
/>\s*\/dev\/null/,
|
|
47
|
-
/&>\s*\/dev\/null/,
|
|
48
|
-
/\$\(/,
|
|
49
|
-
/\bsource\s+/,
|
|
50
|
-
/\bexport\s+\w+=/,
|
|
73
|
+
/2>\/dev\/null/, // 2>/dev/null → PowerShell에서 Out-File C:\dev\null
|
|
74
|
+
/>\s*\/dev\/null/, // >/dev/null
|
|
75
|
+
/&>\s*\/dev\/null/, // &>/dev/null
|
|
76
|
+
/\$\(/, // $(cmd) → PowerShell에서 다른 의미
|
|
77
|
+
/\bsource\s+/, // source → PowerShell에 없음
|
|
78
|
+
/\bexport\s+\w+=/, // export VAR= → PowerShell에 없음
|
|
51
79
|
];
|
|
52
80
|
|
|
53
81
|
const SSH_POWERSHELL_HINT =
|
|
@@ -56,28 +84,59 @@ const SSH_POWERSHELL_HINT =
|
|
|
56
84
|
|
|
57
85
|
// ── 경고 규칙 ──────────────────────────────────────────────
|
|
58
86
|
const WARN_RULES = [
|
|
59
|
-
{
|
|
60
|
-
|
|
87
|
+
{
|
|
88
|
+
pattern: /\bgit\s+push\b(?!.*--force)/i,
|
|
89
|
+
warn: "git push 감지. 원격 저장소에 반영됩니다.",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /\bgit\s+rebase\b/i,
|
|
93
|
+
warn: "git rebase 감지. 커밋 히스토리가 변경됩니다.",
|
|
94
|
+
},
|
|
61
95
|
{ pattern: /\bgit\s+branch\s+-[dD]\b/i, warn: "브랜치 삭제 감지." },
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
{
|
|
97
|
+
pattern: /\bnpm\s+publish\b/i,
|
|
98
|
+
warn: "npm publish 감지. 공개 레지스트리에 배포됩니다.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
pattern: /\brm\s+(-[^\s]*)?-rf?\s/i,
|
|
102
|
+
warn: "재귀 삭제 감지. 대상을 확인하세요.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
pattern: /--no-verify\b/i,
|
|
106
|
+
warn: "--no-verify 감지. 훅 건너뛰기는 권장하지 않습니다.",
|
|
107
|
+
},
|
|
65
108
|
{ pattern: /\bchmod\s+777\b/i, warn: "chmod 777 감지. 보안 위험." },
|
|
66
|
-
{
|
|
109
|
+
{
|
|
110
|
+
pattern: /\bcurl\s.*\|\s*(bash|sh)\b/i,
|
|
111
|
+
warn: "curl | sh 감지. 원격 스크립트 실행 주의.",
|
|
112
|
+
},
|
|
67
113
|
];
|
|
68
114
|
|
|
69
115
|
// ── reflexion 적응형 패널티 로드 ──────────────────────────────
|
|
70
116
|
function loadReflexionPenalties() {
|
|
71
117
|
try {
|
|
72
118
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
73
|
-
const penaltyFile = join(
|
|
119
|
+
const penaltyFile = join(
|
|
120
|
+
home,
|
|
121
|
+
".triflux",
|
|
122
|
+
"reflexion",
|
|
123
|
+
"pending-penalties.jsonl",
|
|
124
|
+
);
|
|
74
125
|
if (!existsSync(penaltyFile)) return [];
|
|
75
126
|
return readFileSync(penaltyFile, "utf8")
|
|
76
127
|
.split("\n")
|
|
77
128
|
.filter(Boolean)
|
|
78
|
-
.map(
|
|
129
|
+
.map((line) => {
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(line);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
})
|
|
79
136
|
.filter(Boolean);
|
|
80
|
-
} catch {
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
81
140
|
}
|
|
82
141
|
|
|
83
142
|
function readStdin() {
|
|
@@ -89,7 +148,11 @@ function readStdin() {
|
|
|
89
148
|
}
|
|
90
149
|
|
|
91
150
|
function shouldSkipSegment(segment) {
|
|
92
|
-
return
|
|
151
|
+
return (
|
|
152
|
+
!segment ||
|
|
153
|
+
segment.startsWith("#") ||
|
|
154
|
+
/^\s*(echo|printf|grep|git\s+commit)\b/i.test(segment)
|
|
155
|
+
);
|
|
93
156
|
}
|
|
94
157
|
|
|
95
158
|
function hasSegmentInvocation(cmd, patterns) {
|
|
@@ -122,7 +185,7 @@ function blockCommand(message, command) {
|
|
|
122
185
|
process.stderr.write(
|
|
123
186
|
`${message}\n` +
|
|
124
187
|
`명령어: ${command.slice(0, 120)}${command.length > 120 ? "..." : ""}\n` +
|
|
125
|
-
"이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요."
|
|
188
|
+
"이 명령은 실행할 수 없습니다. 안전한 대안을 사용하세요.",
|
|
126
189
|
);
|
|
127
190
|
process.exit(2);
|
|
128
191
|
}
|
|
@@ -161,7 +224,15 @@ function main() {
|
|
|
161
224
|
const penalties = loadReflexionPenalties();
|
|
162
225
|
if (penalties.length > 0) {
|
|
163
226
|
for (const penalty of penalties) {
|
|
164
|
-
if (
|
|
227
|
+
if (
|
|
228
|
+
penalty.error_pattern &&
|
|
229
|
+
new RegExp(
|
|
230
|
+
penalty.error_pattern
|
|
231
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
232
|
+
.slice(0, 80),
|
|
233
|
+
"i",
|
|
234
|
+
).test(command)
|
|
235
|
+
) {
|
|
165
236
|
const output = {
|
|
166
237
|
hookSpecificOutput: {
|
|
167
238
|
hookEventName: "PreToolUse",
|
|
@@ -186,7 +257,7 @@ function main() {
|
|
|
186
257
|
const sshMatch = seg.trim().match(/^ssh\s+\S+\s+(.*)/s);
|
|
187
258
|
if (!sshMatch) continue;
|
|
188
259
|
const sshPayload = sshMatch[1];
|
|
189
|
-
const bashSyntax = BASH_SYNTAX_IN_SSH.find(p => p.test(sshPayload));
|
|
260
|
+
const bashSyntax = BASH_SYNTAX_IN_SSH.find((p) => p.test(sshPayload));
|
|
190
261
|
if (bashSyntax) {
|
|
191
262
|
blockCommand(
|
|
192
263
|
`[safety-guard] SSH 명령에 bash 전용 문법 감지: ${bashSyntax}. ${SSH_POWERSHELL_HINT}`,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// hooks/session-start-fast.mjs — SessionStart in-process fast-path
|
|
2
|
+
//
|
|
3
|
+
// hook-orchestrator.mjs가 SessionStart 이벤트일 때 이 모듈을 dynamic import.
|
|
4
|
+
// 6개 훅을 1개 node 프로세스 안에서 실행하여 콜드스타트 7회 → 1회로 줄인다.
|
|
5
|
+
//
|
|
6
|
+
// 분류:
|
|
7
|
+
// BLOCKING (직렬, stdout 반환 전 완료): setup.runCritical, mcp-safety-guard.run
|
|
8
|
+
// DEFERRED (병렬, 실패해도 안 죽음): hub-ensure.run, mcp-gateway-ensure.run, setup.runDeferred
|
|
9
|
+
// BACKGROUND (fire-and-forget): preflight-cache.run
|
|
10
|
+
//
|
|
11
|
+
// external source 훅 (session-vault 등)은 여전히 execFile로 실행된다.
|
|
12
|
+
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { createModuleLogger } from "../scripts/lib/logger.mjs";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const SCRIPTS = join(__dirname, "..", "scripts");
|
|
19
|
+
|
|
20
|
+
const log = createModuleLogger("session-start-fast");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* BLOCKING 훅을 순차 실행. 하나라도 throw하면 로그만 남기고 계속.
|
|
24
|
+
* @param {string} stdinData
|
|
25
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
26
|
+
*/
|
|
27
|
+
async function runBlocking(stdinData) {
|
|
28
|
+
const output = { stdout: "", stderr: "" };
|
|
29
|
+
const timings = [];
|
|
30
|
+
|
|
31
|
+
// 1. setup.runCritical — 환경 초기화 필수
|
|
32
|
+
try {
|
|
33
|
+
const t0 = performance.now();
|
|
34
|
+
const setup = await import(join(SCRIPTS, "setup.mjs"));
|
|
35
|
+
const result = await setup.runCritical(stdinData);
|
|
36
|
+
const dur = performance.now() - t0;
|
|
37
|
+
timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
|
|
38
|
+
if (result?.stdout) output.stdout += result.stdout + "\n";
|
|
39
|
+
if (result?.stderr) output.stderr += result.stderr + "\n";
|
|
40
|
+
log.info({ hook: "setup.critical", dur_ms: Math.round(dur) }, "hook.completed");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error({ hook: "setup.critical", err: String(err.message || err) }, "hook.failed");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. mcp-safety-guard.run — EPERM 방지
|
|
46
|
+
try {
|
|
47
|
+
const t0 = performance.now();
|
|
48
|
+
const guard = await import(join(SCRIPTS, "mcp-safety-guard.mjs"));
|
|
49
|
+
guard.run();
|
|
50
|
+
const dur = performance.now() - t0;
|
|
51
|
+
timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
|
|
52
|
+
log.info({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) }, "hook.completed");
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.error({ hook: "mcp-safety-guard", err: String(err.message || err) }, "hook.failed");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { ...output, timings };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* DEFERRED 훅을 병렬 실행. 실패해도 crash 안 함, 로그만 남김.
|
|
62
|
+
* Promise는 의도적으로 관리하지 않음 (fire-and-forget with logging).
|
|
63
|
+
* @param {string} stdinData
|
|
64
|
+
*/
|
|
65
|
+
function runDeferred(stdinData) {
|
|
66
|
+
const tasks = [
|
|
67
|
+
{
|
|
68
|
+
name: "hub-ensure",
|
|
69
|
+
fn: async () => {
|
|
70
|
+
const mod = await import(join(SCRIPTS, "hub-ensure.mjs"));
|
|
71
|
+
return mod.run(stdinData);
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "mcp-gateway-ensure",
|
|
76
|
+
fn: async () => {
|
|
77
|
+
const mod = await import(join(SCRIPTS, "mcp-gateway-ensure.mjs"));
|
|
78
|
+
return mod.run(stdinData);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "setup.deferred",
|
|
83
|
+
fn: async () => {
|
|
84
|
+
const mod = await import(join(SCRIPTS, "setup.mjs"));
|
|
85
|
+
return mod.runDeferred(stdinData);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const task of tasks) {
|
|
91
|
+
const t0 = performance.now();
|
|
92
|
+
task.fn()
|
|
93
|
+
.then((result) => {
|
|
94
|
+
const dur = performance.now() - t0;
|
|
95
|
+
log.info({ hook: task.name, dur_ms: Math.round(dur), code: result?.code }, "deferred.completed");
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
const dur = performance.now() - t0;
|
|
99
|
+
log.error({ hook: task.name, dur_ms: Math.round(dur), err: String(err.message || err) }, "deferred.failed");
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* BACKGROUND 훅. fire-and-forget.
|
|
106
|
+
* @param {string} stdinData
|
|
107
|
+
*/
|
|
108
|
+
function runBackground(stdinData) {
|
|
109
|
+
// preflight-cache
|
|
110
|
+
import(join(SCRIPTS, "preflight-cache.mjs"))
|
|
111
|
+
.then((mod) => mod.run(stdinData))
|
|
112
|
+
.catch(() => {}); // 완전 무시
|
|
113
|
+
|
|
114
|
+
// session-vault은 external source — hook-orchestrator가 execFile로 실행
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* SessionStart fast-path 진입점.
|
|
119
|
+
* hook-orchestrator.mjs에서 호출된다.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} stdinData — orchestrator가 전달하는 stdin JSON
|
|
122
|
+
* @param {Array} externalHooks — source !== 'triflux'인 훅 목록 (orchestrator가 execFile로 실행)
|
|
123
|
+
* @returns {Promise<{stdout: string, stderr: string, timings: Array}>}
|
|
124
|
+
*/
|
|
125
|
+
export async function execute(stdinData, externalHooks = []) {
|
|
126
|
+
const totalStart = performance.now();
|
|
127
|
+
|
|
128
|
+
// BLOCKING: 프롬프트 전 완료 필수
|
|
129
|
+
const blocking = await runBlocking(stdinData);
|
|
130
|
+
|
|
131
|
+
// DEFERRED + BACKGROUND: fire-and-forget
|
|
132
|
+
runDeferred(stdinData);
|
|
133
|
+
runBackground(stdinData);
|
|
134
|
+
|
|
135
|
+
const totalDur = performance.now() - totalStart;
|
|
136
|
+
log.info({ total_ms: Math.round(totalDur), blocking_count: 2, deferred_count: 3, bg_count: 1 }, "session-start.done");
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
stdout: blocking.stdout,
|
|
140
|
+
stderr: blocking.stderr,
|
|
141
|
+
timings: blocking.timings,
|
|
142
|
+
};
|
|
143
|
+
}
|