@triflux/remote 10.50.1 → 10.51.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 (120) hide show
  1. package/cto/brief.mjs +0 -12
  2. package/cto/collect.mjs +16 -247
  3. package/cto/current.schema.json +4 -20
  4. package/cto/events.mjs +13 -294
  5. package/cto/hygiene.mjs +26 -346
  6. package/cto/lake-root.mjs +114 -16
  7. package/cto/status.mjs +33 -128
  8. package/hub/index.mjs +0 -8
  9. package/hub/store-adapter.mjs +1 -1
  10. package/hub/team/agent-map.json +0 -1
  11. package/hub/team/ansi.mjs +1 -35
  12. package/hub/team/backend.mjs +9 -145
  13. package/hub/team/cli/commands/attach.mjs +1 -17
  14. package/hub/team/cli/commands/control.mjs +12 -38
  15. package/hub/team/cli/commands/debug.mjs +1 -37
  16. package/hub/team/cli/commands/focus.mjs +5 -31
  17. package/hub/team/cli/commands/interrupt.mjs +6 -25
  18. package/hub/team/cli/commands/kill.mjs +1 -30
  19. package/hub/team/cli/commands/list.mjs +0 -16
  20. package/hub/team/cli/commands/send.mjs +6 -26
  21. package/hub/team/cli/commands/start/index.mjs +41 -133
  22. package/hub/team/cli/commands/start/parse-args.mjs +15 -40
  23. package/hub/team/cli/commands/start/start-headless.mjs +138 -105
  24. package/hub/team/cli/commands/start/start-mux.mjs +0 -3
  25. package/hub/team/cli/commands/status.mjs +2 -57
  26. package/hub/team/cli/commands/stop.mjs +14 -19
  27. package/hub/team/cli/help.mjs +4 -5
  28. package/hub/team/cli/manifest.mjs +5 -1
  29. package/hub/team/cli/render.mjs +0 -12
  30. package/hub/team/cli/services/hub-client.mjs +2 -88
  31. package/hub/team/cli/services/runtime-mode.mjs +27 -98
  32. package/hub/team/handoff.mjs +3 -34
  33. package/hub/team/headless.mjs +14 -755
  34. package/hub/team/orchestrator.mjs +4 -30
  35. package/hub/team/pane.mjs +6 -28
  36. package/hub/team/psmux.mjs +16 -403
  37. package/hub/team/remote-session.mjs +1 -2
  38. package/hub/team/session.mjs +8 -119
  39. package/hub/team/swarm-intent.mjs +0 -198
  40. package/hub/team/terminal-opener.mjs +11 -35
  41. package/hub/team/tui-core.mjs +1 -1
  42. package/hub/team/tui-viewer.mjs +2 -2
  43. package/hub/team/tui.mjs +8 -269
  44. package/hub/team/wt-manager.mjs +2 -2
  45. package/hub/workers/factory.mjs +4 -37
  46. package/hub/workers/lib/jsonrpc-ws-uds.mjs +0 -2
  47. package/package.json +1 -1
  48. package/scripts/lib/agent-route-policy.mjs +162 -14
  49. package/scripts/lib/claudemd-scanner.mjs +2 -199
  50. package/scripts/lib/cli-claude.mjs +0 -2
  51. package/scripts/lib/context.mjs +3 -19
  52. package/scripts/lib/env-probe.mjs +5 -64
  53. package/scripts/lib/gemini-profiles.mjs +2 -69
  54. package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
  55. package/scripts/lib/machine-profile.mjs +3 -17
  56. package/scripts/lib/mcp-filter.mjs +109 -53
  57. package/scripts/lib/mcp-health.mjs +2 -2
  58. package/scripts/lib/mcp-server-catalog.mjs +2 -2
  59. package/scripts/lib/psmux-info.mjs +25 -104
  60. package/scripts/lib/skill-template.mjs +0 -271
  61. package/cto/hygiene-actions.mjs +0 -355
  62. package/cto/hygiene-notify.mjs +0 -170
  63. package/hub/team/build-worker-prompt.mjs +0 -92
  64. package/hub/team/check-mcp-hub.mjs +0 -49
  65. package/hub/team/claude-native-bridge.mjs +0 -987
  66. package/hub/team/cli/commands/start/start-in-process.mjs +0 -50
  67. package/hub/team/cli/commands/start/start-wt.mjs +0 -86
  68. package/hub/team/cli/services/native-control.mjs +0 -145
  69. package/hub/team/codex-review.mjs +0 -520
  70. package/hub/team/conductor-mesh-bridge.mjs +0 -122
  71. package/hub/team/conductor.mjs +0 -1529
  72. package/hub/team/consensus-meta.mjs +0 -176
  73. package/hub/team/cto-pull.mjs +0 -31
  74. package/hub/team/daemon-pty-tmux-bridge.mjs +0 -146
  75. package/hub/team/dashboard.mjs +0 -322
  76. package/hub/team/event-log.mjs +0 -84
  77. package/hub/team/execution-mode.mjs +0 -324
  78. package/hub/team/extract-completion-payload.mjs +0 -110
  79. package/hub/team/headless-bridge-session.mjs +0 -17
  80. package/hub/team/health-probe.mjs +0 -435
  81. package/hub/team/interactive-native-launcher.mjs +0 -119
  82. package/hub/team/interactive-native-worker.mjs +0 -80
  83. package/hub/team/interactive-tui-transport.mjs +0 -479
  84. package/hub/team/launcher-template.mjs +0 -144
  85. package/hub/team/mcp-selector.mjs +0 -145
  86. package/hub/team/native-supervisor.mjs +0 -460
  87. package/hub/team/native.mjs +0 -727
  88. package/hub/team/recovery-store.mjs +0 -149
  89. package/hub/team/remote-probe.mjs +0 -292
  90. package/hub/team/remote-watcher.mjs +0 -506
  91. package/hub/team/runtime-strategy.mjs +0 -200
  92. package/hub/team/schemas/worker-completion.json +0 -39
  93. package/hub/team/sentinel-capture.mjs +0 -127
  94. package/hub/team/swarm-cli.mjs +0 -390
  95. package/hub/team/swarm-hypervisor.mjs +0 -2206
  96. package/hub/team/swarm-locks.mjs +0 -335
  97. package/hub/team/swarm-planner.mjs +0 -371
  98. package/hub/team/swarm-preflight.mjs +0 -366
  99. package/hub/team/swarm-reconciler.mjs +0 -242
  100. package/hub/team/tui-remote-adapter.mjs +0 -377
  101. package/hub/team/worker-completion-validator.mjs +0 -79
  102. package/hub/team/worktree-lifecycle.mjs +0 -639
  103. package/hub/team/wt-templates.json +0 -43
  104. package/hub/workers/antigravity-route-worker.mjs +0 -179
  105. package/hub/workers/gemini-worker.mjs +0 -217
  106. package/scripts/lib/agent-json.mjs +0 -27
  107. package/scripts/lib/async.mjs +0 -174
  108. package/scripts/lib/cli-agy.mjs +0 -85
  109. package/scripts/lib/cli-codex.mjs +0 -62
  110. package/scripts/lib/cli-gemini.mjs +0 -37
  111. package/scripts/lib/dynamic-route-cli.mjs +0 -107
  112. package/scripts/lib/env.mjs +0 -14
  113. package/scripts/lib/hub.mjs +0 -48
  114. package/scripts/lib/pid.mjs +0 -63
  115. package/scripts/lib/quota.mjs +0 -47
  116. package/scripts/lib/skill-state.mjs +0 -220
  117. package/scripts/lib/team.mjs +0 -75
  118. package/scripts/lib/timeout.mjs +0 -66
  119. package/scripts/lib/tmp.mjs +0 -26
  120. package/scripts/lib/toml.mjs +0 -132
@@ -1,84 +0,0 @@
1
- // hub/team/event-log.mjs — JSONL 블랙박스 리코더
2
- // Conductor 세션 lifecycle 이벤트를 JSONL 파일에 기록한다.
3
- // 기존 hub/server.mjs의 batch-events.jsonl(MCP 이벤트)과 독립. 공존.
4
-
5
- import { createWriteStream, mkdirSync } from "node:fs";
6
- import { dirname } from "node:path";
7
-
8
- /**
9
- * JSONL event log 팩토리.
10
- * @param {string} filePath — 로그 파일 경로 (.jsonl)
11
- * @param {object} [opts]
12
- * @param {string} [opts.sessionId] — 모든 이벤트에 자동 삽입할 세션 ID
13
- * @returns {{ append, flush, close, filePath }}
14
- */
15
- export function createEventLog(filePath, opts = {}) {
16
- const { sessionId } = opts;
17
-
18
- mkdirSync(dirname(filePath), { recursive: true });
19
- const stream = createWriteStream(filePath, { flags: "a" });
20
-
21
- let closed = false;
22
- let pending = 0;
23
-
24
- /**
25
- * 이벤트를 JSONL 한 줄로 기록.
26
- * @param {string} event — 이벤트 타입 (spawn, health, kill, stateChange, ...)
27
- * @param {object} [data] — 이벤트 페이로드
28
- */
29
- function append(event, data = {}) {
30
- if (closed) return;
31
- const entry = {
32
- ts: new Date().toISOString(),
33
- ...(sessionId ? { session: sessionId } : {}),
34
- event,
35
- ...data,
36
- };
37
- pending += 1;
38
- stream.write(JSON.stringify(entry) + "\n", () => {
39
- pending -= 1;
40
- });
41
- }
42
-
43
- /**
44
- * 버퍼된 이벤트를 디스크에 flush.
45
- * @returns {Promise<void>}
46
- */
47
- function flush() {
48
- if (closed) return Promise.resolve();
49
- return new Promise((resolve, reject) => {
50
- stream.once("error", reject);
51
- stream.write("", () => {
52
- stream.removeListener("error", reject);
53
- resolve();
54
- });
55
- });
56
- }
57
-
58
- /**
59
- * 스트림 종료. flush 후 close.
60
- * @returns {Promise<void>}
61
- */
62
- function close() {
63
- if (closed) return Promise.resolve();
64
- closed = true;
65
- return new Promise((resolve) => {
66
- stream.end(() => resolve());
67
- });
68
- }
69
-
70
- return Object.freeze({
71
- append,
72
- flush,
73
- close,
74
- get filePath() {
75
- return filePath;
76
- },
77
- get pending() {
78
- return pending;
79
- },
80
- get closed() {
81
- return closed;
82
- },
83
- });
84
- }
@@ -1,324 +0,0 @@
1
- // hub/team/execution-mode.mjs — headless vs interactive execution mode selection
2
-
3
- import { existsSync, readFileSync } from "node:fs";
4
- import { dirname, resolve as pathResolve } from "node:path";
5
- import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
6
- import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
7
- import {
8
- resolveGeminiModel,
9
- resolveGeminiProfileForPurpose,
10
- } from "../../scripts/lib/gemini-profiles.mjs";
11
- import {
12
- buildDisabledCliError,
13
- normalizeCliName,
14
- resolveCliPolicy,
15
- } from "../../scripts/lib/machine-profile.mjs";
16
- import { whichCommand } from "@triflux/core/hub/platform.mjs";
17
-
18
- const WIN32_EXT_PRECEDENCE = [".cmd", ".exe", ".bat", ".ps1"];
19
-
20
- // #128: parse npm-cmd-shim wrapper to extract the underlying .js entry point.
21
- // npm shim wrappers end with: "%_prog%" "%dp0%\node_modules\...\<cli>.js" %*
22
- // %* is cmd batch arg pass-through which mangles multi-line / fenced prompts.
23
- // Bypass cmd entirely by spawning node + the .js directly.
24
- export function unwrapCmdToJsScript(cmdPath, opts = {}) {
25
- const readFile = opts.readFile || readFileSync;
26
- const existsFn = opts.existsSyncFn || existsSync;
27
- try {
28
- const content = readFile(cmdPath, "utf8");
29
- const match = content.match(/"%dp0%[\\/]([^"]+\.[cm]?js)"/i);
30
- if (!match) return null;
31
- const dir = dirname(cmdPath);
32
- const jsPath = pathResolve(dir, match[1].replace(/\\/g, "/"));
33
- return existsFn(jsPath) ? jsPath : null;
34
- } catch {
35
- return null;
36
- }
37
- }
38
-
39
- export const MODES = Object.freeze({
40
- HEADLESS: "headless",
41
- INTERACTIVE: "interactive",
42
- AUTO: "auto",
43
- });
44
-
45
- function asPrompt(prompt) {
46
- return typeof prompt === "string" ? prompt : "";
47
- }
48
-
49
- function pushFlag(args, flag, value) {
50
- if (typeof value === "string" && value.length > 0) {
51
- args.push(flag, value);
52
- }
53
- }
54
-
55
- // #116-C: codex headless 의 argv-inline prompt 가 macOS oh-my-codex hook
56
- // SessionStart Failed 회귀를 유발 (codex CLI v0.130.0). cli-adapter-base 의
57
- // buildExecCommand 가 shell command 형태에서 동일 fix 를 적용했고
58
- // (TFX_CODEX_STDIN_PROMPT=0 opt-out), 본 함수의 spawn-args 형태에서도 같은
59
- // 정신으로 prompt 를 stdin transport 로 분리한다. 동일 env var 를 재사용해
60
- // 사용자가 한 곳만 끄면 양쪽 path 모두 비활성.
61
- export function resolveStdinPromptMode(explicit, envSource) {
62
- if (typeof explicit === "boolean") return explicit;
63
- const env = (envSource || process.env).TFX_CODEX_STDIN_PROMPT;
64
- if (env === "0" || env === "false") return false;
65
- return true;
66
- }
67
-
68
- function normalizeCli(cli) {
69
- const value = String(cli || "codex").toLowerCase();
70
- if (value === "gemini" || value === "antigravity" || value === "agy") {
71
- return "antigravity";
72
- }
73
- return value;
74
- }
75
-
76
- export function resolveCliExecutable(cli, opts = {}) {
77
- const name = String(cli || "codex");
78
- const resolveCommand = opts.resolveCommand || whichCommand;
79
- const resolved = resolveCommand(name) || name;
80
-
81
- // Windows: Node spawn({ shell: false }) calls CreateProcess directly and does NOT
82
- // search PATHEXT. npm-installed CLIs (codex, gemini) live at `<npm>/codex` (Git Bash
83
- // shell script) alongside `<npm>/codex.cmd` (Windows batch wrapper). whichCommand
84
- // returns the extensionless path, which Windows cannot execute → ENOENT "The system
85
- // cannot find the file specified." Append the correct extension when the resolved
86
- // path has none.
87
- const platform = opts.platform || process.platform;
88
- if (platform === "win32" && resolved) {
89
- const hasExt = /\.[^\\/.]+$/.test(resolved);
90
- if (!hasExt) {
91
- const existsFn = opts.existsSyncFn || existsSync;
92
- for (const ext of WIN32_EXT_PRECEDENCE) {
93
- const candidate = `${resolved}${ext}`;
94
- try {
95
- if (existsFn(candidate)) return candidate;
96
- } catch {
97
- // ignore stat failures, try next extension
98
- }
99
- }
100
- }
101
- }
102
-
103
- return resolved;
104
- }
105
-
106
- export function buildSpawnSpecForMode(mode, opts = {}) {
107
- const cli = normalizeCli(opts.cli || "codex");
108
-
109
- // TFX_DISABLE_* SSOT 집행. route 를 우회하는 spawn 경로도 tfx-route.sh 와 같은
110
- // 정책을 따른다. 정책 판독은 런타임 process.env 와 machine profile 만 쓰고,
111
- // 자식에게 넘길 세션 env(opts.env)는 쓰지 않는다. 주입 시임은 opts.cliPolicy
112
- // (계산된 정책)와 opts.policyEnv(env 객체) 둘뿐이다.
113
- // claude 처럼 정책 대상이 아닌 CLI 는 프로파일 파일을 읽지 않는다.
114
- if (normalizeCliName(cli)) {
115
- const cliPolicy =
116
- opts.cliPolicy ?? resolveCliPolicy(opts.policyEnv ?? process.env);
117
- const blockedCli = buildDisabledCliError(cli, cliPolicy);
118
- if (blockedCli) throw blockedCli;
119
- }
120
-
121
- const prompt = asPrompt(opts.prompt);
122
- const commandName = cli === "antigravity" ? "agy" : cli;
123
- const resolvedCommand = resolveCliExecutable(commandName, opts);
124
- const platform = opts.platform || process.platform;
125
-
126
- // Node v20.12+ (CVE-2024-27980) rejects spawn of .cmd/.bat files with shell:false
127
- // (EINVAL). npm-installed Windows wrappers (e.g. codex.cmd) hit this.
128
- //
129
- // #128 (BUG-A): the cmd /c wrapper used to be the workaround, but cmd batch
130
- // %* arg pass-through mangles multi-line and fenced (```bash) prompts before
131
- // they reach the underlying .js. We now unwrap the .cmd to the node script
132
- // it launches and spawn `node <script>` directly, bypassing cmd entirely.
133
- // Falls back to cmd /c if the .cmd cannot be parsed.
134
- const needsCmdWrap =
135
- platform === "win32" && /\.(cmd|bat)$/i.test(resolvedCommand);
136
- const unwrappedJs = needsCmdWrap
137
- ? (opts.unwrapCmdFn || unwrapCmdToJsScript)(resolvedCommand)
138
- : null;
139
- const wrap = (args) => {
140
- if (unwrappedJs) {
141
- const nodeBin = opts.nodeExecPath || process.execPath;
142
- return { command: nodeBin, args: [unwrappedJs, ...args] };
143
- }
144
- if (needsCmdWrap) {
145
- return { command: "cmd", args: ["/c", resolvedCommand, ...args] };
146
- }
147
- return { command: resolvedCommand, args };
148
- };
149
-
150
- if (cli === "antigravity") {
151
- // agy 1.1.27: --print 는 값(프롬프트)을 받는 마지막 인자다. stdin 프롬프트는
152
- // "empty prompt" 로 거부되므로 argv 로 넘긴다.
153
- // effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
154
- const model =
155
- typeof opts.model === "string" && opts.model.trim()
156
- ? resolveGeminiModel(opts.model.trim(), {
157
- profilesPath: opts.geminiProfilesPath,
158
- })
159
- : resolveGeminiModel(
160
- resolveGeminiProfileForPurpose(opts.role || opts.agent),
161
- { profilesPath: opts.geminiProfilesPath },
162
- );
163
- const args = ["--dangerously-skip-permissions"];
164
- if (model) args.push("--model", model);
165
- args.push("--print", prompt);
166
- return {
167
- ...wrap(args),
168
- useExec: true,
169
- shell: false,
170
- stdinPrompt: false,
171
- prompt,
172
- };
173
- }
174
-
175
- if (mode === MODES.INTERACTIVE || mode === MODES.AUTO) {
176
- return { ...wrap([]), useExec: false, shell: false };
177
- }
178
-
179
- if (cli === "claude") {
180
- const args = [];
181
- pushFlag(args, "--model", opts.model);
182
- args.push("-p", prompt);
183
- return { ...wrap(args), useExec: true, shell: false };
184
- }
185
-
186
- const args = [];
187
- args.push(
188
- "exec",
189
- "--dangerously-bypass-approvals-and-sandbox",
190
- "--skip-git-repo-check",
191
- "--color",
192
- "never",
193
- );
194
- // Select the effort profile via `-c` config overrides instead of
195
- // `--profile <name>`: codex 0.134+ rejects `--profile X` whenever config.toml
196
- // still contains an inline [profiles.X] table (which codex re-injects on
197
- // config rewrite). These go straight to spawn argv (shell: false), so push
198
- // the raw TOML-scalar overrides without shell-quoting.
199
- const codexHome = opts.env?.CODEX_HOME;
200
- const codexProfile = resolveNestedCodexAgentProfile(opts.role || "executor", {
201
- profileOverride: opts.profile ?? "auto",
202
- globalProfile: opts.env?.TFX_CODEX_PROFILE ?? process.env.TFX_CODEX_PROFILE,
203
- codexHome,
204
- });
205
- for (const override of codexProfileConfigOverrides(codexProfile, {
206
- codexHome,
207
- disallowUltra: true,
208
- enforceCanonicalProfile: true,
209
- })) {
210
- args.push("-c", override);
211
- }
212
- if (Array.isArray(opts.mcpServers)) {
213
- const excludedMcpServers = new Set(
214
- (Array.isArray(opts.excludeMcpServers) ? opts.excludeMcpServers : [])
215
- .map((server) => String(server ?? "").trim())
216
- .filter(Boolean),
217
- );
218
- for (const rawServer of opts.mcpServers) {
219
- const server = String(rawServer ?? "").trim();
220
- if (!server) continue;
221
- if (excludedMcpServers.has(server)) {
222
- const message = `Skipping MCP server '${server}' because Codex preflight did not find an enabled config entry.`;
223
- if (typeof opts.onWarning === "function") {
224
- opts.onWarning(message, { type: "mcp_server_skipped", server });
225
- }
226
- continue;
227
- }
228
- args.push("-c", `mcp_servers.${server}.enabled=true`);
229
- }
230
- }
231
-
232
- // #116-C: codex headless 의 argv-inline prompt → macOS oh-my-codex hook
233
- // SessionStart Failed 회귀 (codex v0.130.0). default 가 stdin transport.
234
- // conductor.respawnSession 이 spec.prompt 를 child.stdin.write 로 주입한다.
235
- const useStdin =
236
- resolveStdinPromptMode(opts.stdinPrompt, opts.env) && prompt.length > 0;
237
- if (useStdin) {
238
- return {
239
- ...wrap(args),
240
- useExec: true,
241
- shell: false,
242
- stdinPrompt: true,
243
- prompt,
244
- };
245
- }
246
-
247
- args.push(prompt);
248
- return { ...wrap(args), useExec: true, shell: false };
249
- }
250
-
251
- /**
252
- * @param {{
253
- * cli: "codex"|"gemini"|"antigravity"|"agy"|"claude",
254
- * taskType?: "implement"|"review"|"research"|"test"|"analyze",
255
- * needsInput?: boolean,
256
- * estimatedDuration?: number,
257
- * hasHub?: boolean,
258
- * }} opts
259
- * @returns {{ mode: string, reason: string }}
260
- */
261
- export function selectExecutionMode(opts = {}) {
262
- const {
263
- cli: rawCli = "codex",
264
- taskType = "research",
265
- needsInput = false,
266
- estimatedDuration = 0,
267
- hasHub = false,
268
- } = opts;
269
-
270
- const cli = normalizeCli(rawCli);
271
-
272
- if (!hasHub) {
273
- return {
274
- mode: MODES.HEADLESS,
275
- reason: "interactive mode requires hub; falling back to headless",
276
- };
277
- }
278
-
279
- if (cli === "antigravity") {
280
- return {
281
- mode: MODES.HEADLESS,
282
- reason: "antigravity CLI uses headless stdin print mode",
283
- };
284
- }
285
-
286
- if (taskType === "implement" && !needsInput) {
287
- return {
288
- mode: MODES.HEADLESS,
289
- reason: "implementation without expected input fits headless execution",
290
- };
291
- }
292
-
293
- if (taskType === "review" || taskType === "analyze") {
294
- return {
295
- mode: MODES.HEADLESS,
296
- reason: "review and analyze tasks default to headless execution",
297
- };
298
- }
299
-
300
- if (needsInput === true) {
301
- return {
302
- mode: MODES.INTERACTIVE,
303
- reason: "task may require operator input during execution",
304
- };
305
- }
306
-
307
- if (estimatedDuration > 300) {
308
- return {
309
- mode: MODES.INTERACTIVE,
310
- reason: "long-running work benefits from monitored interactive mode",
311
- };
312
- }
313
-
314
- return {
315
- mode: MODES.HEADLESS,
316
- reason: "defaulting to headless execution",
317
- };
318
- }
319
-
320
- // buildCommandForMode 는 caller 가 없어 제거됨 (v10.20.x). argv-inline 프롬프트와
321
- // `-s danger-full-access` flag 가 PR #252 의 stdin redirect 패턴과 정합하지
322
- // 않아 회귀 위험이 있었다. 실제 헤드리스 spawn 은 buildSpawnSpecForMode
323
- // (conductor.mjs:544) 가 담당하며, 셸 명령 형태가 필요한 경우는
324
- // hub/cli-adapter-base.mjs:buildExecCommand 를 사용한다.
@@ -1,110 +0,0 @@
1
- // hub/team/extract-completion-payload.mjs — worker stdout tail → completion JSON
2
- //
3
- // Two-tier extraction:
4
- // 1) Sentinel-framed (preferred, #125): worker emits the payload between
5
- // `<<<TFX_COMPLETION_BEGIN>>>` and `<<<TFX_COMPLETION_END>>>` markers
6
- // that each occupy their own line. If a standalone-line BEGIN is present
7
- // but END is missing, this is a deterministic truncation signal and we
8
- // return null instead of falling back. Substring matches inside log
9
- // lines (e.g. `[debug] saw <<<TFX_COMPLETION_BEGIN>>> earlier`) do NOT
10
- // trigger Tier 1 (Codex R1 LOW finding).
11
- // 2) Brace-scan fallback (legacy, #115): for workers that haven't adopted
12
- // the sentinel protocol yet (or for unit fixtures), scan the tail for
13
- // the latest `{...}` pair that parses as a JSON object.
14
- //
15
- // The hypervisor's F7 guard (validateWorkerCompletion) decides whether the
16
- // parsed payload is semantically valid — this layer only handles extraction.
17
-
18
- import { SENTINEL_BEGIN, SENTINEL_END } from "./sentinel-capture.mjs";
19
-
20
- function isLineEdgeChar(ch) {
21
- return ch === "\n" || ch === "\r";
22
- }
23
-
24
- function findStandaloneMarkerLast(text, marker) {
25
- let from = 0;
26
- let last = -1;
27
- while (from <= text.length - marker.length) {
28
- const idx = text.indexOf(marker, from);
29
- if (idx === -1) return last;
30
- const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
31
- const afterIdx = idx + marker.length;
32
- const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
33
- if (before && after) last = idx;
34
- from = idx + 1;
35
- }
36
- return last;
37
- }
38
-
39
- function findStandaloneMarkerFrom(text, marker, fromIdx) {
40
- let from = fromIdx;
41
- while (from <= text.length - marker.length) {
42
- const idx = text.indexOf(marker, from);
43
- if (idx === -1) return -1;
44
- const before = idx === 0 || isLineEdgeChar(text[idx - 1]);
45
- const afterIdx = idx + marker.length;
46
- const after = afterIdx === text.length || isLineEdgeChar(text[afterIdx]);
47
- if (before && after) return idx;
48
- from = idx + 1;
49
- }
50
- return -1;
51
- }
52
-
53
- /**
54
- * @param {string} tail raw stdout (sentinel snapshot or 16 KiB sliding tail)
55
- * @returns {{payload: object} | null}
56
- */
57
- export function extractCompletionPayload(tail) {
58
- if (typeof tail !== "string" || tail.length === 0) return null;
59
-
60
- // Tier 1: sentinel-framed. Use last standalone-line BEGIN so a worker that
61
- // emitted multiple payloads (retry, debug log) yields the most recent one.
62
- const beginIdx = findStandaloneMarkerLast(tail, SENTINEL_BEGIN);
63
- if (beginIdx !== -1) {
64
- const innerStart = beginIdx + SENTINEL_BEGIN.length;
65
- const endIdx = findStandaloneMarkerFrom(tail, SENTINEL_END, innerStart);
66
- if (endIdx === -1) return null; // BEGIN without END → truncation/incomplete
67
- const inner = tail.slice(innerStart, endIdx).trim();
68
- try {
69
- const parsed = JSON.parse(inner);
70
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
71
- return { payload: parsed };
72
- }
73
- } catch {
74
- // Sentinel-framed but invalid JSON — do not fall back to brace scan,
75
- // because the worker explicitly tried (and failed) to use the protocol.
76
- }
77
- return null;
78
- }
79
-
80
- // Tier 2: brace-scan fallback. Iterate from the latest `}` backward; for
81
- // each close, try every `{` that sits before it (nearest first, widening
82
- // outward). Tolerates trailing noise containing `}` (`noise } more`) and
83
- // dangling `{` (`{ broken`) after the real payload.
84
- const closes = [];
85
- const opens = [];
86
- for (let i = 0; i < tail.length; i += 1) {
87
- const ch = tail[i];
88
- if (ch === "}") closes.push(i);
89
- else if (ch === "{") opens.push(i);
90
- }
91
- if (closes.length === 0 || opens.length === 0) return null;
92
-
93
- for (let c = closes.length - 1; c >= 0; c -= 1) {
94
- const closeIdx = closes[c];
95
- for (let o = opens.length - 1; o >= 0; o -= 1) {
96
- const openIdx = opens[o];
97
- if (openIdx >= closeIdx) continue;
98
- const candidate = tail.slice(openIdx, closeIdx + 1);
99
- try {
100
- const parsed = JSON.parse(candidate);
101
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
102
- return { payload: parsed };
103
- }
104
- } catch {
105
- // try an earlier `{` for this close, then an earlier `}`.
106
- }
107
- }
108
- }
109
- return null;
110
- }
@@ -1,17 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- export async function writeBridgeSession(sessionId, socketPath, baseDir) {
5
- const sessionsDir = path.join(baseDir, "sessions");
6
- await fs.mkdir(sessionsDir, { recursive: true });
7
-
8
- const sessionData = {
9
- session_id: sessionId,
10
- messagingSock: socketPath,
11
- status: "RUNNING",
12
- created_at: new Date().toISOString(),
13
- };
14
-
15
- const filePath = path.join(sessionsDir, `${sessionId}.json`);
16
- await fs.writeFile(filePath, JSON.stringify(sessionData, null, 2), "utf8");
17
- }