@triflux/core 10.1.0 → 10.25.1
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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- 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 +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- 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/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// hooks/session-end-cleanup.mjs — SessionEnd lifecycle state reporter
|
|
4
|
+
//
|
|
5
|
+
// Report-only by default. Opt-in cleanup only removes the repo-local
|
|
6
|
+
// .triflux/subagents/subagents.json file when stale running lifecycle state is
|
|
7
|
+
// present. It never kills processes or touches external HOME/MCP configs.
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const MARKER = "[session-end-cleanup]";
|
|
14
|
+
const STALE_MS = 30 * 60 * 1000;
|
|
15
|
+
const MAX_OUTPUT_BYTES = 2 * 1024;
|
|
16
|
+
const DEFAULT_TMUX_PREFIX = "tfx-*";
|
|
17
|
+
const MAX_TMUX_REPORT_SESSIONS = 8;
|
|
18
|
+
|
|
19
|
+
function readStdin() {
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(0, "utf8");
|
|
22
|
+
} catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseJson(raw) {
|
|
28
|
+
if (!raw?.trim()) return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function timestampMs(agent) {
|
|
37
|
+
const value =
|
|
38
|
+
agent?.startedAt ??
|
|
39
|
+
agent?.started_at ??
|
|
40
|
+
agent?.createdAt ??
|
|
41
|
+
agent?.created_at ??
|
|
42
|
+
agent?.timestamp;
|
|
43
|
+
const parsed = Date.parse(String(value ?? ""));
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isCompleted(agent) {
|
|
48
|
+
return (
|
|
49
|
+
Boolean(
|
|
50
|
+
agent?.stoppedAt ??
|
|
51
|
+
agent?.stopped_at ??
|
|
52
|
+
agent?.completedAt ??
|
|
53
|
+
agent?.completed_at ??
|
|
54
|
+
agent?.endedAt ??
|
|
55
|
+
agent?.ended_at ??
|
|
56
|
+
agent?.durationMs ??
|
|
57
|
+
agent?.duration_ms ??
|
|
58
|
+
agent?.result,
|
|
59
|
+
) ||
|
|
60
|
+
agent?.status === "completed" ||
|
|
61
|
+
agent?.status === "stopped"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function agentEntries(state) {
|
|
66
|
+
const agents = state?.agents;
|
|
67
|
+
if (Array.isArray(agents)) return agents;
|
|
68
|
+
if (agents && typeof agents === "object") return Object.values(agents);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compileSimpleGlob(pattern) {
|
|
73
|
+
const source = String(pattern || DEFAULT_TMUX_PREFIX)
|
|
74
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
75
|
+
.replace(/\*/g, ".*");
|
|
76
|
+
return new RegExp(`^${source}$`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatAge(ageSec) {
|
|
80
|
+
if (!Number.isFinite(ageSec) || ageSec < 0) return "unknown";
|
|
81
|
+
if (ageSec < 60) return `${Math.floor(ageSec)}s`;
|
|
82
|
+
if (ageSec < 3600) return `${Math.floor(ageSec / 60)}m`;
|
|
83
|
+
if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}h`;
|
|
84
|
+
return `${Math.floor(ageSec / 86400)}d`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseSessionCreated(rawValue) {
|
|
88
|
+
const parsed = Number.parseInt(String(rawValue || "").trim(), 10);
|
|
89
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseTmuxListSessions(output, nowSec) {
|
|
93
|
+
const sessions = [];
|
|
94
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
95
|
+
const [name, attachedText, createdText] = line.split("\t");
|
|
96
|
+
if (!name) continue;
|
|
97
|
+
const attached = Number.parseInt(attachedText || "0", 10);
|
|
98
|
+
const created = parseSessionCreated(createdText);
|
|
99
|
+
sessions.push({
|
|
100
|
+
name,
|
|
101
|
+
attached: Number.isFinite(attached) ? attached : 0,
|
|
102
|
+
ageSec: created == null ? null : Math.max(0, nowSec - created),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return sessions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseFirstPane(output) {
|
|
109
|
+
const first = String(output || "")
|
|
110
|
+
.split(/\r?\n/)
|
|
111
|
+
.find((line) => line.trim());
|
|
112
|
+
if (!first) return { cwd: null, command: null, pid: null };
|
|
113
|
+
const [cwd, command, pidText] = first.split("\t");
|
|
114
|
+
const pid = Number.parseInt(pidText || "", 10);
|
|
115
|
+
return {
|
|
116
|
+
cwd: cwd || null,
|
|
117
|
+
command: command || null,
|
|
118
|
+
pid: Number.isFinite(pid) && pid > 0 ? pid : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function queryMemoryEstimateMb(pid, execFile = execFileSync) {
|
|
123
|
+
if (!pid) return null;
|
|
124
|
+
try {
|
|
125
|
+
const output = execFile("ps", ["-o", "rss=", "-p", String(pid)], {
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
timeout: 400,
|
|
128
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
windowsHide: true,
|
|
130
|
+
});
|
|
131
|
+
const rssKb = Number.parseInt(String(output || "").trim(), 10);
|
|
132
|
+
if (!Number.isFinite(rssKb) || rssKb < 0) return null;
|
|
133
|
+
return Math.round(rssKb / 1024);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function inspectDetachedTmuxSessions({
|
|
140
|
+
prefix = DEFAULT_TMUX_PREFIX,
|
|
141
|
+
now = Date.now(),
|
|
142
|
+
execFile = execFileSync,
|
|
143
|
+
limit = MAX_TMUX_REPORT_SESSIONS,
|
|
144
|
+
} = {}) {
|
|
145
|
+
const nowSec = Math.floor(now / 1000);
|
|
146
|
+
const matcher = compileSimpleGlob(prefix);
|
|
147
|
+
let output = "";
|
|
148
|
+
try {
|
|
149
|
+
output = execFile(
|
|
150
|
+
"tmux",
|
|
151
|
+
[
|
|
152
|
+
"list-sessions",
|
|
153
|
+
"-F",
|
|
154
|
+
"#{session_name}\t#{session_attached}\t#{session_created}",
|
|
155
|
+
],
|
|
156
|
+
{
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
timeout: 500,
|
|
159
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
160
|
+
windowsHide: true,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
return { available: false, prefix, sessions: [] };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sessions = [];
|
|
168
|
+
for (const session of parseTmuxListSessions(output, nowSec)) {
|
|
169
|
+
if (session.attached !== 0 || !matcher.test(session.name)) continue;
|
|
170
|
+
let pane = { cwd: null, command: null, pid: null };
|
|
171
|
+
try {
|
|
172
|
+
pane = parseFirstPane(
|
|
173
|
+
execFile(
|
|
174
|
+
"tmux",
|
|
175
|
+
[
|
|
176
|
+
"list-panes",
|
|
177
|
+
"-t",
|
|
178
|
+
session.name,
|
|
179
|
+
"-F",
|
|
180
|
+
"#{pane_current_path}\t#{pane_current_command}\t#{pane_pid}",
|
|
181
|
+
],
|
|
182
|
+
{
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
timeout: 500,
|
|
185
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
186
|
+
windowsHide: true,
|
|
187
|
+
},
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
} catch {
|
|
191
|
+
pane = { cwd: null, command: null, pid: null };
|
|
192
|
+
}
|
|
193
|
+
sessions.push({
|
|
194
|
+
name: session.name,
|
|
195
|
+
age: formatAge(session.ageSec),
|
|
196
|
+
cwd: pane.cwd,
|
|
197
|
+
command: pane.command,
|
|
198
|
+
memory_estimate_mb: queryMemoryEstimateMb(pane.pid, execFile),
|
|
199
|
+
});
|
|
200
|
+
if (sessions.length >= limit) break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { available: true, prefix, sessions };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function summarizeLifecycleState(state, now = Date.now()) {
|
|
207
|
+
const completedEntries = Array.isArray(state?.completed)
|
|
208
|
+
? state.completed
|
|
209
|
+
: [];
|
|
210
|
+
const summary = {
|
|
211
|
+
running: 0,
|
|
212
|
+
completed: completedEntries.length,
|
|
213
|
+
stale: 0,
|
|
214
|
+
total: completedEntries.length,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const agent of agentEntries(state)) {
|
|
218
|
+
if (!agent || typeof agent !== "object") continue;
|
|
219
|
+
summary.total += 1;
|
|
220
|
+
|
|
221
|
+
if (isCompleted(agent)) {
|
|
222
|
+
summary.completed += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const started = timestampMs(agent);
|
|
227
|
+
if (started !== null && now - started >= STALE_MS) {
|
|
228
|
+
summary.stale += 1;
|
|
229
|
+
} else {
|
|
230
|
+
summary.running += 1;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return summary;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pruneStaleRunningAgents(state, now = Date.now()) {
|
|
238
|
+
const agents = state?.agents;
|
|
239
|
+
if (!agents || typeof agents !== "object" || Array.isArray(agents)) {
|
|
240
|
+
return { nextState: state, removed: 0 };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let removed = 0;
|
|
244
|
+
const nextAgents = {};
|
|
245
|
+
for (const [key, agent] of Object.entries(agents)) {
|
|
246
|
+
const started = timestampMs(agent);
|
|
247
|
+
const stale =
|
|
248
|
+
!isCompleted(agent) && started !== null && now - started >= STALE_MS;
|
|
249
|
+
if (stale) {
|
|
250
|
+
removed += 1;
|
|
251
|
+
} else {
|
|
252
|
+
nextAgents[key] = agent;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
nextState: { ...state, agents: nextAgents },
|
|
258
|
+
removed,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildContext(summary, cleanup, tmuxReport) {
|
|
263
|
+
let context =
|
|
264
|
+
`${MARKER} subagent lifecycle: ` +
|
|
265
|
+
`running=${summary.running} completed=${summary.completed} ` +
|
|
266
|
+
`stale=${summary.stale} total=${summary.total} cleanup=${cleanup}`;
|
|
267
|
+
|
|
268
|
+
if (tmuxReport?.sessions?.length > 0) {
|
|
269
|
+
context += ` detached_tmux_sessions=${JSON.stringify(tmuxReport.sessions)}`;
|
|
270
|
+
}
|
|
271
|
+
return context;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function buildOutput(summary, cleanup, tmuxReport) {
|
|
275
|
+
const context = buildContext(summary, cleanup, tmuxReport);
|
|
276
|
+
const output = {
|
|
277
|
+
systemMessage: context,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
let json = JSON.stringify(output);
|
|
281
|
+
if (Buffer.byteLength(json, "utf8") >= MAX_OUTPUT_BYTES) {
|
|
282
|
+
const bounded = buildContext(summary, cleanup, {
|
|
283
|
+
sessions: tmuxReport?.sessions?.slice(0, 2) || [],
|
|
284
|
+
});
|
|
285
|
+
json = JSON.stringify({
|
|
286
|
+
systemMessage: bounded,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return json;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function run(input, env = process.env) {
|
|
293
|
+
if (input?.hook_event_name !== "SessionEnd") return "";
|
|
294
|
+
|
|
295
|
+
const root = env.CLAUDE_CWD || process.cwd();
|
|
296
|
+
const statePath = join(root, ".triflux", "subagents", "subagents.json");
|
|
297
|
+
if (!existsSync(statePath)) return "";
|
|
298
|
+
|
|
299
|
+
const state = parseJson(readFileSync(statePath, "utf8"));
|
|
300
|
+
if (!state) return "";
|
|
301
|
+
|
|
302
|
+
const summary = summarizeLifecycleState(state);
|
|
303
|
+
let cleanup = "report-only";
|
|
304
|
+
const tmuxReport = inspectDetachedTmuxSessions({
|
|
305
|
+
prefix: env.TFX_SESSION_END_TMUX_PREFIX || DEFAULT_TMUX_PREFIX,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (env.TFX_SESSION_END_CLEANUP === "1" && summary.stale > 0) {
|
|
309
|
+
try {
|
|
310
|
+
const { nextState, removed } = pruneStaleRunningAgents(state);
|
|
311
|
+
const remainingAgents = Object.keys(nextState?.agents || {}).length;
|
|
312
|
+
const remainingCompleted = Array.isArray(nextState?.completed)
|
|
313
|
+
? nextState.completed.length
|
|
314
|
+
: 0;
|
|
315
|
+
if (remainingAgents === 0 && remainingCompleted === 0) {
|
|
316
|
+
unlinkSync(statePath);
|
|
317
|
+
cleanup = "removed";
|
|
318
|
+
} else {
|
|
319
|
+
writeFileSync(
|
|
320
|
+
statePath,
|
|
321
|
+
`${JSON.stringify({ ...nextState, updatedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
322
|
+
"utf8",
|
|
323
|
+
);
|
|
324
|
+
cleanup = `pruned-${removed}`;
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
cleanup = "failed";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return buildOutput(summary, cleanup, tmuxReport);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function main() {
|
|
335
|
+
const input = parseJson(readStdin());
|
|
336
|
+
if (!input) return;
|
|
337
|
+
const output = run(input);
|
|
338
|
+
if (output) process.stdout.write(output);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
main();
|
|
343
|
+
} catch {
|
|
344
|
+
process.exit(0);
|
|
345
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
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, hub-ensure.run
|
|
8
|
+
// DEFERRED (병렬, 실패해도 안 죽음): 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, pathToFileURL } 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
|
+
const importMod = (p) => import(pathToFileURL(p).href);
|
|
20
|
+
|
|
21
|
+
const log = createModuleLogger("session-start-fast");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* BLOCKING 훅을 순차 실행. 하나라도 throw하면 로그만 남기고 계속.
|
|
25
|
+
* @param {string} stdinData
|
|
26
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
27
|
+
*/
|
|
28
|
+
async function runBlocking(stdinData) {
|
|
29
|
+
const output = { stdout: "", stderr: "" };
|
|
30
|
+
const timings = [];
|
|
31
|
+
|
|
32
|
+
// 1. setup.runCritical — 환경 초기화 필수
|
|
33
|
+
try {
|
|
34
|
+
const t0 = performance.now();
|
|
35
|
+
const setup = await importMod(join(SCRIPTS, "setup.mjs"));
|
|
36
|
+
const result = await setup.runCritical(stdinData);
|
|
37
|
+
const dur = performance.now() - t0;
|
|
38
|
+
timings.push({ hook: "setup.critical", dur_ms: Math.round(dur) });
|
|
39
|
+
if (result?.stdout) output.stdout += result.stdout + "\n";
|
|
40
|
+
if (result?.stderr) output.stderr += result.stderr + "\n";
|
|
41
|
+
log.info(
|
|
42
|
+
{ hook: "setup.critical", dur_ms: Math.round(dur) },
|
|
43
|
+
"hook.completed",
|
|
44
|
+
);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
log.error(
|
|
47
|
+
{ hook: "setup.critical", err: String(err.message || err) },
|
|
48
|
+
"hook.failed",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. mcp-safety-guard.run — EPERM 방지
|
|
53
|
+
try {
|
|
54
|
+
const t0 = performance.now();
|
|
55
|
+
const guard = await importMod(join(SCRIPTS, "mcp-safety-guard.mjs"));
|
|
56
|
+
guard.run();
|
|
57
|
+
const dur = performance.now() - t0;
|
|
58
|
+
timings.push({ hook: "mcp-safety-guard", dur_ms: Math.round(dur) });
|
|
59
|
+
log.info(
|
|
60
|
+
{ hook: "mcp-safety-guard", dur_ms: Math.round(dur) },
|
|
61
|
+
"hook.completed",
|
|
62
|
+
);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
log.error(
|
|
65
|
+
{ hook: "mcp-safety-guard", err: String(err.message || err) },
|
|
66
|
+
"hook.failed",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. hub-ensure — Hub 필수 인프라, BLOCKING으로 실행
|
|
71
|
+
try {
|
|
72
|
+
const t0 = performance.now();
|
|
73
|
+
const hubMod = await importMod(join(SCRIPTS, "hub-ensure.mjs"));
|
|
74
|
+
const result = await hubMod.run(stdinData);
|
|
75
|
+
const dur = performance.now() - t0;
|
|
76
|
+
timings.push({ hook: "hub-ensure", dur_ms: Math.round(dur) });
|
|
77
|
+
if (result?.stdout) output.stdout += result.stdout + "\n";
|
|
78
|
+
if (result?.stderr) output.stderr += result.stderr + "\n";
|
|
79
|
+
if (result?.code !== 0) {
|
|
80
|
+
log.warn(
|
|
81
|
+
{ hook: "hub-ensure", dur_ms: Math.round(dur), code: result?.code },
|
|
82
|
+
"hook.warn",
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
log.info(
|
|
86
|
+
{ hook: "hub-ensure", dur_ms: Math.round(dur) },
|
|
87
|
+
"hook.completed",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.error(
|
|
92
|
+
{ hook: "hub-ensure", err: String(err.message || err) },
|
|
93
|
+
"hook.failed",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { ...output, timings };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* DEFERRED 훅을 병렬 실행. 실패해도 crash 안 함, 로그만 남김.
|
|
102
|
+
* Promise는 의도적으로 관리하지 않음 (fire-and-forget with logging).
|
|
103
|
+
* @param {string} stdinData
|
|
104
|
+
*/
|
|
105
|
+
function runDeferred(stdinData) {
|
|
106
|
+
const tasks = [
|
|
107
|
+
{
|
|
108
|
+
name: "session-stale-cleanup",
|
|
109
|
+
fn: async () => {
|
|
110
|
+
const mod = await importMod(join(SCRIPTS, "session-stale-cleanup.mjs"));
|
|
111
|
+
if (typeof mod.main === "function") mod.main();
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "claude-login-detect",
|
|
116
|
+
fn: async () => {
|
|
117
|
+
const mod = await importMod(join(SCRIPTS, "claude-login-detect.mjs"));
|
|
118
|
+
const result = mod.run?.();
|
|
119
|
+
if (result?.changed) {
|
|
120
|
+
return {
|
|
121
|
+
stdout: `[claude-login] HUD 캐시 ${result.cleared}개 초기화됨\n`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "mcp-gateway-ensure",
|
|
128
|
+
fn: async () => {
|
|
129
|
+
const mod = await importMod(join(SCRIPTS, "mcp-gateway-ensure.mjs"));
|
|
130
|
+
return mod.run(stdinData);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "setup.deferred",
|
|
135
|
+
fn: async () => {
|
|
136
|
+
const mod = await importMod(join(SCRIPTS, "setup.mjs"));
|
|
137
|
+
return mod.runDeferred(stdinData);
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const task of tasks) {
|
|
143
|
+
const t0 = performance.now();
|
|
144
|
+
task
|
|
145
|
+
.fn()
|
|
146
|
+
.then((result) => {
|
|
147
|
+
const dur = performance.now() - t0;
|
|
148
|
+
log.info(
|
|
149
|
+
{ hook: task.name, dur_ms: Math.round(dur), code: result?.code },
|
|
150
|
+
"deferred.completed",
|
|
151
|
+
);
|
|
152
|
+
})
|
|
153
|
+
.catch((err) => {
|
|
154
|
+
const dur = performance.now() - t0;
|
|
155
|
+
log.error(
|
|
156
|
+
{
|
|
157
|
+
hook: task.name,
|
|
158
|
+
dur_ms: Math.round(dur),
|
|
159
|
+
err: String(err.message || err),
|
|
160
|
+
},
|
|
161
|
+
"deferred.failed",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* BACKGROUND 훅. fire-and-forget.
|
|
169
|
+
* @param {string} stdinData
|
|
170
|
+
*/
|
|
171
|
+
function runBackground(stdinData) {
|
|
172
|
+
// preflight-cache
|
|
173
|
+
importMod(join(SCRIPTS, "preflight-cache.mjs"))
|
|
174
|
+
.then((mod) => mod.run(stdinData))
|
|
175
|
+
.catch(() => {}); // 완전 무시
|
|
176
|
+
|
|
177
|
+
// session-vault은 external source — hook-orchestrator가 execFile로 실행
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* SessionStart fast-path 진입점.
|
|
182
|
+
* hook-orchestrator.mjs에서 호출된다.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} stdinData — orchestrator가 전달하는 stdin JSON
|
|
185
|
+
* @param {Array} externalHooks — source !== 'triflux'인 훅 목록 (orchestrator가 execFile로 실행)
|
|
186
|
+
* @returns {Promise<{stdout: string, stderr: string, timings: Array}>}
|
|
187
|
+
*/
|
|
188
|
+
export async function execute(stdinData, externalHooks = []) {
|
|
189
|
+
const totalStart = performance.now();
|
|
190
|
+
|
|
191
|
+
// BLOCKING: 프롬프트 전 완료 필수
|
|
192
|
+
const blocking = await runBlocking(stdinData);
|
|
193
|
+
|
|
194
|
+
// DEFERRED + BACKGROUND: fire-and-forget
|
|
195
|
+
runDeferred(stdinData);
|
|
196
|
+
runBackground(stdinData);
|
|
197
|
+
|
|
198
|
+
const totalDur = performance.now() - totalStart;
|
|
199
|
+
log.info(
|
|
200
|
+
{
|
|
201
|
+
total_ms: Math.round(totalDur),
|
|
202
|
+
blocking_count: 3,
|
|
203
|
+
deferred_count: 2,
|
|
204
|
+
bg_count: 1,
|
|
205
|
+
},
|
|
206
|
+
"session-start.done",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
stdout: blocking.stdout,
|
|
211
|
+
stderr: blocking.stderr,
|
|
212
|
+
timings: blocking.timings,
|
|
213
|
+
};
|
|
214
|
+
}
|