triflux 10.3.4 → 10.7.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 (329) hide show
  1. package/LICENSE +21 -21
  2. package/bin/tfx-doctor-tui.mjs +1 -1
  3. package/bin/tfx-doctor.mjs +6 -1
  4. package/bin/tfx-profile.mjs +1 -1
  5. package/bin/tfx-setup-tui.mjs +1 -1
  6. package/bin/tfx-setup.mjs +6 -1
  7. package/bin/triflux.mjs +2396 -1140
  8. package/hooks/agent-route-guard.mjs +12 -8
  9. package/hooks/cross-review-tracker.mjs +21 -8
  10. package/hooks/error-context.mjs +19 -7
  11. package/hooks/hook-adaptive-collector.mjs +18 -16
  12. package/hooks/hook-manager.mjs +93 -32
  13. package/hooks/hook-orchestrator.mjs +108 -24
  14. package/hooks/hook-registry.json +11 -0
  15. package/hooks/keyword-rules.json +6 -10
  16. package/hooks/lib/resolve-root.mjs +1 -1
  17. package/hooks/mcp-config-watcher.mjs +6 -2
  18. package/hooks/pipeline-stop.mjs +3 -6
  19. package/hooks/safety-guard.mjs +99 -28
  20. package/hooks/session-start-fast.mjs +143 -0
  21. package/hooks/subagent-verifier.mjs +5 -4
  22. package/hub/account-broker.mjs +256 -60
  23. package/hub/adaptive-diagnostic.mjs +75 -48
  24. package/hub/adaptive-inject.mjs +95 -57
  25. package/hub/adaptive-memory.mjs +156 -42
  26. package/hub/adaptive.mjs +60 -31
  27. package/hub/assign-callbacks.mjs +67 -30
  28. package/hub/bridge.mjs +0 -1
  29. package/hub/cli-adapter-base.mjs +200 -48
  30. package/hub/codex-adapter.mjs +76 -96
  31. package/hub/codex-compat.mjs +3 -3
  32. package/hub/codex-preflight.mjs +63 -37
  33. package/hub/delegator/contracts.mjs +19 -23
  34. package/hub/delegator/index.mjs +3 -3
  35. package/hub/delegator/service.mjs +88 -64
  36. package/hub/delegator/tool-definitions.mjs +5 -5
  37. package/hub/fullcycle.mjs +33 -17
  38. package/hub/gemini-adapter.mjs +69 -94
  39. package/hub/hitl.mjs +89 -30
  40. package/hub/intent.mjs +161 -38
  41. package/hub/lib/cache-guard.mjs +43 -17
  42. package/hub/lib/mcp-response-cache.mjs +66 -32
  43. package/hub/lib/memory-store.mjs +285 -111
  44. package/hub/lib/path-utils.mjs +35 -37
  45. package/hub/lib/process-utils.mjs +106 -37
  46. package/hub/lib/spawn-trace.mjs +527 -0
  47. package/hub/lib/ssh-command.mjs +34 -4
  48. package/hub/lib/ssh-retry.mjs +5 -1
  49. package/hub/lib/uuidv7.mjs +4 -3
  50. package/hub/memory-doctor.mjs +266 -106
  51. package/hub/middleware/request-logger.mjs +61 -34
  52. package/hub/paths.mjs +9 -9
  53. package/hub/pipeline/gates/confidence.mjs +34 -15
  54. package/hub/pipeline/gates/consensus.mjs +27 -15
  55. package/hub/pipeline/gates/index.mjs +7 -3
  56. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  57. package/hub/pipeline/index.mjs +77 -42
  58. package/hub/pipeline/state.mjs +10 -10
  59. package/hub/pipeline/transitions.mjs +40 -23
  60. package/hub/platform.mjs +57 -48
  61. package/hub/promote-penalties.mjs +25 -7
  62. package/hub/quality/deslop.mjs +70 -49
  63. package/hub/research.mjs +32 -25
  64. package/hub/router.mjs +240 -107
  65. package/hub/routing/complexity.mjs +132 -29
  66. package/hub/routing/index.mjs +17 -12
  67. package/hub/routing/q-learning.mjs +76 -28
  68. package/hub/server.mjs +4 -4
  69. package/hub/session-fingerprint.mjs +126 -60
  70. package/hub/state.mjs +84 -43
  71. package/hub/store-adapter.mjs +59 -26
  72. package/hub/store.mjs +356 -153
  73. package/hub/team/agent-map.json +22 -7
  74. package/hub/team/ansi.mjs +186 -122
  75. package/hub/team/backend.mjs +28 -10
  76. package/hub/team/cli/commands/attach.mjs +29 -9
  77. package/hub/team/cli/commands/control.mjs +29 -8
  78. package/hub/team/cli/commands/debug.mjs +32 -11
  79. package/hub/team/cli/commands/focus.mjs +38 -11
  80. package/hub/team/cli/commands/interrupt.mjs +18 -6
  81. package/hub/team/cli/commands/kill.mjs +16 -5
  82. package/hub/team/cli/commands/list.mjs +11 -4
  83. package/hub/team/cli/commands/send.mjs +19 -6
  84. package/hub/team/cli/commands/start/index.mjs +154 -31
  85. package/hub/team/cli/commands/start/parse-args.mjs +38 -11
  86. package/hub/team/cli/commands/start/start-headless.mjs +112 -36
  87. package/hub/team/cli/commands/start/start-in-process.mjs +12 -2
  88. package/hub/team/cli/commands/start/start-mux.mjs +70 -21
  89. package/hub/team/cli/commands/start/start-wt.mjs +29 -12
  90. package/hub/team/cli/commands/status.mjs +43 -14
  91. package/hub/team/cli/commands/stop.mjs +11 -4
  92. package/hub/team/cli/commands/task.mjs +8 -3
  93. package/hub/team/cli/commands/tasks.mjs +1 -1
  94. package/hub/team/cli/index.mjs +2 -2
  95. package/hub/team/cli/manifest.mjs +38 -8
  96. package/hub/team/cli/render.mjs +30 -8
  97. package/hub/team/cli/services/attach-fallback.mjs +31 -11
  98. package/hub/team/cli/services/hub-client.mjs +42 -14
  99. package/hub/team/cli/services/member-selector.mjs +11 -4
  100. package/hub/team/cli/services/native-control.mjs +48 -21
  101. package/hub/team/cli/services/runtime-mode.mjs +2 -1
  102. package/hub/team/cli/services/state-store.mjs +25 -8
  103. package/hub/team/cli/services/task-model.mjs +16 -6
  104. package/hub/team/conductor-mesh-bridge.mjs +24 -23
  105. package/hub/team/conductor.mjs +8 -4
  106. package/hub/team/dashboard-anchor.mjs +4 -5
  107. package/hub/team/dashboard-layout.mjs +3 -1
  108. package/hub/team/dashboard-open.mjs +41 -21
  109. package/hub/team/dashboard.mjs +76 -28
  110. package/hub/team/event-log.mjs +18 -10
  111. package/hub/team/handoff.mjs +31 -15
  112. package/hub/team/headless.mjs +2 -1
  113. package/hub/team/health-probe.mjs +69 -54
  114. package/hub/team/launcher-template.mjs +16 -13
  115. package/hub/team/native-supervisor.mjs +65 -21
  116. package/hub/team/native.mjs +74 -35
  117. package/hub/team/nativeProxy.mjs +184 -113
  118. package/hub/team/notify.mjs +119 -76
  119. package/hub/team/orchestrator.mjs +9 -4
  120. package/hub/team/pane.mjs +12 -7
  121. package/hub/team/process-cleanup.mjs +25 -16
  122. package/hub/team/psmux.mjs +491 -201
  123. package/hub/team/remote-probe.mjs +68 -52
  124. package/hub/team/remote-session.mjs +117 -59
  125. package/hub/team/remote-watcher.mjs +61 -33
  126. package/hub/team/routing.mjs +51 -25
  127. package/hub/team/runtime-strategy.mjs +3 -1
  128. package/hub/team/session.mjs +98 -34
  129. package/hub/team/staleState.mjs +72 -30
  130. package/hub/team/swarm-locks.mjs +15 -13
  131. package/hub/team/swarm-planner.mjs +32 -21
  132. package/hub/team/swarm-reconciler.mjs +48 -23
  133. package/hub/team/tui-lite.mjs +266 -68
  134. package/hub/team/tui-remote-adapter.mjs +14 -10
  135. package/hub/team/tui-viewer.mjs +99 -43
  136. package/hub/team/tui.mjs +708 -271
  137. package/hub/team/worktree-lifecycle.mjs +152 -58
  138. package/hub/team/wt-manager.mjs +24 -14
  139. package/hub/token-mode.mjs +71 -71
  140. package/hub/tray.mjs +66 -23
  141. package/hub/workers/claude-worker.mjs +162 -118
  142. package/hub/workers/codex-mcp.mjs +192 -141
  143. package/hub/workers/delegator-mcp.mjs +507 -333
  144. package/hub/workers/factory.mjs +8 -8
  145. package/hub/workers/gemini-worker.mjs +115 -84
  146. package/hub/workers/interface.mjs +6 -1
  147. package/hub/workers/worker-utils.mjs +21 -14
  148. package/hud/colors.mjs +27 -9
  149. package/hud/constants.mjs +162 -26
  150. package/hud/context-monitor.mjs +82 -41
  151. package/hud/hud-qos-status.mjs +129 -49
  152. package/hud/mission-board.mjs +6 -3
  153. package/hud/providers/claude.mjs +226 -115
  154. package/hud/providers/codex.mjs +62 -22
  155. package/hud/providers/gemini.mjs +168 -56
  156. package/hud/renderers.mjs +384 -119
  157. package/hud/terminal.mjs +101 -31
  158. package/hud/utils.mjs +78 -38
  159. package/mesh/index.mjs +11 -5
  160. package/mesh/mesh-budget.mjs +18 -9
  161. package/mesh/mesh-heartbeat.mjs +1 -1
  162. package/mesh/mesh-queue.mjs +3 -5
  163. package/mesh/mesh-router.mjs +5 -4
  164. package/package.json +2 -1
  165. package/scripts/__tests__/gen-skill-docs.test.mjs +36 -7
  166. package/scripts/__tests__/keyword-detector.test.mjs +77 -28
  167. package/scripts/__tests__/mcp-guard-engine.test.mjs +58 -20
  168. package/scripts/__tests__/remote-spawn-transfer.test.mjs +30 -19
  169. package/scripts/__tests__/remote-spawn.test.mjs +10 -4
  170. package/scripts/__tests__/session-start-fast.test.mjs +36 -0
  171. package/scripts/__tests__/skill-template.test.mjs +98 -50
  172. package/scripts/__tests__/smoke.test.mjs +1 -1
  173. package/scripts/__tests__/spawn-trace.test.mjs +102 -0
  174. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +48 -0
  175. package/scripts/cache-doctor.mjs +11 -4
  176. package/scripts/cache-warmup.mjs +96 -37
  177. package/scripts/claudemd-sync.mjs +27 -17
  178. package/scripts/codex-gateway-preflight.mjs +52 -37
  179. package/scripts/codex-mcp-gateway-sync.mjs +59 -39
  180. package/scripts/completions/tfx.bash +47 -47
  181. package/scripts/completions/tfx.fish +44 -44
  182. package/scripts/completions/tfx.zsh +83 -83
  183. package/scripts/config-audit.mjs +232 -0
  184. package/scripts/convert-to-tmpl.mjs +54 -0
  185. package/scripts/cross-review-gate.mjs +35 -12
  186. package/scripts/cross-review-tracker.mjs +21 -8
  187. package/scripts/demo.mjs +35 -17
  188. package/scripts/doctor-diagnose.mjs +284 -0
  189. package/scripts/gen-skill-docs.mjs +7 -2
  190. package/scripts/gen-skill-manifest.mjs +2 -1
  191. package/scripts/headless-guard.mjs +86 -48
  192. package/scripts/hub-ensure.mjs +45 -26
  193. package/scripts/keyword-detector.mjs +41 -20
  194. package/scripts/keyword-rules-expander.mjs +47 -30
  195. package/scripts/lib/claudemd-scanner.mjs +6 -1
  196. package/scripts/lib/context.mjs +3 -3
  197. package/scripts/lib/cross-review-utils.mjs +6 -3
  198. package/scripts/lib/env-probe.mjs +47 -28
  199. package/scripts/lib/gemini-profiles.mjs +44 -10
  200. package/scripts/lib/handoff.mjs +33 -17
  201. package/scripts/lib/hook-utils.mjs +8 -6
  202. package/scripts/lib/keyword-rules.mjs +43 -19
  203. package/scripts/lib/logger.mjs +24 -24
  204. package/scripts/lib/mcp-filter.mjs +377 -239
  205. package/scripts/lib/mcp-guard-engine.mjs +194 -79
  206. package/scripts/lib/mcp-manifest.mjs +23 -13
  207. package/scripts/lib/mcp-server-catalog.mjs +300 -63
  208. package/scripts/lib/psmux-info.mjs +11 -6
  209. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  210. package/scripts/lib/skill-template.mjs +30 -7
  211. package/scripts/mcp-check.mjs +58 -39
  212. package/scripts/mcp-gateway-config.mjs +83 -39
  213. package/scripts/mcp-gateway-ensure.mjs +43 -35
  214. package/scripts/mcp-gateway-integration-test.mjs +70 -58
  215. package/scripts/mcp-gateway-start.mjs +126 -60
  216. package/scripts/mcp-gateway-verify.mjs +24 -22
  217. package/scripts/mcp-safety-guard.mjs +44 -11
  218. package/scripts/notion-read.mjs +199 -84
  219. package/scripts/pack.mjs +94 -89
  220. package/scripts/preflight-cache.mjs +27 -10
  221. package/scripts/preinstall.mjs +42 -13
  222. package/scripts/remote-spawn.mjs +309 -94
  223. package/scripts/run.cjs +8 -5
  224. package/scripts/session-spawn-helper.mjs +130 -39
  225. package/scripts/session-stale-cleanup.mjs +123 -0
  226. package/scripts/setup.mjs +941 -492
  227. package/scripts/test-lock.mjs +20 -7
  228. package/scripts/test-tfx-route-no-claude-native.mjs +16 -12
  229. package/scripts/tfx-batch-stats.mjs +32 -11
  230. package/scripts/tfx-gate-activate.mjs +11 -4
  231. package/scripts/tfx-route-post.mjs +87 -20
  232. package/scripts/tfx-route-worker.mjs +57 -51
  233. package/scripts/tfx-route.sh +41 -124
  234. package/scripts/tmp-cleanup.mjs +21 -7
  235. package/scripts/token-snapshot.mjs +204 -85
  236. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  237. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  238. package/skills/.omc/state/last-tool-error.json +7 -0
  239. package/skills/.omc/state/subagent-tracking.json +7 -0
  240. package/skills/_templates/base.md +1 -6
  241. package/skills/merge-worktree/SKILL.md.tmpl +144 -0
  242. package/skills/shared/telemetry-segment.md +6 -0
  243. package/skills/star-prompt/SKILL.md.tmpl +222 -0
  244. package/skills/tfx-analysis/SKILL.md.tmpl +107 -0
  245. package/skills/tfx-analysis/skill.json +1 -6
  246. package/skills/tfx-auto/SKILL.md +1 -0
  247. package/skills/tfx-auto-codex/SKILL.md.tmpl +106 -0
  248. package/skills/tfx-auto-codex/skill.json +1 -3
  249. package/skills/tfx-autopilot/SKILL.md.tmpl +116 -0
  250. package/skills/tfx-autopilot/skill.json +1 -5
  251. package/skills/tfx-autoresearch/SKILL.md.tmpl +136 -0
  252. package/skills/tfx-autoroute/SKILL.md.tmpl +189 -0
  253. package/skills/tfx-autoroute/skill.json +1 -7
  254. package/skills/tfx-codex/SKILL.md +1 -0
  255. package/skills/tfx-codex/skill.json +1 -3
  256. package/skills/tfx-codex-swarm/SKILL.md.tmpl +16 -0
  257. package/skills/tfx-codex-swarm/evals/evals.json +1 -1
  258. package/skills/tfx-codex-swarm/skill.json +1 -4
  259. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +54 -12
  260. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +35 -7
  261. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +35 -7
  262. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +25 -5
  263. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +25 -5
  264. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +20 -4
  265. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +16 -4
  266. package/skills/tfx-consensus/SKILL.md.tmpl +146 -0
  267. package/skills/tfx-debate/SKILL.md.tmpl +192 -0
  268. package/skills/tfx-debate/skill.json +1 -7
  269. package/skills/tfx-deep-analysis/SKILL.md.tmpl +228 -0
  270. package/skills/tfx-deep-analysis/skill.json +1 -5
  271. package/skills/tfx-deep-interview/SKILL.md.tmpl +203 -0
  272. package/skills/tfx-deep-plan/SKILL.md.tmpl +282 -0
  273. package/skills/tfx-deep-qa/SKILL.md.tmpl +165 -0
  274. package/skills/tfx-deep-qa/skill.json +1 -6
  275. package/skills/tfx-deep-research/SKILL.md.tmpl +217 -0
  276. package/skills/tfx-deep-review/SKILL.md.tmpl +179 -0
  277. package/skills/tfx-doctor/SKILL.md +21 -0
  278. package/skills/tfx-doctor/SKILL.md.tmpl +172 -0
  279. package/skills/tfx-doctor/skill.json +1 -3
  280. package/skills/tfx-find/SKILL.md +1 -0
  281. package/skills/tfx-forge/SKILL.md.tmpl +187 -0
  282. package/skills/tfx-fullcycle/SKILL.md.tmpl +286 -0
  283. package/skills/tfx-fullcycle/skill.json +1 -6
  284. package/skills/tfx-gemini/SKILL.md.tmpl +91 -0
  285. package/skills/tfx-gemini/skill.json +1 -3
  286. package/skills/tfx-hooks/SKILL.md.tmpl +216 -0
  287. package/skills/tfx-hooks/skill.json +1 -3
  288. package/skills/tfx-hub/SKILL.md.tmpl +212 -0
  289. package/skills/tfx-hub/skill.json +1 -3
  290. package/skills/tfx-index/SKILL.md +1 -0
  291. package/skills/tfx-index/skill.json +1 -6
  292. package/skills/tfx-interview/SKILL.md.tmpl +285 -0
  293. package/skills/tfx-multi/SKILL.md.tmpl +183 -0
  294. package/skills/tfx-multi/skill.json +1 -3
  295. package/skills/tfx-panel/SKILL.md.tmpl +189 -0
  296. package/skills/tfx-panel/skill.json +1 -7
  297. package/skills/tfx-persist/SKILL.md.tmpl +270 -0
  298. package/skills/tfx-persist/skill.json +1 -7
  299. package/skills/tfx-plan/SKILL.md +1 -0
  300. package/skills/tfx-plan/skill.json +1 -6
  301. package/skills/tfx-profile/SKILL.md.tmpl +239 -0
  302. package/skills/tfx-profile/skill.json +1 -3
  303. package/skills/tfx-prune/SKILL.md.tmpl +200 -0
  304. package/skills/tfx-prune/skill.json +1 -7
  305. package/skills/tfx-psmux-rules/SKILL.md.tmpl +326 -0
  306. package/skills/tfx-psmux-rules/skill.json +1 -4
  307. package/skills/tfx-qa/SKILL.md +1 -0
  308. package/skills/tfx-qa/skill.json +1 -6
  309. package/skills/tfx-ralph/SKILL.md.tmpl +28 -0
  310. package/skills/tfx-ralph/skill.json +1 -4
  311. package/skills/tfx-remote-setup/SKILL.md.tmpl +576 -0
  312. package/skills/tfx-remote-setup/skill.json +1 -3
  313. package/skills/tfx-remote-spawn/SKILL.md.tmpl +263 -0
  314. package/skills/tfx-remote-spawn/references/hosts.json +16 -0
  315. package/skills/tfx-remote-spawn/skill.json +1 -4
  316. package/skills/tfx-research/SKILL.md +1 -0
  317. package/skills/tfx-review/SKILL.md +1 -0
  318. package/skills/tfx-review/skill.json +1 -6
  319. package/skills/tfx-setup/SKILL.md.tmpl +504 -0
  320. package/skills/tfx-setup/skill.json +1 -3
  321. package/skills/tfx-swarm/SKILL.md +22 -0
  322. package/skills/tfx-swarm/SKILL.md.tmpl +218 -0
  323. package/tui/codex-profile.mjs +88 -33
  324. package/tui/core.mjs +45 -15
  325. package/tui/doctor.mjs +75 -28
  326. package/tui/gemini-profile.mjs +74 -29
  327. package/tui/monitor-data.mjs +8 -4
  328. package/tui/monitor.mjs +71 -27
  329. package/tui/setup.mjs +133 -42
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ // scripts/doctor-diagnose.mjs — 진단 번들 생성기
3
+ //
4
+ // spawn-trace JSONL + process report + hook timing + spawn stats + system info
5
+ // → ~/.triflux/diagnostics/diag-{timestamp}.zip (PowerShell Compress-Archive)
6
+
7
+ import { execFileSync, execSync } from "node:child_process";
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { arch, cpus, freemem, homedir, platform, release, totalmem } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ const TRIFLUX_DIR = join(homedir(), ".triflux");
19
+ const LOGS_DIR = join(TRIFLUX_DIR, "logs");
20
+ const DIAG_DIR = join(TRIFLUX_DIR, "diagnostics");
21
+ const ONE_HOUR_MS = 60 * 60 * 1000;
22
+
23
+ function collectSpawnTraces(cutoffMs = ONE_HOUR_MS) {
24
+ if (!existsSync(LOGS_DIR)) return [];
25
+
26
+ const now = Date.now();
27
+ const entries = [];
28
+
29
+ for (const file of readdirSync(LOGS_DIR)) {
30
+ if (!file.startsWith("spawn-trace-") || !file.endsWith(".jsonl")) continue;
31
+ const lines = readFileSync(join(LOGS_DIR, file), "utf8")
32
+ .split("\n")
33
+ .filter(Boolean);
34
+ for (const line of lines) {
35
+ try {
36
+ const entry = JSON.parse(line);
37
+ const ts = new Date(entry.ts).getTime();
38
+ if (now - ts <= cutoffMs) entries.push(entry);
39
+ } catch { /* skip malformed */ }
40
+ }
41
+ }
42
+
43
+ return entries;
44
+ }
45
+
46
+ function computeSpawnStats(traces) {
47
+ if (traces.length === 0) {
48
+ return { total: 0, peakRatePerSec: 0, maxConcurrent: 0, blocked: 0 };
49
+ }
50
+
51
+ const spawnEvents = traces.filter((t) => t.event === "spawn");
52
+ const exitEvents = traces.filter((t) => t.event === "exit" || t.event === "error");
53
+ const blockedEvents = traces.filter((t) => t.event === "blocked");
54
+
55
+ // peak rate per second
56
+ let peakRate = 0;
57
+ for (const evt of spawnEvents) {
58
+ const ts = new Date(evt.ts).getTime();
59
+ const windowEnd = ts + 1000;
60
+ const inWindow = spawnEvents.filter((e) => {
61
+ const t = new Date(e.ts).getTime();
62
+ return t >= ts && t < windowEnd;
63
+ }).length;
64
+ if (inWindow > peakRate) peakRate = inWindow;
65
+ }
66
+
67
+ // max concurrent (spawn without matching exit)
68
+ const events = [
69
+ ...spawnEvents.map((e) => ({ ts: new Date(e.ts).getTime(), delta: 1 })),
70
+ ...exitEvents.map((e) => ({ ts: new Date(e.ts).getTime(), delta: -1 })),
71
+ ].sort((a, b) => a.ts - b.ts);
72
+
73
+ let concurrent = 0;
74
+ let maxConcurrent = 0;
75
+ for (const e of events) {
76
+ concurrent += e.delta;
77
+ if (concurrent > maxConcurrent) maxConcurrent = concurrent;
78
+ }
79
+
80
+ return {
81
+ total: spawnEvents.length,
82
+ peakRatePerSec: peakRate,
83
+ maxConcurrent,
84
+ blocked: blockedEvents.length,
85
+ };
86
+ }
87
+
88
+ function collectSystemInfo() {
89
+ const info = {
90
+ platform: platform(),
91
+ release: release(),
92
+ arch: arch(),
93
+ nodeVersion: process.version,
94
+ cpuModel: cpus()[0]?.model ?? "unknown",
95
+ cpuCores: cpus().length,
96
+ totalMemMB: Math.round(totalmem() / 1024 / 1024),
97
+ freeMemMB: Math.round(freemem() / 1024 / 1024),
98
+ };
99
+
100
+ // Windows Terminal version
101
+ try {
102
+ const wtVer = execSync("wt.exe --version 2>&1", {
103
+ encoding: "utf8",
104
+ timeout: 5000,
105
+ windowsHide: true,
106
+ }).trim();
107
+ info.wtVersion = wtVer;
108
+ } catch {
109
+ info.wtVersion = "not found";
110
+ }
111
+
112
+ // triflux version
113
+ try {
114
+ const pkgPath = join(homedir(), ".triflux", "package.json");
115
+ if (existsSync(pkgPath)) {
116
+ info.trifluxVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
117
+ }
118
+ } catch { /* ignore */ }
119
+
120
+ return info;
121
+ }
122
+
123
+ function collectHookTimings() {
124
+ // hook-orchestrator 또는 session-start-fast 로그에서 타이밍 추출
125
+ const hookLogDir = join(TRIFLUX_DIR, "logs");
126
+ if (!existsSync(hookLogDir)) return [];
127
+
128
+ const timings = [];
129
+ for (const file of readdirSync(hookLogDir)) {
130
+ if (!file.startsWith("hook-") || !file.endsWith(".jsonl")) continue;
131
+ try {
132
+ const lines = readFileSync(join(hookLogDir, file), "utf8")
133
+ .split("\n")
134
+ .filter(Boolean);
135
+ const now = Date.now();
136
+ for (const line of lines) {
137
+ try {
138
+ const entry = JSON.parse(line);
139
+ const ts = new Date(entry.ts || entry.time).getTime();
140
+ if (now - ts <= ONE_HOUR_MS) timings.push(entry);
141
+ } catch { /* skip */ }
142
+ }
143
+ } catch { /* skip */ }
144
+ }
145
+ return timings;
146
+ }
147
+
148
+ function generateSummary(stats, sysInfo, hookTimings, traceCount) {
149
+ const lines = [
150
+ "=== triflux diagnostic summary ===",
151
+ `generated: ${new Date().toISOString()}`,
152
+ "",
153
+ "--- System ---",
154
+ `OS: ${sysInfo.platform} ${sysInfo.release} (${sysInfo.arch})`,
155
+ `Node: ${sysInfo.nodeVersion}`,
156
+ `CPU: ${sysInfo.cpuModel} (${sysInfo.cpuCores} cores)`,
157
+ `RAM: ${sysInfo.freeMemMB}MB free / ${sysInfo.totalMemMB}MB total`,
158
+ `WT: ${sysInfo.wtVersion}`,
159
+ `triflux: ${sysInfo.trifluxVersion || "unknown"}`,
160
+ "",
161
+ "--- Spawn Stats (last 1h) ---",
162
+ `total spawns: ${stats.total}`,
163
+ `peak rate/sec: ${stats.peakRatePerSec}`,
164
+ `max concurrent: ${stats.maxConcurrent}`,
165
+ `blocked: ${stats.blocked}`,
166
+ `trace entries: ${traceCount}`,
167
+ "",
168
+ "--- Hook Timings (last 1h) ---",
169
+ ];
170
+
171
+ if (hookTimings.length === 0) {
172
+ lines.push("no hook timing data found");
173
+ } else {
174
+ for (const t of hookTimings.slice(-20)) {
175
+ const hook = t.hook || t.msg || "unknown";
176
+ const dur = t.dur_ms ?? t.duration_ms ?? "?";
177
+ lines.push(` ${hook}: ${dur}ms`);
178
+ }
179
+ }
180
+
181
+ return lines.join("\n");
182
+ }
183
+
184
+ export async function diagnose({ json = false } = {}) {
185
+ mkdirSync(DIAG_DIR, { recursive: true });
186
+
187
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
188
+ const bundleDir = join(DIAG_DIR, `diag-${timestamp}`);
189
+ mkdirSync(bundleDir, { recursive: true });
190
+
191
+ // 1. spawn-trace JSONL
192
+ const traces = collectSpawnTraces();
193
+ writeFileSync(
194
+ join(bundleDir, "blackbox.jsonl"),
195
+ traces.map((t) => JSON.stringify(t)).join("\n") + "\n",
196
+ );
197
+
198
+ // 2. process report
199
+ try {
200
+ const reportPath = process.report.writeReport(join(bundleDir, "process-report.json"));
201
+ // writeReport returns the path it wrote to
202
+ if (reportPath && reportPath !== join(bundleDir, "process-report.json")) {
203
+ // move if written elsewhere
204
+ const { renameSync } = await import("node:fs");
205
+ try { renameSync(reportPath, join(bundleDir, "process-report.json")); } catch { /* ignore */ }
206
+ }
207
+ } catch {
208
+ writeFileSync(join(bundleDir, "process-report.json"), JSON.stringify({ error: "report generation failed" }));
209
+ }
210
+
211
+ // 3. hook timings
212
+ const hookTimings = collectHookTimings();
213
+ writeFileSync(
214
+ join(bundleDir, "hook-timings.jsonl"),
215
+ hookTimings.map((t) => JSON.stringify(t)).join("\n") + "\n",
216
+ );
217
+
218
+ // 4. spawn stats
219
+ const stats = computeSpawnStats(traces);
220
+ writeFileSync(join(bundleDir, "spawn-stats.json"), JSON.stringify(stats, null, 2));
221
+
222
+ // 5. system info
223
+ const sysInfo = collectSystemInfo();
224
+ writeFileSync(join(bundleDir, "system-info.json"), JSON.stringify(sysInfo, null, 2));
225
+
226
+ // 6. summary
227
+ const summary = generateSummary(stats, sysInfo, hookTimings, traces.length);
228
+ writeFileSync(join(bundleDir, "summary.txt"), summary);
229
+
230
+ // 7. zip via PowerShell Compress-Archive
231
+ const zipPath = `${bundleDir}.zip`;
232
+ try {
233
+ execFileSync("powershell.exe", [
234
+ "-NoProfile", "-NonInteractive", "-Command",
235
+ `Compress-Archive -Path '${bundleDir}\\*' -DestinationPath '${zipPath}' -Force`,
236
+ ], { timeout: 30000, windowsHide: true });
237
+ } catch (err) {
238
+ // fallback: leave the directory unzipped
239
+ if (json) {
240
+ return { ok: false, bundleDir, error: `zip failed: ${err.message}` };
241
+ }
242
+ return { ok: false, bundleDir, zipPath: null, error: err.message };
243
+ }
244
+
245
+ // cleanup temp directory
246
+ try {
247
+ const { rmSync } = await import("node:fs");
248
+ rmSync(bundleDir, { recursive: true, force: true });
249
+ } catch { /* leave it */ }
250
+
251
+ const result = {
252
+ ok: true,
253
+ zipPath,
254
+ stats,
255
+ sysInfo,
256
+ traceCount: traces.length,
257
+ hookTimingCount: hookTimings.length,
258
+ };
259
+
260
+ if (json) {
261
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
262
+ }
263
+
264
+ return result;
265
+ }
266
+
267
+ // CLI direct execution
268
+ const isMain =
269
+ import.meta.url === `file:///${process.argv[1]?.replace(/\\/g, "/")}` ||
270
+ import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}`;
271
+
272
+ if (isMain) {
273
+ const json = process.argv.includes("--json");
274
+ const result = await diagnose({ json });
275
+ if (!json) {
276
+ if (result.ok) {
277
+ console.log(`\n 진단 번들 생성: ${result.zipPath}`);
278
+ console.log(` spawn 이벤트: ${result.traceCount}건, 훅 타이밍: ${result.hookTimingCount}건\n`);
279
+ } else {
280
+ console.error(` 진단 실패: ${result.error}`);
281
+ process.exit(1);
282
+ }
283
+ }
284
+ }
@@ -33,7 +33,9 @@ function collectSkillTemplateFiles(skillsDir) {
33
33
  .filter((file) => file.endsWith("SKILL.md.tmpl"))
34
34
  .filter(
35
35
  (file) =>
36
- !relative(skillsDir, file).replace(/\\/g, "/").startsWith("_templates/"),
36
+ !relative(skillsDir, file)
37
+ .replace(/\\/g, "/")
38
+ .startsWith("_templates/"),
37
39
  )
38
40
  .sort((left, right) => left.localeCompare(right));
39
41
  }
@@ -78,7 +80,10 @@ export function generateSkillDocs({
78
80
  generated.push({
79
81
  templatePath,
80
82
  outputPath,
81
- relativeTemplatePath: relative(skillsDir, templatePath).replace(/\\/g, "/"),
83
+ relativeTemplatePath: relative(skillsDir, templatePath).replace(
84
+ /\\/g,
85
+ "/",
86
+ ),
82
87
  context,
83
88
  rendered,
84
89
  });
@@ -23,7 +23,8 @@ function extractManifest(skillMdContent) {
23
23
  if (data.description) manifest.description = data.description;
24
24
  if (data.triggers) manifest.triggers = data.triggers;
25
25
  if (data["argument-hint"]) manifest.argument_hint = data["argument-hint"];
26
- if (data.internal === true || data.internal === "true") manifest.internal = true;
26
+ if (data.internal === true || data.internal === "true")
27
+ manifest.internal = true;
27
28
 
28
29
  return manifest;
29
30
  }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  /**
3
4
  * headless-guard.mjs — PreToolUse 훅 (상시 활성 auto-route)
4
5
  *
@@ -24,11 +25,11 @@
24
25
  * 성능: psmux 감지 결과를 5분간 캐시 ($TMPDIR/tfx-psmux-check.json)
25
26
  */
26
27
 
27
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
28
28
  import { execFileSync } from "node:child_process";
29
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
29
30
  import { tmpdir } from "node:os";
30
31
  import { join } from "node:path";
31
- import { nudge, deny } from "./lib/hook-utils.mjs";
32
+ import { deny, nudge } from "./lib/hook-utils.mjs";
32
33
  import { probePsmuxSupport } from "./lib/psmux-info.mjs";
33
34
 
34
35
  const CACHE_FILE = join(tmpdir(), "tfx-psmux-check.json");
@@ -37,8 +38,8 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
37
38
  // ── tfx-multi 상태 관리 (A+B) ──
38
39
  const MULTI_STATE_FILE = join(tmpdir(), "tfx-multi-state.json");
39
40
  const MULTI_EXPIRE_MS = 30 * 60 * 1000; // 30분 자동 만료
40
- const GATE_THRESHOLD = 2; // A: dispatch 전 허용할 Agent 호출 수
41
- const NUDGE_THRESHOLD = 4; // B: dispatch 후 nudge 트리거 횟수
41
+ const GATE_THRESHOLD = 2; // A: dispatch 전 허용할 Agent 호출 수
42
+ const NUDGE_THRESHOLD = 4; // B: dispatch 후 nudge 트리거 횟수
42
43
 
43
44
  function readMultiState() {
44
45
  try {
@@ -47,17 +48,25 @@ function readMultiState() {
47
48
  if (!state.active) return null;
48
49
  // 자동 만료
49
50
  if (Date.now() - state.activatedAt > MULTI_EXPIRE_MS) {
50
- try { unlinkSync(MULTI_STATE_FILE); } catch { /* ignore */ }
51
+ try {
52
+ unlinkSync(MULTI_STATE_FILE);
53
+ } catch {
54
+ /* ignore */
55
+ }
51
56
  return null;
52
57
  }
53
58
  return state;
54
- } catch { return null; }
59
+ } catch {
60
+ return null;
61
+ }
55
62
  }
56
63
 
57
64
  function writeMultiState(state) {
58
65
  try {
59
66
  writeFileSync(MULTI_STATE_FILE, JSON.stringify(state));
60
- } catch { /* ignore */ }
67
+ } catch {
68
+ /* ignore */
69
+ }
61
70
  }
62
71
 
63
72
  function isPsmuxInstalled() {
@@ -69,15 +78,27 @@ function isPsmuxInstalled() {
69
78
  if (age >= 0 && age < CACHE_TTL_MS) return cache.ok;
70
79
  // age < 0 → 미래 ts (오염) → 캐시 무시하고 재검사
71
80
  }
72
- } catch { /* cache miss */ }
81
+ } catch {
82
+ /* cache miss */
83
+ }
73
84
 
74
85
  const probe = probePsmuxSupport({ execFileSyncFn: execFileSync });
75
86
  const ok = probe.ok;
76
87
 
77
88
  // 캐시 저장
78
89
  try {
79
- writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), ok, version: probe.version, missingCommands: probe.missingCommands }));
80
- } catch { /* ignore */ }
90
+ writeFileSync(
91
+ CACHE_FILE,
92
+ JSON.stringify({
93
+ ts: Date.now(),
94
+ ok,
95
+ version: probe.version,
96
+ missingCommands: probe.missingCommands,
97
+ }),
98
+ );
99
+ } catch {
100
+ /* ignore */
101
+ }
81
102
 
82
103
  return ok;
83
104
  }
@@ -116,7 +137,7 @@ function parseRouteCommand(cmd) {
116
137
  // v3: 원본 명령에서 추가 플래그 추출
117
138
  const flags = {};
118
139
  const afterPrompt = cmd.replace(/'.+?'/gs, "").replace(/".+?"/gs, "");
119
- const timeoutMatch = afterPrompt.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
140
+ const timeoutMatch = afterPrompt.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
120
141
  if (timeoutMatch) flags.timeout = parseInt(timeoutMatch[1], 10);
121
142
 
122
143
  // 환경변수 기반 글로벌 플래그
@@ -127,18 +148,20 @@ function parseRouteCommand(cmd) {
127
148
  }
128
149
 
129
150
  function autoRoute(updatedCommand, reason) {
130
- process.stdout.write(JSON.stringify({
131
- hookSpecificOutput: {
132
- hookEventName: "PreToolUse",
133
- updatedInput: { command: updatedCommand },
134
- additionalContext: reason,
135
- },
136
- }));
151
+ process.stdout.write(
152
+ JSON.stringify({
153
+ hookSpecificOutput: {
154
+ hookEventName: "PreToolUse",
155
+ updatedInput: { command: updatedCommand },
156
+ additionalContext: reason,
157
+ },
158
+ }),
159
+ );
137
160
  process.exit(0);
138
161
  }
139
162
 
140
163
  const HEADLESS_FALLBACK_COMMAND =
141
- 'Bash("tfx multi --teammate-mode headless --assign \'codex:prompt:role\' ...")';
164
+ "Bash(\"tfx multi --teammate-mode headless --assign 'codex:prompt:role' ...\")";
142
165
  const DIRECT_CLI_BYPASS_HINT =
143
166
  "로컬 디버깅이 목적이면 TFX_ALLOW_DIRECT_CLI=1로 일시 우회할 수 있습니다.";
144
167
 
@@ -153,7 +176,7 @@ async function main() {
153
176
  for await (const chunk of process.stdin) raw += chunk;
154
177
 
155
178
  if (!raw?.trim()) {
156
- console.error('[headless-guard] stdin이 비어있습니다 — 기본 허용');
179
+ console.error("[headless-guard] stdin이 비어있습니다 — 기본 허용");
157
180
  process.exit(0);
158
181
  }
159
182
 
@@ -192,8 +215,8 @@ async function main() {
192
215
  if (/\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)) {
193
216
  deny(
194
217
  "[headless-guard] psmux send-keys/split-window에 codex/gemini 직접 호출이 포함되어 있습니다. " +
195
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
196
- DIRECT_CLI_BYPASS_HINT,
218
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
219
+ DIRECT_CLI_BYPASS_HINT,
197
220
  );
198
221
  }
199
222
  process.exit(0);
@@ -205,7 +228,7 @@ async function main() {
205
228
  // #37 Bug4: gh/git 명령은 본문에 codex/gemini 문자열이 있어도 차단하지 않음
206
229
  const SAFE_CMD_RE = /^\s*(?:[\w_]+=\S+\s+)*\s*(gh|git)\b/;
207
230
  const cmdParts = cmd.split(/\s*(?:&&|\|\||\||;)\s*/);
208
- let hasDirectCli = cmdParts.some(part => {
231
+ let hasDirectCli = cmdParts.some((part) => {
209
232
  // gh/git 세그먼트는 건너뜀 (이슈 본문/커밋 메시지 내 codex/gemini 언급은 정상)
210
233
  if (SAFE_CMD_RE.test(part)) return false;
211
234
  // 1단계: env var prefix 제거 (FOO=bar ...)
@@ -214,24 +237,31 @@ async function main() {
214
237
  .replace(/^\s*(?:[\w_]+=\S+\s+)*/, "")
215
238
  .replace(/^\s*(?:env|command|nohup)\s+/, "")
216
239
  .replace(/^\s*timeout\s+\d+\s+/, "")
217
- .replace(/^\s*(?:bash|sh)\s+(?:-\w+\s+)*(?:"([^"]*)".*|'([^']*)'.*)/i, "$1$2")
240
+ .replace(
241
+ /^\s*(?:bash|sh)\s+(?:-\w+\s+)*(?:"([^"]*)".*|'([^']*)'.*)/i,
242
+ "$1$2",
243
+ )
218
244
  .replace(/^\s*(?:\/[\w./+-]+\/)(codex|gemini)\b/, " $1");
219
- return /^\s*codex\b.*\bexec\b/i.test(stripped) || /^\s*gemini\s+(-p|--prompt)\b/i.test(stripped);
245
+ return (
246
+ /^\s*codex\b.*\bexec\b/i.test(stripped) ||
247
+ /^\s*gemini\s+(-p|--prompt)\b/i.test(stripped)
248
+ );
220
249
  });
221
250
  // 2차 휴리스틱: 1차 세그먼트 검사를 통과한 간접 실행 패턴 탐지
222
251
  // full AST 파서 대신 현실적 위협 벡터만 커버 — eval, subshell, variable 확장
223
252
  // 2차 휴리스틱: 간접 실행 패턴 탐지 (eval, subshell, variable 확장)
224
253
  if (!hasDirectCli) {
225
- const isAllSafeCmd = cmdParts.every(p => SAFE_CMD_RE.test(p));
254
+ const isAllSafeCmd = cmdParts.every((p) => SAFE_CMD_RE.test(p));
226
255
  if (isAllSafeCmd) {
227
256
  // gh/git 전용: $(codex exec ...) 직접 명령 치환만 차단
228
257
  // $(cat <<'EOF'\n...codex exec text...\nEOF) 같은 heredoc 텍스트는 허용
229
- hasDirectCli = /\$\(\s*(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd);
258
+ hasDirectCli = /\$\(\s*(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(
259
+ cmd,
260
+ );
230
261
  } else {
231
- hasDirectCli = (
262
+ hasDirectCli =
232
263
  /\beval\b.*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd) ||
233
- /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd)
234
- );
264
+ /\$[({].*\b(codex\s+exec|gemini\s+(-p|--prompt))\b/i.test(cmd);
235
265
  }
236
266
  }
237
267
 
@@ -242,15 +272,17 @@ async function main() {
242
272
  }
243
273
  deny(
244
274
  "[headless-guard] codex/gemini 직접 호출(파이프/복합 명령 포함)은 headless-guard에서 차단됩니다. " +
245
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
246
- DIRECT_CLI_BYPASS_HINT,
275
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
276
+ DIRECT_CLI_BYPASS_HINT,
247
277
  );
248
278
  }
249
279
 
250
280
  // tfx-route.sh 실행만 감지: 명령이 bash로 시작할 때만 (커밋 메시지/echo 등 무시)
251
281
  if (/^\s*bash\s+.*tfx-route\.sh\s/.test(cmd)) {
252
282
  // --async, --job-status, --job-result, --job-wait는 tfx-route.sh 내부 플래그 → 통과
253
- if (/tfx-route\.sh\s+--(async|job-status|job-result|job-wait)\b/.test(cmd)) {
283
+ if (
284
+ /tfx-route\.sh\s+--(async|job-status|job-result|job-wait)\b/.test(cmd)
285
+ ) {
254
286
  process.exit(0);
255
287
  }
256
288
 
@@ -261,7 +293,7 @@ async function main() {
261
293
  if (!process.env.TFX_FORCE_HEADLESS) {
262
294
  const isMultiWorker = /\s--(multi|parallel)\b/.test(cmd);
263
295
  if (!isMultiWorker) {
264
- process.exit(0); // 원본 tfx-route.sh 명령 그대로 통과
296
+ process.exit(0); // 원본 tfx-route.sh 명령 그대로 통과
265
297
  }
266
298
  }
267
299
 
@@ -272,10 +304,11 @@ async function main() {
272
304
  // v3: 플래그 빌더 — 하드코딩 제거, 원본 의도 보존
273
305
  const parts = ["tfx multi --teammate-mode headless"];
274
306
  if (!f.noAutoAttach) parts.push("--auto-attach");
275
- if (!f.noAutoAttach) parts.push("--dashboard"); // 워커 요약 스플릿이 기본
307
+ if (!f.noAutoAttach) parts.push("--dashboard"); // 워커 요약 스플릿이 기본
276
308
  if (f.verbose) parts.push("--verbose");
277
309
  parts.push(`--assign '${parsed.agent}:${safePrompt}:${parsed.agent}'`);
278
- if (parsed.mcp && VALID_MCP.has(parsed.mcp)) parts.push(`--mcp-profile ${parsed.mcp}`);
310
+ if (parsed.mcp && VALID_MCP.has(parsed.mcp))
311
+ parts.push(`--mcp-profile ${parsed.mcp}`);
279
312
  parts.push(`--timeout ${f.timeout || 600}`);
280
313
 
281
314
  const builtCmd = parts.join(" ");
@@ -286,7 +319,7 @@ async function main() {
286
319
  }
287
320
  deny(
288
321
  "[headless-guard] tfx-route.sh를 headless로 변환 실패. " +
289
- 'Bash("tfx multi --teammate-mode headless --assign \'cli:prompt:role\' ...") 형식을 사용하세요.',
322
+ "Bash(\"tfx multi --teammate-mode headless --assign 'cli:prompt:role' ...\") 형식을 사용하세요.",
290
323
  );
291
324
  }
292
325
  }
@@ -307,24 +340,27 @@ async function main() {
307
340
  if (multiState.nativeWorkCalls > GATE_THRESHOLD) {
308
341
  deny(
309
342
  `[headless-guard] tfx-multi gate: ${toolName} 호출 ${multiState.nativeWorkCalls}회 — headless dispatch 먼저 하세요.\n` +
310
- 'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
343
+ "Bash(\"tfx multi --teammate-mode headless --auto-attach --dashboard --assign 'codex:프롬프트:역할' --timeout 600\")",
311
344
  );
312
345
  }
313
346
 
314
347
  nudge(
315
348
  `[headless-guard] tfx-multi 활성 (${multiState.nativeWorkCalls}/${GATE_THRESHOLD}). ` +
316
- "headless dispatch 후 작업을 시작하세요.",
349
+ "headless dispatch 후 작업을 시작하세요.",
317
350
  );
318
351
  }
319
352
 
320
353
  // H3 fix: Agent gate와 동일하게 NUDGE_THRESHOLD 기반 주기적 nudge
321
- multiState.nativeWorkCallsSinceDispatch = (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
354
+ multiState.nativeWorkCallsSinceDispatch =
355
+ (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
322
356
  writeMultiState(multiState);
323
357
 
324
358
  if (multiState.nativeWorkCallsSinceDispatch >= NUDGE_THRESHOLD) {
325
359
  multiState.nativeWorkCallsSinceDispatch = 0;
326
360
  writeMultiState(multiState);
327
- nudge("[headless-guard] nudge: headless 워커가 코드 수정 중. 직접 수정은 충돌 위험.");
361
+ nudge(
362
+ "[headless-guard] nudge: headless 워커가 코드 수정 중. 직접 수정은 충돌 위험.",
363
+ );
328
364
  }
329
365
  process.exit(0); // threshold 미만이면 조용히 통과
330
366
  }
@@ -333,7 +369,8 @@ async function main() {
333
369
  if (toolName === "Agent") {
334
370
  const subType = (toolInput.subagent_type || "").toLowerCase();
335
371
  const NATIVE_TYPES = new Set(["explore", "plan", "general-purpose", ""]);
336
- const isNative = NATIVE_TYPES.has(subType) || subType.startsWith("oh-my-claudecode:");
372
+ const isNative =
373
+ NATIVE_TYPES.has(subType) || subType.startsWith("oh-my-claudecode:");
337
374
 
338
375
  // ── A+B: tfx-multi 상태 기반 처리 ──
339
376
  const multiState = readMultiState();
@@ -346,17 +383,18 @@ async function main() {
346
383
  if (multiState.nativeWorkCalls > GATE_THRESHOLD) {
347
384
  deny(
348
385
  `[headless-guard] tfx-multi gate: Agent(${subType || "default"}) 호출 ${multiState.nativeWorkCalls}회 — headless에 먼저 dispatch하세요.\n` +
349
- 'Bash("tfx multi --teammate-mode headless --auto-attach --dashboard --assign \'codex:프롬프트:역할\' --timeout 600")',
386
+ "Bash(\"tfx multi --teammate-mode headless --auto-attach --dashboard --assign 'codex:프롬프트:역할' --timeout 600\")",
350
387
  );
351
388
  }
352
389
  // 허용 범위 내 → 경고 + 통과
353
390
  nudge(
354
391
  `[headless-guard] tfx-multi 활성 (${multiState.nativeWorkCalls}/${GATE_THRESHOLD}). ` +
355
- "headless dispatch 후 작업을 시작하세요.",
392
+ "headless dispatch 후 작업을 시작하세요.",
356
393
  );
357
394
  } else {
358
395
  // ── B: nudge — dispatch 후, 네이티브 드리프트 감지 ──
359
- multiState.nativeWorkCallsSinceDispatch = (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
396
+ multiState.nativeWorkCallsSinceDispatch =
397
+ (multiState.nativeWorkCallsSinceDispatch || 0) + 1;
360
398
  writeMultiState(multiState);
361
399
 
362
400
  if (multiState.nativeWorkCallsSinceDispatch >= NUDGE_THRESHOLD) {
@@ -364,7 +402,7 @@ async function main() {
364
402
  writeMultiState(multiState);
365
403
  nudge(
366
404
  "[headless-guard] nudge: headless 워커가 실행 중입니다. " +
367
- "결과를 기다리거나 추가 --assign으로 위임하세요.",
405
+ "결과를 기다리거나 추가 --assign으로 위임하세요.",
368
406
  );
369
407
  }
370
408
  }
@@ -388,8 +426,8 @@ async function main() {
388
426
  if (cliPatterns.some((p) => p.test(combined))) {
389
427
  deny(
390
428
  "[headless-guard] Codex/Gemini를 Agent()로 래핑하지 마세요. " +
391
- `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
392
- DIRECT_CLI_BYPASS_HINT,
429
+ `승인된 경로: ${HEADLESS_FALLBACK_COMMAND}. ` +
430
+ DIRECT_CLI_BYPASS_HINT,
393
431
  );
394
432
  }
395
433
  }