@triflux/core 10.50.2 → 10.52.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.
Files changed (84) hide show
  1. package/hooks/agy-session-hook.mjs +11 -56
  2. package/hooks/codex-session-hook.mjs +11 -101
  3. package/hub/bridge.mjs +65 -1250
  4. package/hub/cli-adapter-base.mjs +12 -90
  5. package/hub/codex-adapter.mjs +0 -1
  6. package/hub/codex-preflight.mjs +5 -28
  7. package/hub/index.mjs +3 -19
  8. package/hub/lib/process-utils.mjs +1 -156
  9. package/hub/lib/spawn-trace.mjs +3 -2
  10. package/hub/lib/timeout-defaults.mjs +0 -1
  11. package/hub/team/claude-daemon-control.mjs +1 -2
  12. package/hub/team/claude-session-projection.mjs +5 -73
  13. package/hub/team/nativeProxy.mjs +759 -0
  14. package/hub/team/session-context.mjs +1 -1
  15. package/hud/cli-policy.mjs +0 -22
  16. package/hud/constants.mjs +13 -26
  17. package/hud/hud-qos-status.mjs +42 -54
  18. package/hud/providers/gemini.mjs +125 -20
  19. package/hud/renderers.mjs +101 -122
  20. package/hud/utils.mjs +10 -6
  21. package/package.json +2 -6
  22. package/scripts/lib/agent-route-policy.mjs +143 -62
  23. package/scripts/lib/cli-claude.mjs +0 -14
  24. package/scripts/lib/env-probe.mjs +2 -131
  25. package/scripts/lib/gemini-profiles.mjs +2 -69
  26. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  27. package/scripts/lib/machine-profile.mjs +3 -17
  28. package/scripts/lib/mcp-filter.mjs +109 -53
  29. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  30. package/scripts/lib/mcp-health.mjs +2 -2
  31. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  32. package/scripts/lib/psmux-info.mjs +11 -59
  33. package/scripts/lib/session-presence.mjs +2 -100
  34. package/scripts/lib/skill-template.mjs +0 -245
  35. package/hub/adaptive-diagnostic.mjs +0 -350
  36. package/hub/adaptive-inject.mjs +0 -224
  37. package/hub/adaptive-memory.mjs +0 -277
  38. package/hub/adaptive.mjs +0 -172
  39. package/hub/assign-callbacks.mjs +0 -170
  40. package/hub/delegator/contracts.mjs +0 -38
  41. package/hub/delegator/index.mjs +0 -14
  42. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  43. package/hub/delegator/service.mjs +0 -336
  44. package/hub/delegator/tool-definitions.mjs +0 -35
  45. package/hub/dynamic-routing-engine.mjs +0 -511
  46. package/hub/hitl.mjs +0 -202
  47. package/hub/lib/known-errors.json +0 -72
  48. package/hub/lib/memory-store.mjs +0 -1001
  49. package/hub/lib/phase-manager.mjs +0 -493
  50. package/hub/lib/risk-tier.mjs +0 -53
  51. package/hub/lib/ssh-command.mjs +0 -231
  52. package/hub/lib/staged-file-detect.mjs +0 -43
  53. package/hub/lib/state-snapshot.mjs +0 -293
  54. package/hub/lib/tfx-route-args.mjs +0 -353
  55. package/hub/lib/trace-recorder.mjs +0 -153
  56. package/hub/lib/uuidv7.mjs +0 -45
  57. package/hub/memory-doctor.mjs +0 -640
  58. package/hub/middleware/quota-middleware.mjs +0 -70
  59. package/hub/middleware/request-logger.mjs +0 -191
  60. package/hub/paths.mjs +0 -24
  61. package/hub/pipeline/index.mjs +0 -163
  62. package/hub/pipeline/state.mjs +0 -191
  63. package/hub/pipeline/transitions.mjs +0 -131
  64. package/hub/reflexion.mjs +0 -290
  65. package/hub/role-contract.mjs +0 -314
  66. package/hub/router.mjs +0 -2026
  67. package/hub/routing-snapshot.mjs +0 -261
  68. package/hub/session-fingerprint.mjs +0 -418
  69. package/hub/state.mjs +0 -300
  70. package/hub/team/synapse-http.mjs +0 -311
  71. package/hub/team-bridge.mjs +0 -27
  72. package/mesh/index.mjs +0 -69
  73. package/mesh/mesh-budget.mjs +0 -137
  74. package/mesh/mesh-heartbeat.mjs +0 -100
  75. package/mesh/mesh-protocol.mjs +0 -96
  76. package/mesh/mesh-queue.mjs +0 -163
  77. package/mesh/mesh-registry.mjs +0 -78
  78. package/mesh/mesh-router.mjs +0 -77
  79. package/scripts/lib/claudemd-scanner.mjs +0 -223
  80. package/scripts/lib/context.mjs +0 -67
  81. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  82. package/scripts/lib/handoff.mjs +0 -187
  83. package/scripts/lib/logger.mjs +0 -122
  84. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,223 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- export const TFX_START = "<!-- TFX:START -->";
7
- export const TFX_END = "<!-- TFX:END -->";
8
- export const OMC_END = "<!-- OMC:END -->";
9
- const TFX_VERSION_RE = /<!-- TFX:VERSION:([\d.]+) -->/u;
10
-
11
- const LEGACY_PATTERNS = [
12
- /<user_cli_routing>/u,
13
- /Codex Pro 무료 기간/u,
14
- /codex exec --dangerously-bypass.*skip-git-repo-check/u,
15
- /OMC 에이전트 → CLI 매핑/u,
16
- /Spark 가드레일/u,
17
- ];
18
-
19
- const DEFAULT_TFX_TEMPLATE = [
20
- "### triflux CLI routing (managed)",
21
- "- 이 블록은 triflux setup에서 자동으로 관리됩니다.",
22
- "- 직접 수정이 필요하면 블록 바깥에 사용자 섹션을 추가하세요.",
23
- ].join("\n");
24
-
25
- const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
26
- const DEFAULT_TEMPLATE_PATH = join(
27
- SCRIPT_DIR,
28
- "..",
29
- "templates",
30
- "claudemd-tfx-section.md",
31
- );
32
-
33
- function resolveVersion(version) {
34
- if (version) return version;
35
- try {
36
- const pkgPath = join(SCRIPT_DIR, "..", "..", "package.json");
37
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
38
- return pkg.version || "0.0.0";
39
- } catch {
40
- return "0.0.0";
41
- }
42
- }
43
-
44
- function resolveTemplate(template, templatePath = DEFAULT_TEMPLATE_PATH) {
45
- if (typeof template === "string" && template.trim()) {
46
- return template.trim();
47
- }
48
- if (existsSync(templatePath)) {
49
- const raw = readFileSync(templatePath, "utf8").trim();
50
- if (raw) return raw;
51
- }
52
- return DEFAULT_TFX_TEMPLATE;
53
- }
54
-
55
- function findManagedSection(rawText) {
56
- const raw = String(rawText || "");
57
- const startIdx = raw.indexOf(TFX_START);
58
- const endMarkerIdx = raw.indexOf(TFX_END);
59
- if (startIdx === -1 || endMarkerIdx === -1 || endMarkerIdx <= startIdx) {
60
- return {
61
- found: false,
62
- content: null,
63
- version: null,
64
- startIdx: -1,
65
- endIdx: -1,
66
- };
67
- }
68
-
69
- const endIdx = endMarkerIdx + TFX_END.length;
70
- const content = raw.slice(startIdx, endIdx);
71
- const versionMatch = content.match(TFX_VERSION_RE);
72
-
73
- return {
74
- found: true,
75
- content,
76
- version: versionMatch ? versionMatch[1] : null,
77
- startIdx,
78
- endIdx,
79
- };
80
- }
81
-
82
- function detectLegacyRange(rawText) {
83
- const raw = String(rawText || "");
84
- const matches = LEGACY_PATTERNS.some((pattern) => pattern.test(raw));
85
- if (!matches) {
86
- return { found: false, startIdx: -1, endIdx: -1 };
87
- }
88
-
89
- const startTag = raw.indexOf("<user_cli_routing>");
90
- const endTag = raw.indexOf("</user_cli_routing>");
91
- if (startTag === -1 || endTag === -1 || endTag <= startTag) {
92
- return { found: true, startIdx: -1, endIdx: -1 };
93
- }
94
-
95
- let removeStart = startTag;
96
- const userOverridesComment = "<!-- USER OVERRIDES";
97
- const commentIdx = raw.lastIndexOf(userOverridesComment, startTag);
98
- if (commentIdx !== -1 && startTag - commentIdx < 200) {
99
- removeStart = commentIdx;
100
- }
101
-
102
- return {
103
- found: true,
104
- startIdx: removeStart,
105
- endIdx: endTag + "</user_cli_routing>".length,
106
- };
107
- }
108
-
109
- function normalizeSpacing(text) {
110
- return String(text || "").replace(/\n{3,}/gu, "\n\n");
111
- }
112
-
113
- export function findAllClaudeMdPaths(options = {}) {
114
- const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
115
- const homeDir = options.homeDir ? resolve(options.homeDir) : homedir();
116
- const includeGlobal = options.includeGlobal !== false;
117
- const includeProject = options.includeProject !== false;
118
-
119
- const candidates = [];
120
- if (includeGlobal) candidates.push(join(homeDir, ".claude", "CLAUDE.md"));
121
- if (includeProject) candidates.push(join(cwd, "CLAUDE.md"));
122
-
123
- const seen = new Set();
124
- const paths = [];
125
- for (const candidate of candidates) {
126
- const normalized = resolve(candidate);
127
- if (seen.has(normalized)) continue;
128
- seen.add(normalized);
129
- if (existsSync(normalized)) {
130
- paths.push(normalized);
131
- }
132
- }
133
- return paths;
134
- }
135
-
136
- export function writeSection(filePath, options = {}) {
137
- const absolutePath = resolve(filePath);
138
- const version = resolveVersion(options.version);
139
- const template = resolveTemplate(options.template, options.templatePath);
140
- const block = [
141
- TFX_START,
142
- `<!-- TFX:VERSION:${version} -->`,
143
- template,
144
- TFX_END,
145
- ].join("\n");
146
-
147
- if (!existsSync(absolutePath)) {
148
- writeFileSync(absolutePath, `${block}\n`, "utf8");
149
- return { action: "created", version, path: absolutePath };
150
- }
151
-
152
- const raw = readFileSync(absolutePath, "utf8");
153
- const existing = findManagedSection(raw);
154
-
155
- if (existing.found) {
156
- const nextContent = `${raw.slice(0, existing.startIdx)}${block}${raw.slice(existing.endIdx)}`;
157
- writeFileSync(absolutePath, nextContent, "utf8");
158
- return {
159
- action: "updated",
160
- oldVersion: existing.version,
161
- version,
162
- path: absolutePath,
163
- };
164
- }
165
-
166
- const omcEndIdx = raw.indexOf(OMC_END);
167
- if (omcEndIdx !== -1) {
168
- const insertAt = omcEndIdx + OMC_END.length;
169
- const before = raw.slice(0, insertAt);
170
- const after = raw.slice(insertAt);
171
- const nextContent = `${before}\n${block}${after}`;
172
- writeFileSync(absolutePath, nextContent, "utf8");
173
- return { action: "inserted_after_omc", version, path: absolutePath };
174
- }
175
-
176
- const separator = raw.endsWith("\n") ? "" : "\n";
177
- writeFileSync(absolutePath, `${raw}${separator}${block}\n`, "utf8");
178
- return { action: "appended", version, path: absolutePath };
179
- }
180
-
181
- export function migrateClaudeMd(filePath, options = {}) {
182
- const absolutePath = resolve(filePath);
183
- if (!existsSync(absolutePath)) {
184
- return { action: "no_file", removed: [], path: absolutePath };
185
- }
186
-
187
- const raw = readFileSync(absolutePath, "utf8");
188
- const existing = findManagedSection(raw);
189
- const legacy = detectLegacyRange(raw);
190
-
191
- if (existing.found && !legacy.found) {
192
- return {
193
- action: "already_managed",
194
- removed: [],
195
- version: existing.version,
196
- path: absolutePath,
197
- };
198
- }
199
-
200
- const removed = [];
201
- let nextContent = raw;
202
-
203
- if (legacy.found && legacy.startIdx !== -1 && legacy.endIdx !== -1) {
204
- nextContent = `${nextContent.slice(0, legacy.startIdx)}${nextContent.slice(legacy.endIdx)}`;
205
- removed.push("<user_cli_routing> block");
206
- }
207
-
208
- const existingAfterLegacy = findManagedSection(nextContent);
209
- if (existingAfterLegacy.found) {
210
- nextContent = `${nextContent.slice(0, existingAfterLegacy.startIdx)}${nextContent.slice(existingAfterLegacy.endIdx)}`;
211
- removed.push("old TFX block");
212
- }
213
-
214
- writeFileSync(absolutePath, normalizeSpacing(nextContent), "utf8");
215
- const writeResult = writeSection(absolutePath, options);
216
-
217
- return {
218
- action: "migrated",
219
- removed,
220
- version: writeResult.version,
221
- path: absolutePath,
222
- };
223
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
3
- *
4
- * Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
5
- * 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
6
- *
7
- * 사용법:
8
- * import { getLogger, getCorrelationId, withRequestContext } from './lib/context.mjs';
9
- *
10
- * // 미들웨어에서 컨텍스트 생성
11
- * withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
12
- * const log = getLogger();
13
- * log.info({ agentId }, 'bridge.result_received');
14
- * });
15
- *
16
- * // 내부 함수에서 자동 상관 ID
17
- * function processResult() {
18
- * const log = getLogger();
19
- * log.info('result.processed'); // correlationId 자동 포함
20
- * }
21
- */
22
- import { AsyncLocalStorage } from "node:async_hooks";
23
- import { randomUUID } from "node:crypto";
24
-
25
- import { logger } from "./logger.mjs";
26
-
27
- /** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
28
- export const asyncLocalStorage = new AsyncLocalStorage();
29
-
30
- /**
31
- * 현재 요청 컨텍스트의 로거를 반환한다.
32
- * 요청 컨텍스트 밖에서 호출하면 기본 로거를 반환한다.
33
- *
34
- * @returns {import('pino').Logger}
35
- */
36
- export function getLogger() {
37
- return asyncLocalStorage.getStore()?.logger || logger;
38
- }
39
-
40
- /**
41
- * 현재 요청의 상관 ID를 반환한다.
42
- *
43
- * @returns {string|undefined}
44
- */
45
- export function getCorrelationId() {
46
- return asyncLocalStorage.getStore()?.correlationId;
47
- }
48
-
49
- /**
50
- * 요청 컨텍스트를 생성하고 콜백을 실행한다.
51
- *
52
- * @param {object} context — 컨텍스트 필드 (method, path 등)
53
- * @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
54
- * @param {function} callback — 컨텍스트 내에서 실행할 함수
55
- * @returns {*}
56
- */
57
- export function withRequestContext(context, callback) {
58
- const correlationId = context.correlationId || randomUUID();
59
- const { correlationId: _, ...rest } = context;
60
-
61
- const store = {
62
- correlationId,
63
- logger: logger.child({ correlationId, ...rest }),
64
- };
65
-
66
- return asyncLocalStorage.run(store, callback);
67
- }
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env node
2
- // scripts/lib/dynamic-route-cli.mjs
3
- //
4
- // Phase 1 Wire-up 3 helper — tfx-route.sh 등 sh 경로에서 dynamic routing
5
- // decision 을 받아 단일 CLI 문자열 (또는 JSON) 으로 출력한다.
6
- //
7
- // 사용:
8
- // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex
9
- // → "codex"
10
- // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --json
11
- // → {"scenario":"fallback","mode":"codex-default","shards":[{"cli":"codex",...}]}
12
- // node scripts/lib/dynamic-route-cli.mjs --agent-hint codex --field scenario
13
- // → "fallback"
14
- //
15
- // 정책:
16
- // - opt-in env TRIFLUX_DYNAMIC_ROUTING=1|true 미설정 시 즉시 silent exit 0
17
- // (override 없음 = stdout empty, sh caller 가 graceful no-op).
18
- // - error 발생 시 stderr 에 한 줄 + exit 0 (sh caller 가 fallback path 진행).
19
-
20
- import { createDynamicRouter } from "../../hub/dynamic-routing-engine.mjs";
21
-
22
- function parseArgs(argv) {
23
- const args = {
24
- taskId: `tfx-route-${process.pid}-${Date.now()}`,
25
- agentHint: "codex",
26
- teamSize: 1,
27
- json: false,
28
- field: null,
29
- };
30
- for (let i = 2; i < argv.length; i++) {
31
- const v = argv[i];
32
- switch (v) {
33
- case "--task-id":
34
- args.taskId = argv[++i] || args.taskId;
35
- break;
36
- case "--agent-hint":
37
- args.agentHint = argv[++i] || args.agentHint;
38
- break;
39
- case "--team-size":
40
- args.teamSize = Number(argv[++i]) || args.teamSize;
41
- break;
42
- case "--json":
43
- args.json = true;
44
- break;
45
- case "--field":
46
- args.field = argv[++i] || null;
47
- break;
48
- case "--help":
49
- case "-h":
50
- process.stdout.write(
51
- "Usage: dynamic-route-cli.mjs [--task-id ID] [--agent-hint CLI] [--team-size N] [--json|--field FIELD]\n",
52
- );
53
- process.exit(0);
54
- break;
55
- }
56
- }
57
- return args;
58
- }
59
-
60
- async function main() {
61
- const args = parseArgs(process.argv);
62
-
63
- // opt-in env 미설정 시 silent no-op (sh caller graceful path)
64
- const flag = process.env.TRIFLUX_DYNAMIC_ROUTING;
65
- if (flag !== "1" && flag !== "true") return;
66
-
67
- let decision;
68
- try {
69
- const router = createDynamicRouter();
70
- decision = await router.routeRequest({
71
- task_id: args.taskId,
72
- team_size: args.teamSize,
73
- agent_hint: args.agentHint,
74
- });
75
- } catch (err) {
76
- process.stderr.write(
77
- `[dynamic-route-cli] error: ${err?.message || String(err)}\n`,
78
- );
79
- return;
80
- }
81
-
82
- if (args.json) {
83
- process.stdout.write(JSON.stringify(decision));
84
- return;
85
- }
86
-
87
- if (args.field) {
88
- const value = decision?.[args.field];
89
- if (value !== undefined && value !== null) {
90
- process.stdout.write(String(value));
91
- }
92
- return;
93
- }
94
-
95
- // 기본 — shards[0].cli 만 stdout 으로 (sh caller 가 즉시 비교)
96
- const cli = decision?.shards?.[0]?.cli;
97
- if (cli) {
98
- process.stdout.write(String(cli));
99
- }
100
- }
101
-
102
- main().catch((err) => {
103
- process.stderr.write(
104
- `[dynamic-route-cli] fatal: ${err?.message || String(err)}\n`,
105
- );
106
- process.exit(0); // sh caller 가 정상 fallback 하도록 always 0
107
- });
@@ -1,187 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { basename, resolve } from "node:path";
4
- import { findAllClaudeMdPaths } from "./claudemd-scanner.mjs";
5
-
6
- function runCommand(command, cwd, executor = execSync) {
7
- try {
8
- const output = executor(command, {
9
- cwd,
10
- encoding: "utf8",
11
- stdio: ["ignore", "pipe", "ignore"],
12
- windowsHide: true,
13
- });
14
- return String(output || "").trimEnd();
15
- } catch {
16
- return "";
17
- }
18
- }
19
-
20
- function parseStatusLines(statusText) {
21
- const lines = String(statusText || "")
22
- .split(/\r?\n/u)
23
- .filter(Boolean);
24
- const changedFiles = [];
25
- const status = [];
26
-
27
- for (const line of lines) {
28
- const normalized = line.trimEnd();
29
- if (!normalized) continue;
30
-
31
- const rawStatus = normalized.slice(0, 2).trim() || "??";
32
- const rawPath = normalized.slice(3).trim();
33
- const filePath = rawPath.includes(" -> ")
34
- ? rawPath.split(" -> ").at(-1)?.trim()
35
- : rawPath;
36
- if (!filePath) continue;
37
-
38
- changedFiles.push(filePath);
39
- status.push({ path: filePath, status: rawStatus });
40
- }
41
-
42
- return {
43
- changedFiles: Array.from(new Set(changedFiles)),
44
- status,
45
- };
46
- }
47
-
48
- function normalizeDecisionLine(line) {
49
- return String(line || "")
50
- .trim()
51
- .replace(/^[-*]\s+/u, "")
52
- .trim();
53
- }
54
-
55
- function parseDecisionFile(decisionFile) {
56
- if (!decisionFile) return [];
57
- const absolute = resolve(decisionFile);
58
- if (!existsSync(absolute)) return [];
59
-
60
- const raw = readFileSync(absolute, "utf8");
61
- return raw
62
- .split(/\r?\n/u)
63
- .map(normalizeDecisionLine)
64
- .filter((line) => line.length > 0);
65
- }
66
-
67
- function normalizeDecisions(decisions, decisionFile) {
68
- const merged = [
69
- ...(Array.isArray(decisions) ? decisions : []),
70
- ...parseDecisionFile(decisionFile),
71
- ]
72
- .map(normalizeDecisionLine)
73
- .filter(Boolean);
74
-
75
- return Array.from(new Set(merged));
76
- }
77
-
78
- function formatAheadBehind(value) {
79
- if (!value) return null;
80
- const [behindRaw, aheadRaw] = value.split(/\s+/u);
81
- const behind = Number.parseInt(behindRaw, 10);
82
- const ahead = Number.parseInt(aheadRaw, 10);
83
- if (!Number.isFinite(behind) || !Number.isFinite(ahead)) return null;
84
- return { ahead, behind };
85
- }
86
-
87
- export function collectHandoffContext(options = {}) {
88
- const cwd = resolve(options.cwd || process.cwd());
89
- const executor =
90
- typeof options.commandRunner === "function"
91
- ? options.commandRunner
92
- : execSync;
93
- const target = options.target === "local" ? "local" : "remote";
94
- const decisions = normalizeDecisions(options.decisions, options.decisionFile);
95
- const generatedAt = options.generatedAt || new Date().toISOString();
96
- const claudeMdPaths = Array.isArray(options.claudeMdPaths)
97
- ? options.claudeMdPaths
98
- : findAllClaudeMdPaths({
99
- cwd,
100
- homeDir: options.homeDir,
101
- });
102
-
103
- const gitRoot = runCommand("git rev-parse --show-toplevel", cwd, executor);
104
- const branch =
105
- runCommand("git rev-parse --abbrev-ref HEAD", cwd, executor) || null;
106
- const shortStatus = runCommand("git status --short", cwd, executor);
107
- const diffStat = runCommand("git diff --stat --no-color", cwd, executor);
108
- const upstreamRaw = runCommand(
109
- "git rev-list --left-right --count @{upstream}...HEAD",
110
- cwd,
111
- executor,
112
- );
113
- const parsedStatus = parseStatusLines(shortStatus);
114
-
115
- return {
116
- generatedAt,
117
- target,
118
- cwd,
119
- gitRoot: gitRoot || null,
120
- repository: gitRoot ? basename(gitRoot) : basename(cwd),
121
- branch,
122
- upstream: formatAheadBehind(upstreamRaw),
123
- changedFiles: parsedStatus.changedFiles,
124
- fileStatus: parsedStatus.status,
125
- diffStat,
126
- decisions,
127
- claudeMdPaths,
128
- };
129
- }
130
-
131
- export function buildHandoffPrompt(context) {
132
- const safeContext = context || collectHandoffContext();
133
- const branch = safeContext.branch || "unknown";
134
- const upstream = safeContext.upstream
135
- ? `ahead ${safeContext.upstream.ahead}, behind ${safeContext.upstream.behind}`
136
- : "unknown";
137
- const changedFiles =
138
- safeContext.changedFiles.length > 0
139
- ? safeContext.changedFiles.map((file) => `- ${file}`).join("\n")
140
- : "- 변경 파일 없음";
141
- const decisions =
142
- safeContext.decisions.length > 0
143
- ? safeContext.decisions.map((decision) => `- ${decision}`).join("\n")
144
- : "- 명시된 결정사항 없음";
145
- const claudeMdList =
146
- Array.isArray(safeContext.claudeMdPaths) &&
147
- safeContext.claudeMdPaths.length > 0
148
- ? safeContext.claudeMdPaths.map((path) => `- ${path}`).join("\n")
149
- : "- 자동 탐지된 CLAUDE.md 없음";
150
- const diffStat = safeContext.diffStat || "(diff stat 없음)";
151
-
152
- return [
153
- "## TFX Remote Handoff",
154
- `- generated_at: ${safeContext.generatedAt}`,
155
- `- target: ${safeContext.target}`,
156
- `- repository: ${safeContext.repository}`,
157
- `- branch: ${branch} (${upstream})`,
158
- `- cwd: ${safeContext.cwd}`,
159
- "",
160
- "### 변경 파일",
161
- changedFiles,
162
- "",
163
- "### 변경 요약 (git diff --stat)",
164
- "```",
165
- diffStat,
166
- "```",
167
- "",
168
- "### 결정사항",
169
- decisions,
170
- "",
171
- "### CLAUDE.md 참조",
172
- claudeMdList,
173
- "",
174
- "### 다음 세션 지시",
175
- "- 위 변경사항을 먼저 검토하고 누락된 테스트를 확인하세요.",
176
- "- 필요 시 CLAUDE.md 지침을 재확인한 뒤 작업을 이어가세요.",
177
- "- 작업 완료 후 변경 파일과 검증 결과를 요약하세요.",
178
- ].join("\n");
179
- }
180
-
181
- export function serializeHandoff(options = {}) {
182
- const context = collectHandoffContext(options);
183
- return {
184
- ...context,
185
- prompt: buildHandoffPrompt(context),
186
- };
187
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * Logify — triflux 구조화 로깅 설정
3
- *
4
- * 사용법:
5
- * import { logger, createModuleLogger } from './lib/logger.mjs';
6
- *
7
- * // 기본 로거
8
- * logger.info({ taskId: 'abc' }, 'task.started');
9
- *
10
- * // 모듈별 로거
11
- * const log = createModuleLogger('hub');
12
- * log.info({ port: 27888 }, 'server.started');
13
- * log.error({ err }, 'server.error');
14
- *
15
- * 이벤트 네이밍: {도메인}.{액션} 형식
16
- * hub.started, hub.stopped, route.started, route.completed,
17
- * worker.spawned, worker.completed, worker.timeout,
18
- * mcp.connected, mcp.disconnected, mcp.error,
19
- * team.created, team.deleted, task.claimed, task.completed,
20
- * pipe.connected, pipe.message, pipe.error,
21
- * http.request, http.response, http.error
22
- *
23
- * 로그 레벨 가이드:
24
- * debug — 개발/트러블슈팅용 (변수 값, MCP 메시지, 캐시 키)
25
- * info — 정상 흐름 상태 변경 (서버 시작, 워커 완료, 팀 생성)
26
- * warn — 위험 신호 (재시도 발생, 쿼타 임박, 느린 워커)
27
- * error — 작업 실패 (CLI 실행 실패, MCP 연결 끊김)
28
- * fatal — 프로세스 위협 (DB 연결 불가, 포트 충돌)
29
- */
30
- import pino from "pino";
31
-
32
- const isDev = process.env.NODE_ENV !== "production";
33
-
34
- // 로그는 항상 stderr(fd 2)로 보낸다.
35
- //
36
- // Claude Code 훅은 stdout 을 훅의 JSON 페이로드로 파싱한다. 로그가 stdout 으로
37
- // 나가면 페이로드와 한 스트림에 섞여, 훅이 돌려준 hookSpecificOutput 이 JSON 으로
38
- // 해석되지 않고 그냥 본문 텍스트가 된다. Claude Code 2.1.257 부터는 stdout 이
39
- // "{" 로 시작하는데 유효한 JSON 이 아니면 훅 오류로 보고하므로, 로그가 먼저 나가느냐
40
- // 페이로드가 먼저 나가느냐에 따라 훅 전체가 실패로 뒤집힌다.
41
- //
42
- // stderr 로 보내면 Claude Code 가 verbose/transcript 에서 그대로 보여주고 stdout 은
43
- // 페이로드만 남는다. CLI 관례상으로도 구조화 로그는 stderr 가 맞다.
44
- const LOG_FD = 2;
45
-
46
- export const logger = pino(
47
- {
48
- level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
49
-
50
- // 모든 로그에 포함되는 기본 필드
51
- base: {
52
- service: process.env.SERVICE_NAME || "triflux",
53
- env: process.env.NODE_ENV || "development",
54
- },
55
-
56
- // 레벨을 대문자로 출력 (AI 파싱 용이)
57
- formatters: {
58
- level: (label) => ({ level: label.toUpperCase() }),
59
- },
60
-
61
- // ISO 8601 타임스탬프
62
- timestamp: pino.stdTimeFunctions.isoTime,
63
-
64
- // 민감정보 자동 필터링
65
- redact: {
66
- paths: [
67
- "password",
68
- "token",
69
- "apiKey",
70
- "secret",
71
- "authorization",
72
- "*.password",
73
- "*.token",
74
- "*.apiKey",
75
- "*.secret",
76
- "req.headers.authorization",
77
- "req.headers.cookie",
78
- "hubToken",
79
- ],
80
- remove: true,
81
- },
82
-
83
- // 개발 환경: 컬러 콘솔 출력 (stderr)
84
- transport: isDev
85
- ? {
86
- target: "pino-pretty",
87
- options: {
88
- colorize: true,
89
- translateTime: "yyyy-mm-dd HH:MM:ss",
90
- ignore: "pid,hostname",
91
- destination: LOG_FD,
92
- },
93
- }
94
- : undefined,
95
- },
96
- // 운영 환경(transport 미사용)에서도 stdout 이 아니라 stderr 로 쓴다.
97
- isDev ? undefined : pino.destination({ dest: LOG_FD, sync: false }),
98
- );
99
-
100
- /**
101
- * 모듈별 Child Logger 생성.
102
- * 모듈 이름이 모든 로그에 자동 포함된다.
103
- *
104
- * @param {string} module — 모듈 이름 (hub, route, worker, mcp, team 등)
105
- * @returns {import('pino').Logger}
106
- */
107
- export function createModuleLogger(module) {
108
- return logger.child({ module });
109
- }
110
-
111
- // 정상 종료 시 버퍼 flush 보장
112
- process.on("uncaughtException", (err) => {
113
- logger.fatal({ err }, "process.uncaught_exception");
114
- logger.flush();
115
- process.exit(1);
116
- });
117
-
118
- process.on("unhandledRejection", (reason) => {
119
- logger.fatal({ reason: String(reason) }, "process.unhandled_rejection");
120
- logger.flush();
121
- process.exit(1);
122
- });