@triflux/remote 10.51.0 → 10.53.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/cto/brief.mjs +0 -8
- package/cto/collect.mjs +1 -127
- package/cto/current.schema.json +2 -19
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -139
- package/hub/index.mjs +0 -9
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/cli/commands/debug.mjs +1 -2
- package/hub/team/cli/commands/start/index.mjs +2 -1
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +2 -4
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/headless.mjs +132 -551
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/results-index.mjs +92 -0
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/tui-viewer.mjs +9 -56
- package/hub/team/tui.mjs +0 -37
- package/hub/team/uds-orchestrator.mjs +20 -599
- package/hub/workers/factory.mjs +6 -51
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +42 -101
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/cli/services/hub-client.mjs +0 -178
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-intent.mjs +0 -18
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/tools.mjs +0 -900
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
// hub/workers/lib/jsonrpc-stdio.mjs
|
|
2
|
-
// Minimal line-delimited JSON-RPC 2.0 client over stdio.
|
|
3
|
-
// Replaces vscode-jsonrpc for the codex app-server transport.
|
|
4
|
-
//
|
|
5
|
-
// Wire format (Issue #95 P1 #1): OpenAI App Server JSONL variant omits the
|
|
6
|
-
// top-level `"jsonrpc": "2.0"` header on outbound frames. Inbound decode is
|
|
7
|
-
// lenient — frames with or without the header are accepted for forward compat.
|
|
8
|
-
//
|
|
9
|
-
// Contract:
|
|
10
|
-
// new JsonRpcStdioClient({ stdin, stdout, onError, maxLineSize })
|
|
11
|
-
// request(method, params, timeoutMs=60000) -> Promise<result>
|
|
12
|
-
// notify(method, params) -> void
|
|
13
|
-
// onNotification(method, cb) -> unsubscribe() ('*' = catch-all)
|
|
14
|
-
// close(reason) -> void (idempotent; optional reason marks closing state)
|
|
15
|
-
// isOpen() -> boolean
|
|
16
|
-
//
|
|
17
|
-
// Lifecycle (Issue #95 P1 #3):
|
|
18
|
-
// State machine: running | closing | closed
|
|
19
|
-
// - `running` is the default. Parse/EOF/max-line errors reject in-flight
|
|
20
|
-
// requests and transition to `closed`.
|
|
21
|
-
// - `closing` is entered via `close("closing")` before a graceful shutdown;
|
|
22
|
-
// EOF in `closing` is a normal termination and does NOT fail-fast.
|
|
23
|
-
//
|
|
24
|
-
// AC18: any single line whose raw-byte length would exceed maxLineSize is
|
|
25
|
-
// rejected at the stream layer (before readline emits it) to defend against
|
|
26
|
-
// OOM/DoS from an unresponsive or malicious peer.
|
|
27
|
-
|
|
28
|
-
import { createInterface } from "node:readline";
|
|
29
|
-
|
|
30
|
-
import { JsonRpcDispatchBase } from "./jsonrpc-core.mjs";
|
|
31
|
-
|
|
32
|
-
export { JsonRpcProtocolError } from "./jsonrpc-core.mjs";
|
|
33
|
-
|
|
34
|
-
const DEFAULT_MAX_LINE_SIZE = 1024 * 1024; // 1 MiB
|
|
35
|
-
const CLOSED_MESSAGE = "JsonRpcStdioClient closed";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Thrown (via onError) when an inbound line would exceed maxLineSize.
|
|
39
|
-
*/
|
|
40
|
-
export class MaxLineSizeExceededError extends Error {
|
|
41
|
-
/**
|
|
42
|
-
* @param {number} size Bytes observed so far in the offending line.
|
|
43
|
-
* @param {number} max Configured max.
|
|
44
|
-
*/
|
|
45
|
-
constructor(size, max) {
|
|
46
|
-
super(`JSON-RPC line exceeded max size: ${size} > ${max}`);
|
|
47
|
-
this.name = "MaxLineSizeExceededError";
|
|
48
|
-
this.size = size;
|
|
49
|
-
this.max = max;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Thrown when the underlying stream closes unexpectedly (EOF outside `closing`).
|
|
55
|
-
*/
|
|
56
|
-
export class JsonRpcTransportError extends Error {
|
|
57
|
-
/** @param {string} message @param {{ cause?: unknown }} [options] */
|
|
58
|
-
constructor(message, options = {}) {
|
|
59
|
-
super(message, { cause: options.cause });
|
|
60
|
-
this.name = "JsonRpcTransportError";
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Line-delimited JSON-RPC 2.0 client over a pair of Node streams.
|
|
66
|
-
*/
|
|
67
|
-
export class JsonRpcStdioClient extends JsonRpcDispatchBase {
|
|
68
|
-
/**
|
|
69
|
-
* @param {object} options
|
|
70
|
-
* @param {NodeJS.ReadableStream} options.stdin Server -> client bytes.
|
|
71
|
-
* @param {NodeJS.WritableStream} options.stdout Client -> server bytes.
|
|
72
|
-
* @param {(err: Error) => void} [options.onError] Protocol error sink.
|
|
73
|
-
* @param {number} [options.maxLineSize] Max bytes per inbound line.
|
|
74
|
-
*/
|
|
75
|
-
constructor({ stdin, stdout, onError, maxLineSize = DEFAULT_MAX_LINE_SIZE }) {
|
|
76
|
-
if (!stdin || typeof stdin.on !== "function") {
|
|
77
|
-
throw new TypeError("JsonRpcStdioClient requires a readable stdin");
|
|
78
|
-
}
|
|
79
|
-
if (!stdout || typeof stdout.write !== "function") {
|
|
80
|
-
throw new TypeError("JsonRpcStdioClient requires a writable stdout");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
super({ onError, closedMessage: CLOSED_MESSAGE });
|
|
84
|
-
|
|
85
|
-
this._stdin = stdin;
|
|
86
|
-
this._stdout = stdout;
|
|
87
|
-
this._maxLineSize =
|
|
88
|
-
Number.isFinite(maxLineSize) && maxLineSize > 0
|
|
89
|
-
? maxLineSize
|
|
90
|
-
: DEFAULT_MAX_LINE_SIZE;
|
|
91
|
-
|
|
92
|
-
/** @type {'running'|'closing'|'closed'} */
|
|
93
|
-
this._state = "running";
|
|
94
|
-
|
|
95
|
-
// AC18: track bytes since last newline at the raw stream layer so an
|
|
96
|
-
// oversized line is rejected *before* readline concatenates it internally.
|
|
97
|
-
this._pendingLineSize = 0;
|
|
98
|
-
this._oversized = false;
|
|
99
|
-
|
|
100
|
-
this._onStdinData = (chunk) => this._trackRawBytes(chunk);
|
|
101
|
-
this._stdin.on("data", this._onStdinData);
|
|
102
|
-
|
|
103
|
-
// Without these handlers, an EPIPE/ERR_STREAM_DESTROYED on either pipe
|
|
104
|
-
// would bubble up as an unhandled 'error' and take down the hub process.
|
|
105
|
-
this._onStdinError = (err) => this._handleStreamError("stdin", err);
|
|
106
|
-
this._onStdoutError = (err) => this._handleStreamError("stdout", err);
|
|
107
|
-
if (typeof this._stdin.on === "function") {
|
|
108
|
-
this._stdin.on("error", this._onStdinError);
|
|
109
|
-
}
|
|
110
|
-
if (typeof this._stdout.on === "function") {
|
|
111
|
-
this._stdout.on("error", this._onStdoutError);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
this._rl = createInterface({ input: this._stdin, crlfDelay: Infinity });
|
|
115
|
-
this._rl.on("line", (line) => this._handleInboundMessage(line));
|
|
116
|
-
// readline re-emits the input stream 'error' on itself; the raw stdin
|
|
117
|
-
// handler above already converts it into a JsonRpcTransportError, so
|
|
118
|
-
// suppress the re-emit to avoid an unhandled 'error' on the Interface.
|
|
119
|
-
this._rl.on("error", () => {});
|
|
120
|
-
this._rl.on("close", () => {
|
|
121
|
-
// P1 #3 fail-fast: EOF during `running` is a transport error. Pending
|
|
122
|
-
// requests are rejected with JsonRpcTransportError. EOF during `closing`
|
|
123
|
-
// is a normal shutdown — pending requests are rejected with the generic
|
|
124
|
-
// CLOSED_MESSAGE via close().
|
|
125
|
-
if (this._state === "running") {
|
|
126
|
-
const err = new JsonRpcTransportError(
|
|
127
|
-
"JSON-RPC stream closed unexpectedly (EOF during running state)",
|
|
128
|
-
);
|
|
129
|
-
this._emitError(err);
|
|
130
|
-
this._closeWith("closed", err);
|
|
131
|
-
} else if (this._state !== "closed") {
|
|
132
|
-
this._closeWith("closed");
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// --- internals ---------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
_encodeAndSend(frame) {
|
|
140
|
-
if (this._state === "closed") return;
|
|
141
|
-
const line = `${JSON.stringify(frame)}\n`;
|
|
142
|
-
try {
|
|
143
|
-
this._stdout.write(line, (err) => {
|
|
144
|
-
if (err) this._handleStreamError("stdout-write", err);
|
|
145
|
-
});
|
|
146
|
-
} catch (err) {
|
|
147
|
-
this._handleStreamError("stdout-write", err);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
_teardownTransport() {
|
|
152
|
-
try {
|
|
153
|
-
this._stdin.off?.("data", this._onStdinData);
|
|
154
|
-
} catch {
|
|
155
|
-
/* ignore */
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
this._rl.close();
|
|
159
|
-
} catch {
|
|
160
|
-
/* ignore */
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Convert a raw stream error into a JsonRpcTransportError, emit it to the
|
|
166
|
-
* error sink, and close the client so pending requests are rejected.
|
|
167
|
-
* Idempotent: repeated errors after close are swallowed.
|
|
168
|
-
* @param {string} which identifier for the originating pipe/operation
|
|
169
|
-
* @param {unknown} err raw error from the stream
|
|
170
|
-
*/
|
|
171
|
-
_handleStreamError(which, err) {
|
|
172
|
-
if (this._state === "closed") return;
|
|
173
|
-
const base = err instanceof Error ? err : new Error(String(err));
|
|
174
|
-
const wrapped = new JsonRpcTransportError(
|
|
175
|
-
`JSON-RPC stream error on ${which}: ${base.message}`,
|
|
176
|
-
);
|
|
177
|
-
wrapped.cause = base;
|
|
178
|
-
this._emitError(wrapped);
|
|
179
|
-
this._closeWith("closed", wrapped);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
_trackRawBytes(chunk) {
|
|
183
|
-
if (this._oversized || this._state === "closed") return;
|
|
184
|
-
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
185
|
-
for (let i = 0; i < buf.length; i++) {
|
|
186
|
-
if (buf[i] === 0x0a /* \n */) {
|
|
187
|
-
this._pendingLineSize = 0;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
this._pendingLineSize += 1;
|
|
191
|
-
if (this._pendingLineSize > this._maxLineSize) {
|
|
192
|
-
this._oversized = true;
|
|
193
|
-
const err = new MaxLineSizeExceededError(
|
|
194
|
-
this._pendingLineSize,
|
|
195
|
-
this._maxLineSize,
|
|
196
|
-
);
|
|
197
|
-
this._emitError(err);
|
|
198
|
-
// P1 #3 fail-fast: oversized line → reject pending with the actual error
|
|
199
|
-
this._closeWith("closed", err);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
export function findAllClaudeMdPaths(options = {}) {
|
|
6
|
-
const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
|
|
7
|
-
const homeDir = options.homeDir ? resolve(options.homeDir) : homedir();
|
|
8
|
-
const includeGlobal = options.includeGlobal !== false;
|
|
9
|
-
const includeProject = options.includeProject !== false;
|
|
10
|
-
|
|
11
|
-
const candidates = [];
|
|
12
|
-
if (includeGlobal) candidates.push(join(homeDir, ".claude", "CLAUDE.md"));
|
|
13
|
-
if (includeProject) candidates.push(join(cwd, "CLAUDE.md"));
|
|
14
|
-
|
|
15
|
-
const seen = new Set();
|
|
16
|
-
const paths = [];
|
|
17
|
-
for (const candidate of candidates) {
|
|
18
|
-
const normalized = resolve(candidate);
|
|
19
|
-
if (seen.has(normalized)) continue;
|
|
20
|
-
seen.add(normalized);
|
|
21
|
-
if (existsSync(normalized)) {
|
|
22
|
-
paths.push(normalized);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return paths;
|
|
26
|
-
}
|
package/scripts/lib/context.mjs
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 요청별 로그 컨텍스트 전파 (AsyncLocalStorage 기반).
|
|
3
|
-
*
|
|
4
|
-
* Hub HTTP 서버의 요청마다 correlationId를 자동 할당하여,
|
|
5
|
-
* 하나의 요청에서 발생한 모든 로그를 추적할 수 있다.
|
|
6
|
-
*
|
|
7
|
-
* 사용법:
|
|
8
|
-
* import { getCorrelationId, withRequestContext } from './lib/context.mjs';
|
|
9
|
-
*
|
|
10
|
-
* // 미들웨어에서 컨텍스트 생성
|
|
11
|
-
* withRequestContext({ method: 'POST', path: '/bridge/result' }, () => {
|
|
12
|
-
* const correlationId = getCorrelationId();
|
|
13
|
-
* });
|
|
14
|
-
*
|
|
15
|
-
*/
|
|
16
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
|
-
|
|
19
|
-
import { logger } from "./logger.mjs";
|
|
20
|
-
|
|
21
|
-
/** @type {AsyncLocalStorage<{logger: import('pino').Logger, correlationId: string}>} */
|
|
22
|
-
const asyncLocalStorage = new AsyncLocalStorage();
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 현재 요청의 상관 ID를 반환한다.
|
|
26
|
-
*
|
|
27
|
-
* @returns {string|undefined}
|
|
28
|
-
*/
|
|
29
|
-
export function getCorrelationId() {
|
|
30
|
-
return asyncLocalStorage.getStore()?.correlationId;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 요청 컨텍스트를 생성하고 콜백을 실행한다.
|
|
35
|
-
*
|
|
36
|
-
* @param {object} context — 컨텍스트 필드 (method, path 등)
|
|
37
|
-
* @param {string} [context.correlationId] — 외부에서 전달된 상관 ID (없으면 자동 생성)
|
|
38
|
-
* @param {function} callback — 컨텍스트 내에서 실행할 함수
|
|
39
|
-
* @returns {*}
|
|
40
|
-
*/
|
|
41
|
-
export function withRequestContext(context, callback) {
|
|
42
|
-
const correlationId = context.correlationId || randomUUID();
|
|
43
|
-
const { correlationId: _, ...rest } = context;
|
|
44
|
-
|
|
45
|
-
const store = {
|
|
46
|
-
correlationId,
|
|
47
|
-
logger: logger.child({ correlationId, ...rest }),
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return asyncLocalStorage.run(store, callback);
|
|
51
|
-
}
|
package/scripts/lib/handoff.mjs
DELETED
|
@@ -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
|
-
}
|
package/scripts/lib/logger.mjs
DELETED
|
@@ -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
|
-
});
|