@triflux/core 10.53.0 → 10.55.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 (47) hide show
  1. package/hooks/codex-session-hook.mjs +1 -17
  2. package/hub/cli-adapter-base.mjs +3 -424
  3. package/hub/index.mjs +0 -5
  4. package/hub/lib/hosts-compat.mjs +18 -64
  5. package/hub/lib/private-tmp.mjs +35 -0
  6. package/hub/lib/process-utils.mjs +19 -220
  7. package/hub/lib/prompt-tmp.mjs +6 -24
  8. package/hub/lib/spawn-trace.mjs +112 -5
  9. package/hub/lib/timeout-defaults.mjs +34 -6
  10. package/hub/lib/worker-lifecycle.mjs +3 -0
  11. package/hub/platform.mjs +3 -1
  12. package/hub/team/agent-map.json +29 -0
  13. package/hub/team/intervention.mjs +6 -4
  14. package/hub/team/nativeProxy.mjs +2 -48
  15. package/hub/team/pane.mjs +329 -0
  16. package/hub/team/psmux.mjs +1765 -0
  17. package/hub/team/retry-state-machine.mjs +1 -1
  18. package/hub/team/session.mjs +541 -0
  19. package/hud/constants.mjs +0 -3
  20. package/hud/providers/claude.mjs +35 -15
  21. package/hud/providers/codex.mjs +169 -42
  22. package/hud/utils.mjs +0 -23
  23. package/package.json +2 -2
  24. package/scripts/lib/backup-rotation.mjs +60 -0
  25. package/scripts/lib/codex-profile-config.mjs +20 -7
  26. package/scripts/lib/codex-recovery.sh +2 -3
  27. package/scripts/lib/install-blob-history.json +3196 -0
  28. package/scripts/lib/install-retire.mjs +203 -0
  29. package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
  30. package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
  31. package/scripts/lib/machine-profile.mjs +2 -2
  32. package/scripts/lib/mcp-filter.mjs +1 -42
  33. package/scripts/lib/mcp-guard-engine.mjs +198 -19
  34. package/scripts/lib/mcp-manifest.mjs +0 -40
  35. package/scripts/lib/process-utils.mjs +19 -0
  36. package/scripts/lib/stable-node.mjs +35 -0
  37. package/scripts/lib/test-env.mjs +20 -0
  38. package/scripts/lib/tmr-install.mjs +183 -0
  39. package/hooks/agy-session-hook.mjs +0 -155
  40. package/hooks/claude-cwd-projection-refresh.mjs +0 -11
  41. package/hooks/hook-orchestrator.mjs +0 -11
  42. package/hooks/lib/resolve-root.mjs +0 -62
  43. package/hub/codex-adapter.mjs +0 -193
  44. package/hub/codex-preflight.mjs +0 -219
  45. package/hub/lib/cto-env.mjs +0 -70
  46. package/scripts/lib/remote-spawn-transfer.mjs +0 -226
  47. package/scripts/lib/session-presence.mjs +0 -151
@@ -1,219 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import { whichCommandAsync } from "./platform.mjs";
7
-
8
- let _cachedVersion = null;
9
-
10
- function escapeRegExp(value) {
11
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12
- }
13
-
14
- function readConfigText(configPath = join(homedir(), ".codex", "config.toml")) {
15
- if (!existsSync(configPath)) return "";
16
- try {
17
- return readFileSync(configPath, "utf8");
18
- } catch {
19
- return "";
20
- }
21
- }
22
-
23
- function readTomlString(text, key) {
24
- const match = String(text).match(
25
- new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"\\s*$`, "mu"),
26
- );
27
- return match?.[1] ?? null;
28
- }
29
-
30
- function readSection(text, name) {
31
- const lines = String(text).split(/\r?\n/u);
32
- const header = `[${name}]`;
33
- const start = lines.findIndex((line) => line.trim() === header);
34
- if (start < 0) return "";
35
- const body = [];
36
- for (const line of lines.slice(start + 1)) {
37
- if (/^\s*\[[^\]]+\]\s*$/u.test(line)) break;
38
- body.push(line);
39
- }
40
- return body.join("\n");
41
- }
42
-
43
- /**
44
- * `codex --version` 실행 결과를 파싱하여 마이너 버전 숫자 반환.
45
- * 파싱 실패 시 0 반환 (구버전으로 간주).
46
- * @returns {number} 마이너 버전 (예: 0.117.0 → 117)
47
- */
48
- export function getCodexVersion() {
49
- if (_cachedVersion !== null) return _cachedVersion;
50
- try {
51
- const out = execSync("codex --version", {
52
- encoding: "utf8",
53
- timeout: 5000,
54
- }).trim();
55
- // "codex 0.117.0" 또는 "0.117.0" 형식 대응
56
- const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
57
- _cachedVersion = match ? parseInt(match[2], 10) : 0;
58
- } catch {
59
- _cachedVersion = 0;
60
- }
61
- return _cachedVersion;
62
- }
63
-
64
- async function checkCodexInstalled() {
65
- const codexPath = await whichCommandAsync("codex");
66
- if (codexPath) return { codexPath, ok: true, warnings: [] };
67
- return {
68
- codexPath: null,
69
- ok: false,
70
- warnings: [
71
- "Codex CLI not found. Install Codex and ensure `codex` is available on PATH.",
72
- ],
73
- };
74
- }
75
-
76
- function checkApprovalMode(configText, opts = {}) {
77
- const approvalPolicy = readTomlString(configText, "approval_policy");
78
- const subcommand = opts.subcommand || "exec";
79
- return {
80
- needsBypass: subcommand === "exec" || approvalPolicy !== "never",
81
- };
82
- }
83
-
84
- async function verifyServerHealth(name, configText) {
85
- const section = readSection(configText, `mcp_servers.${name}`);
86
- if (!section)
87
- return { ok: false, warning: `MCP server '${name}' is not configured.` };
88
- if (/^\s*enabled\s*=\s*false\s*$/mu.test(section)) {
89
- return {
90
- ok: false,
91
- warning: `MCP server '${name}' is disabled in config.toml.`,
92
- };
93
- }
94
-
95
- const command = readTomlString(section, "command");
96
- if (command) {
97
- const resolved = await whichCommandAsync(command);
98
- return resolved
99
- ? { ok: true, warning: "" }
100
- : {
101
- ok: false,
102
- warning: `MCP server '${name}' command not found: ${command}`,
103
- };
104
- }
105
-
106
- const url = readTomlString(section, "url");
107
- if (!url || !/^https?:\/\//u.test(url)) return { ok: true, warning: "" };
108
- try {
109
- const response = await fetch(url, { signal: AbortSignal.timeout(2000) });
110
- return response.status < 500
111
- ? { ok: true, warning: "" }
112
- : {
113
- ok: false,
114
- warning: `MCP server '${name}' returned HTTP ${response.status}.`,
115
- };
116
- } catch {
117
- return {
118
- ok: false,
119
- warning: `MCP server '${name}' is unreachable at ${url}.`,
120
- };
121
- }
122
- }
123
-
124
- async function checkMcpHealth(mcpServers, configText) {
125
- const excludeMcpServers = [];
126
- const warnings = [];
127
-
128
- for (const name of Array.isArray(mcpServers) ? mcpServers : []) {
129
- const server = String(name ?? "").trim();
130
- if (!server) continue;
131
- const result = await verifyServerHealth(server, configText);
132
- if (!result.ok) excludeMcpServers.push(server);
133
- if (result.warning) warnings.push(result.warning);
134
- }
135
-
136
- return { excludeMcpServers, warnings };
137
- }
138
-
139
- /**
140
- * Detects codex config drift between global (~/.codex/config.toml) and the
141
- * target workdir. Covers:
142
- * 1. workdir-local `.codex/config.toml` overriding global approval_policy/sandbox_mode
143
- * 2. instruction-file asymmetry (AGENTS.md vs CLAUDE.md)
144
- *
145
- * @param {string} [workdir] absolute path to the invocation workdir
146
- * @param {string} [globalConfigText] already-read ~/.codex/config.toml content
147
- * @returns {{ warnings: string[] }}
148
- */
149
- export function detectWorkdirDrift(workdir, globalConfigText = "") {
150
- if (!workdir || typeof workdir !== "string") return { warnings: [] };
151
- const warnings = [];
152
-
153
- const localCodexConfig = join(workdir, ".codex", "config.toml");
154
- if (existsSync(localCodexConfig)) {
155
- let localText = "";
156
- try {
157
- localText = readFileSync(localCodexConfig, "utf8");
158
- } catch {
159
- localText = "";
160
- }
161
- const keys = ["approval_policy", "sandbox_mode", "model"];
162
- for (const key of keys) {
163
- const local = readTomlString(localText, key);
164
- const global = readTomlString(globalConfigText, key);
165
- if (local && local !== global) {
166
- warnings.push(
167
- `workdir '.codex/config.toml' overrides ${key}='${local}' (global='${global || "unset"}').`,
168
- );
169
- }
170
- }
171
- }
172
-
173
- const hasAgentsMd = existsSync(join(workdir, "AGENTS.md"));
174
- const hasClaudeMd = existsSync(join(workdir, "CLAUDE.md"));
175
- if (hasAgentsMd !== hasClaudeMd) {
176
- const present = hasAgentsMd ? "AGENTS.md" : "CLAUDE.md";
177
- const missing = hasAgentsMd ? "CLAUDE.md" : "AGENTS.md";
178
- warnings.push(
179
- `workdir has ${present} but not ${missing}; codex and other CLIs may read different instruction sources.`,
180
- );
181
- }
182
-
183
- return { warnings };
184
- }
185
-
186
- export async function runPreflight(opts = {}) {
187
- const install = await checkCodexInstalled();
188
- if (!install.ok) {
189
- return {
190
- codexPath: null,
191
- version: 0,
192
- needsBypass: true,
193
- excludeMcpServers: [],
194
- warnings: install.warnings,
195
- ok: false,
196
- };
197
- }
198
-
199
- const warnings = [...install.warnings];
200
- const version = getCodexVersion();
201
-
202
- const configText = readConfigText(opts.configPath);
203
- const approval = checkApprovalMode(configText, opts);
204
-
205
- const mcp = await checkMcpHealth(opts.mcpServers, configText);
206
- warnings.push(...mcp.warnings);
207
-
208
- const drift = detectWorkdirDrift(opts.workdir, configText);
209
- warnings.push(...drift.warnings);
210
-
211
- return {
212
- codexPath: install.codexPath,
213
- version,
214
- needsBypass: approval.needsBypass,
215
- excludeMcpServers: mcp.excludeMcpServers,
216
- warnings,
217
- ok: true,
218
- };
219
- }
@@ -1,70 +0,0 @@
1
- const OFF_VALUES = new Set(["0", "false", "off", "no"]);
2
- const ON_VALUES = new Set(["1", "true", "on", "yes"]);
3
-
4
- function normalizeEnvValue(name) {
5
- return String(process.env[name] ?? "")
6
- .trim()
7
- .toLowerCase();
8
- }
9
-
10
- function isExplicitlyOff(name) {
11
- return OFF_VALUES.has(normalizeEnvValue(name));
12
- }
13
-
14
- function isExplicitlyOn(name) {
15
- return ON_VALUES.has(normalizeEnvValue(name));
16
- }
17
-
18
- function normalizeEnv(env, name) {
19
- return String(env?.[name] ?? "")
20
- .trim()
21
- .toLowerCase();
22
- }
23
-
24
- function isEnvOff(env, name) {
25
- return OFF_VALUES.has(normalizeEnv(env, name));
26
- }
27
-
28
- function isEnvOn(env, name) {
29
- return ON_VALUES.has(normalizeEnv(env, name));
30
- }
31
-
32
- /**
33
- * Resolve the C6a control plane without mutating environment or runtime
34
- * state. Callers own storing and incrementing the returned generation.
35
- */
36
- export function resolveRoleControlSnapshot(env = process.env, options = {}) {
37
- const masterEnabled = !isEnvOff(env, "TFX_CTO");
38
- const ctoManagerEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_MANAGER");
39
- const leadManagerEnabled =
40
- ctoManagerEnabled && isEnvOn(env, "TFX_LEAD_MANAGER");
41
- const northStarEnabled = masterEnabled && isEnvOn(env, "TFX_CTO_NORTH_STAR");
42
- const autoCollectEnabled =
43
- masterEnabled && isEnvOn(env, "TFX_CTO_AUTO_COLLECT");
44
- const values = {
45
- master_enabled: masterEnabled,
46
- cto_manager_enabled: ctoManagerEnabled,
47
- lead_manager_enabled: leadManagerEnabled,
48
- north_star_enabled: northStarEnabled,
49
- auto_collect_enabled: autoCollectEnabled,
50
- };
51
- const previous = options.previous;
52
- const changed = Object.keys(values).some(
53
- (field) => previous?.[field] !== values[field],
54
- );
55
- const generation = previous
56
- ? Number(previous.generation || 0) + (changed ? 1 : 0)
57
- : Number.isSafeInteger(options.generation) && options.generation >= 0
58
- ? options.generation
59
- : 0;
60
- return { generation, ...values };
61
- }
62
-
63
- export function isCtoDisabled() {
64
- return isExplicitlyOff("TFX_CTO");
65
- }
66
-
67
- export function isCtoRetentionEnabled() {
68
- if (isExplicitlyOff("TFX_CTO")) return false;
69
- return isExplicitlyOn("TFX_CTO_RETENTION");
70
- }
@@ -1,226 +0,0 @@
1
- import { existsSync, readFileSync, statSync } from "fs";
2
- import { basename, dirname, isAbsolute, resolve } from "path";
3
-
4
- const URL_LIKE_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//u;
5
- const WINDOWS_ABS_RE = /^[a-zA-Z]:[\\/]/u;
6
-
7
- function normalizeMarkdownTarget(raw) {
8
- const trimmed = String(raw || "").trim();
9
- const match = /^<?([^>\s]+)>?(?:\s+["'][^"']*["'])?$/u.exec(trimmed);
10
- return match ? match[1] : trimmed;
11
- }
12
-
13
- function sanitizeToken(raw) {
14
- let token = String(raw || "").trim();
15
- if (!token) return "";
16
- token = token
17
- .replace(/^[<(]+/u, "")
18
- .replace(/[)>.,;:!?]+$/u, "")
19
- .trim();
20
- return token;
21
- }
22
-
23
- function isLikelyPathToken(token) {
24
- if (!token || URL_LIKE_RE.test(token)) return false;
25
- if (
26
- token.startsWith("app://") ||
27
- token.startsWith("plugin://") ||
28
- token.startsWith("mention://")
29
- )
30
- return false;
31
- if (token === "~") return false;
32
-
33
- const hasPathSeparator = token.includes("/") || token.includes("\\");
34
- const hasDotPrefix =
35
- token.startsWith("./") || token.startsWith("../") || token.startsWith(".");
36
- const isHomeRelative = token.startsWith("~/") || token.startsWith("~\\");
37
- const isWindowsAbs = WINDOWS_ABS_RE.test(token);
38
- const hasFileLikeSuffix = /\.[a-zA-Z0-9]{1,16}$/u.test(token);
39
-
40
- return (
41
- hasPathSeparator ||
42
- hasDotPrefix ||
43
- isHomeRelative ||
44
- isWindowsAbs ||
45
- hasFileLikeSuffix
46
- );
47
- }
48
-
49
- export function extractExplicitFileTokens(text) {
50
- const content = String(text || "");
51
- const candidates = [];
52
-
53
- for (const match of content.matchAll(/\[[^\]]+\]\(([^)\n]+)\)/gu)) {
54
- candidates.push(normalizeMarkdownTarget(match[1]));
55
- }
56
- for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
57
- candidates.push(match[1]);
58
- }
59
- for (const match of content.matchAll(/"([^"\n]+)"/gu)) {
60
- candidates.push(match[1]);
61
- }
62
- for (const match of content.matchAll(/'([^'\n]+)'/gu)) {
63
- candidates.push(match[1]);
64
- }
65
-
66
- const unique = new Set();
67
- for (const raw of candidates) {
68
- const token = sanitizeToken(raw);
69
- if (!isLikelyPathToken(token)) continue;
70
- unique.add(token);
71
- }
72
-
73
- return Array.from(unique);
74
- }
75
-
76
- function validateTransferFile(filePath, maxBytes) {
77
- if (!existsSync(filePath)) {
78
- throw new Error(`referenced file not found: ${filePath}`);
79
- }
80
- const stats = statSync(filePath);
81
- if (!stats.isFile()) {
82
- throw new Error(`referenced path is not a file: ${filePath}`);
83
- }
84
- if (stats.size > maxBytes) {
85
- throw new Error(
86
- `referenced file too large: ${stats.size} bytes (max ${maxBytes}) for ${filePath}`,
87
- );
88
- }
89
- return stats.size;
90
- }
91
-
92
- function resolveReferencePath(token, handoffAbsPath, cwd) {
93
- if (WINDOWS_ABS_RE.test(token) || isAbsolute(token)) {
94
- return resolve(token);
95
- }
96
-
97
- if (token.startsWith("~/") || token.startsWith("~\\")) {
98
- if (!process.env.HOME && !process.env.USERPROFILE) {
99
- return null;
100
- }
101
- const homeDir = process.env.HOME || process.env.USERPROFILE;
102
- return resolve(homeDir, token.slice(2));
103
- }
104
-
105
- const handoffDir = dirname(handoffAbsPath);
106
- const primary = resolve(handoffDir, token);
107
- if (existsSync(primary)) {
108
- return primary;
109
- }
110
-
111
- const fallback = resolve(cwd, token);
112
- return existsSync(fallback) ? fallback : primary;
113
- }
114
-
115
- function toRemotePath(stageRoot, relativePath) {
116
- const root = String(stageRoot).replace(/\\/gu, "/").replace(/\/+$/u, "");
117
- const rel = String(relativePath).replace(/\\/gu, "/").replace(/^\/+/, "");
118
- return `${root}/${rel}`;
119
- }
120
-
121
- export function buildRemoteTransferPlan(options = {}) {
122
- const {
123
- cwd = process.cwd(),
124
- handoffPath = null,
125
- maxBytes,
126
- remoteStageRoot,
127
- userPrompt = null,
128
- maxReferenceFiles = 32,
129
- } = options;
130
-
131
- if (!handoffPath) {
132
- return {
133
- prompt: userPrompt || "",
134
- replacements: [],
135
- stagedHandoffPath: null,
136
- transfers: [],
137
- };
138
- }
139
-
140
- if (!remoteStageRoot) {
141
- throw new Error("remoteStageRoot is required when handoffPath is provided");
142
- }
143
-
144
- if (!Number.isFinite(maxBytes) || maxBytes <= 0) {
145
- throw new Error("maxBytes must be a positive number");
146
- }
147
-
148
- const absoluteHandoffPath = resolve(cwd, handoffPath);
149
- validateTransferFile(absoluteHandoffPath, maxBytes);
150
- const handoffContent = readFileSync(absoluteHandoffPath, "utf8").trim();
151
-
152
- const stagedHandoffPath = toRemotePath(
153
- remoteStageRoot,
154
- `handoff/${basename(absoluteHandoffPath)}`,
155
- );
156
- const transfers = [
157
- {
158
- localPath: absoluteHandoffPath,
159
- remotePath: stagedHandoffPath,
160
- type: "handoff",
161
- },
162
- ];
163
-
164
- const tokens = extractExplicitFileTokens(handoffContent);
165
- if (tokens.length > maxReferenceFiles) {
166
- throw new Error(
167
- `too many referenced files: ${tokens.length} (max ${maxReferenceFiles})`,
168
- );
169
- }
170
-
171
- const stagedByLocalPath = new Map();
172
- const replacements = [];
173
- let fileIndex = 0;
174
-
175
- for (const token of tokens) {
176
- const resolvedPath = resolveReferencePath(token, absoluteHandoffPath, cwd);
177
- validateTransferFile(resolvedPath, maxBytes);
178
-
179
- if (!stagedByLocalPath.has(resolvedPath)) {
180
- fileIndex += 1;
181
- const stagedPath = toRemotePath(
182
- remoteStageRoot,
183
- `refs/${String(fileIndex).padStart(2, "0")}-${basename(resolvedPath)}`,
184
- );
185
- stagedByLocalPath.set(resolvedPath, stagedPath);
186
- transfers.push({
187
- localPath: resolvedPath,
188
- remotePath: stagedPath,
189
- type: "reference",
190
- });
191
- }
192
-
193
- replacements.push({
194
- token,
195
- stagedPath: stagedByLocalPath.get(resolvedPath),
196
- });
197
- }
198
-
199
- const replacementEntries = Array.from(
200
- replacements
201
- .reduce((map, entry) => map.set(entry.token, entry.stagedPath), new Map())
202
- .entries(),
203
- ).sort((a, b) => b[0].length - a[0].length);
204
-
205
- let rewrittenHandoff = handoffContent;
206
- for (const [token, stagedPath] of replacementEntries) {
207
- rewrittenHandoff = rewrittenHandoff.split(token).join(stagedPath);
208
- }
209
-
210
- const prefix = `Staged handoff file: ${stagedHandoffPath}`;
211
- let prompt = rewrittenHandoff ? `${prefix}\n\n${rewrittenHandoff}` : prefix;
212
-
213
- if (userPrompt) {
214
- prompt = `${prompt}\n\n---\n\n${userPrompt}`;
215
- }
216
-
217
- return {
218
- prompt,
219
- replacements: replacementEntries.map(([token, stagedPath]) => ({
220
- stagedPath,
221
- token,
222
- })),
223
- stagedHandoffPath,
224
- transfers,
225
- };
226
- }
@@ -1,151 +0,0 @@
1
- // Codex와 Antigravity 세션 hook이 공유하는 presence 기록 함수.
2
- import { execFileSync } from "node:child_process";
3
- import { join } from "node:path";
4
- import { resolveRoleControlSnapshot } from "../../hub/lib/cto-env.mjs";
5
-
6
- function parseStartPayload(stdinData) {
7
- try {
8
- const raw = typeof stdinData === "string" ? stdinData : "";
9
- return raw.trim() ? JSON.parse(raw) : {};
10
- } catch {
11
- return {};
12
- }
13
- }
14
-
15
- function inferParticipantCli(payload) {
16
- const direct = String(
17
- payload?.actor_cli || payload?.cli || payload?.actor?.cli || "",
18
- )
19
- .trim()
20
- .toLowerCase();
21
- if (direct) return direct;
22
-
23
- const eventName = String(payload?.hook_event_name || "").trim();
24
- if (eventName === "SessionStart") return "claude";
25
- if (eventName.toLowerCase().replace(/-/g, "_") === "session_start") {
26
- return "codex";
27
- }
28
- return "participant";
29
- }
30
-
31
- async function defaultAppendParticipantCtoEvent(lakeRoot, event) {
32
- const { appendCtoEvent } = await import("../../cto/events.mjs");
33
- return appendCtoEvent(lakeRoot, event, {
34
- lockRetries: 1,
35
- lockRetryDelayMs: 0,
36
- });
37
- }
38
-
39
- async function defaultResolveParticipantLakeRoot(cwd) {
40
- const { resolveLakeRootDir } = await import("../../cto/lake-root.mjs");
41
- const projectRoot = resolveLakeRootDir(cwd, {
42
- execFileSync: (cmd, args, options = {}) =>
43
- execFileSync(cmd, args, {
44
- ...options,
45
- timeout: 500,
46
- killSignal: "SIGKILL",
47
- }),
48
- });
49
- return {
50
- projectRoot,
51
- lakeRoot: projectRoot ? join(projectRoot, ".triflux", "lake") : "",
52
- };
53
- }
54
-
55
- /** 참가 세션의 시작 이벤트만 기록한다. */
56
- export async function emitParticipantSessionStarted(stdinData, seams = {}) {
57
- try {
58
- if (
59
- !resolveRoleControlSnapshot(seams.env || process.env).auto_collect_enabled
60
- ) {
61
- return null;
62
- }
63
- const payload = parseStartPayload(stdinData);
64
- const sessionId = String(payload?.session_id || "").trim();
65
- if (!sessionId) return null;
66
- const cwd = typeof payload?.cwd === "string" ? payload.cwd : process.cwd();
67
- const resolveLakeRoot =
68
- seams.resolveLakeRoot || defaultResolveParticipantLakeRoot;
69
- const resolved = await resolveLakeRoot(cwd);
70
- const projectRoot =
71
- typeof resolved === "string"
72
- ? resolved
73
- : String(resolved?.projectRoot || "");
74
- const lakeRoot =
75
- typeof resolved === "string"
76
- ? join(resolved, ".triflux", "lake")
77
- : String(resolved?.lakeRoot || "");
78
- if (!lakeRoot) return null;
79
-
80
- const event = {
81
- event: "session_started",
82
- source: "tfx_participant_hook",
83
- session_id: sessionId,
84
- project_root: projectRoot || cwd,
85
- worktree_path: cwd,
86
- branch: typeof payload?.branch === "string" ? payload.branch : "",
87
- status: "active",
88
- actor: {
89
- cli: inferParticipantCli(payload),
90
- session_id: sessionId,
91
- host: typeof payload?.host === "string" ? payload.host : "local",
92
- },
93
- summary: `${inferParticipantCli(payload)} session_started ${sessionId}`,
94
- now: seams.now,
95
- };
96
-
97
- const append = seams.ctoAppend || defaultAppendParticipantCtoEvent;
98
- return await append(lakeRoot, event);
99
- } catch {
100
- return null;
101
- }
102
- }
103
-
104
- function readAncestorCommands(pid = process.ppid, maxDepth = 6) {
105
- if (process.platform === "win32") return [];
106
- const commands = [];
107
- let currentPid = Number(pid);
108
- for (let depth = 0; depth < maxDepth; depth++) {
109
- if (!Number.isInteger(currentPid) || currentPid <= 1) break;
110
- try {
111
- const output = execFileSync(
112
- "ps",
113
- ["-o", "ppid=", "-o", "command=", "-p", String(currentPid)],
114
- {
115
- encoding: "utf8",
116
- timeout: 200,
117
- windowsHide: true,
118
- },
119
- ).trim();
120
- const match = output.match(/^(\d+)\s+([\s\S]+)$/);
121
- if (!match) break;
122
- commands.push(match[2]);
123
- currentPid = Number(match[1]);
124
- } catch {
125
- break;
126
- }
127
- }
128
- return commands;
129
- }
130
-
131
- function commandUsesClaudePrintMode(command) {
132
- return /\bclaude(?:\s+\S+)*\s+(?:--print|-p)(?:\s|=|$)/u.test(
133
- String(command || ""),
134
- );
135
- }
136
-
137
- export function shouldSkipInteractiveRegistration(payload, seams = {}) {
138
- const declaredKind = String(
139
- payload?.sessionKind || payload?.session_kind || "",
140
- )
141
- .trim()
142
- .toLowerCase();
143
- if (declaredKind === "headless") return true;
144
-
145
- const ancestorCommands = Array.isArray(seams.ancestorCommands)
146
- ? seams.ancestorCommands
147
- : readAncestorCommands(seams.parentPid);
148
- return ancestorCommands.some((command) =>
149
- commandUsesClaudePrintMode(command),
150
- );
151
- }