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
@@ -1,21 +1,21 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash } from "node:crypto";
2
2
 
3
- import { normalizePath } from './platform.mjs';
4
- import { withRetry } from './workers/worker-utils.mjs';
3
+ import { normalizePath } from "./platform.mjs";
4
+ import { withRetry } from "./workers/worker-utils.mjs";
5
5
 
6
6
  const ADAPTIVE_FINGERPRINT_VERSION = 1;
7
- const DEFAULT_SCOPE = 'default';
8
- const META_PREFIX = 'adaptive_fingerprint:';
7
+ const DEFAULT_SCOPE = "default";
8
+ const META_PREFIX = "adaptive_fingerprint:";
9
9
  const DEFAULT_RETRY_OPTIONS = Object.freeze({
10
10
  maxAttempts: 3,
11
11
  baseDelayMs: 50,
12
12
  maxDelayMs: 250,
13
13
  });
14
14
  const TIME_WINDOWS = Object.freeze([
15
- { name: 'overnight', start: 0, end: 5 },
16
- { name: 'morning', start: 6, end: 11 },
17
- { name: 'afternoon', start: 12, end: 17 },
18
- { name: 'evening', start: 18, end: 23 },
15
+ { name: "overnight", start: 0, end: 5 },
16
+ { name: "morning", start: 6, end: 11 },
17
+ { name: "afternoon", start: 12, end: 17 },
18
+ { name: "evening", start: 18, end: 23 },
19
19
  ]);
20
20
  const MEMORY_FINGERPRINT_CACHE = new WeakMap();
21
21
 
@@ -30,8 +30,8 @@ function toIsoTimestamp(value = Date.now()) {
30
30
 
31
31
  function toTimestamp(value) {
32
32
  if (value instanceof Date) return value.getTime();
33
- if (typeof value === 'number' && Number.isFinite(value)) return value;
34
- if (typeof value === 'string') {
33
+ if (typeof value === "number" && Number.isFinite(value)) return value;
34
+ if (typeof value === "string") {
35
35
  const parsed = Date.parse(value);
36
36
  return Number.isFinite(parsed) ? parsed : null;
37
37
  }
@@ -39,9 +39,21 @@ function toTimestamp(value) {
39
39
  }
40
40
 
41
41
  function normalizeRetryOptions(options = {}) {
42
- const maxAttempts = Math.max(1, Number(options.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts) || DEFAULT_RETRY_OPTIONS.maxAttempts);
43
- const baseDelayMs = Math.max(0, Number(options.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs) || DEFAULT_RETRY_OPTIONS.baseDelayMs);
44
- const maxDelayMs = Math.max(baseDelayMs, Number(options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs) || DEFAULT_RETRY_OPTIONS.maxDelayMs);
42
+ const maxAttempts = Math.max(
43
+ 1,
44
+ Number(options.maxAttempts ?? DEFAULT_RETRY_OPTIONS.maxAttempts) ||
45
+ DEFAULT_RETRY_OPTIONS.maxAttempts,
46
+ );
47
+ const baseDelayMs = Math.max(
48
+ 0,
49
+ Number(options.baseDelayMs ?? DEFAULT_RETRY_OPTIONS.baseDelayMs) ||
50
+ DEFAULT_RETRY_OPTIONS.baseDelayMs,
51
+ );
52
+ const maxDelayMs = Math.max(
53
+ baseDelayMs,
54
+ Number(options.maxDelayMs ?? DEFAULT_RETRY_OPTIONS.maxDelayMs) ||
55
+ DEFAULT_RETRY_OPTIONS.maxDelayMs,
56
+ );
45
57
  return { maxAttempts, baseDelayMs, maxDelayMs };
46
58
  }
47
59
 
@@ -55,21 +67,23 @@ function metaKey(scope) {
55
67
  }
56
68
 
57
69
  function hashValue(value) {
58
- const hash = createHash('sha256');
70
+ const hash = createHash("sha256");
59
71
  hash.update(JSON.stringify(value));
60
- return `sha256:${hash.digest('hex')}`;
72
+ return `sha256:${hash.digest("hex")}`;
61
73
  }
62
74
 
63
75
  function normalizeContextPath(value) {
64
- const normalized = normalizePath(String(value ?? ''));
65
- return normalized.replace(/\/+/gu, '/').replace(/\/+$/u, '') || '/';
76
+ const normalized = normalizePath(String(value ?? ""));
77
+ return normalized.replace(/\/+/gu, "/").replace(/\/+$/u, "") || "/";
66
78
  }
67
79
 
68
80
  function toRelativePath(targetPath, cwd) {
69
81
  if (!cwd) return targetPath;
70
82
  const base = normalizeContextPath(cwd);
71
- if (targetPath === base) return '.';
72
- return targetPath.startsWith(`${base}/`) ? targetPath.slice(base.length + 1) : targetPath;
83
+ if (targetPath === base) return ".";
84
+ return targetPath.startsWith(`${base}/`)
85
+ ? targetPath.slice(base.length + 1)
86
+ : targetPath;
73
87
  }
74
88
 
75
89
  function toUniqueList(values) {
@@ -84,29 +98,49 @@ function toUniqueList(values) {
84
98
  }
85
99
 
86
100
  function collectRawPathCandidates(context = {}) {
87
- const direct = [context.file_path, context.filePath, context.path, context.target_path, context.targetPath];
101
+ const direct = [
102
+ context.file_path,
103
+ context.filePath,
104
+ context.path,
105
+ context.target_path,
106
+ context.targetPath,
107
+ ];
88
108
  const fromArrays = [context.files, context.paths, context.targets]
89
109
  .filter(Array.isArray)
90
110
  .flat()
91
- .map((entry) => (typeof entry === 'string' ? entry : entry?.path));
92
- return [...direct, ...fromArrays].filter((entry) => typeof entry === 'string' && entry.trim().length > 0);
111
+ .map((entry) => (typeof entry === "string" ? entry : entry?.path));
112
+ return [...direct, ...fromArrays].filter(
113
+ (entry) => typeof entry === "string" && entry.trim().length > 0,
114
+ );
93
115
  }
94
116
 
95
117
  function collectPathPattern(context = {}) {
96
118
  const normalized = toUniqueList(
97
119
  collectRawPathCandidates(context)
98
120
  .map((entry) => normalizeContextPath(entry))
99
- .map((entry) => toRelativePath(entry, context.cwd || context.project_root || context.projectRoot)),
121
+ .map((entry) =>
122
+ toRelativePath(
123
+ entry,
124
+ context.cwd || context.project_root || context.projectRoot,
125
+ ),
126
+ ),
100
127
  ).sort();
101
128
 
102
129
  const primaryPath = normalized[0] ?? null;
103
130
  const extensions = normalized
104
- .map((entry) => entry.split('/').pop() || '')
105
- .map((entry) => (entry.includes('.') ? entry.slice(entry.lastIndexOf('.')).toLowerCase() : 'none'));
106
- const extensionCounts = extensions.reduce((acc, ext) => ({
107
- ...acc,
108
- [ext]: (acc[ext] || 0) + 1,
109
- }), {});
131
+ .map((entry) => entry.split("/").pop() || "")
132
+ .map((entry) =>
133
+ entry.includes(".")
134
+ ? entry.slice(entry.lastIndexOf(".")).toLowerCase()
135
+ : "none",
136
+ );
137
+ const extensionCounts = extensions.reduce(
138
+ (acc, ext) => ({
139
+ ...acc,
140
+ [ext]: (acc[ext] || 0) + 1,
141
+ }),
142
+ {},
143
+ );
110
144
 
111
145
  return {
112
146
  count: normalized.length,
@@ -118,17 +152,24 @@ function collectPathPattern(context = {}) {
118
152
  }
119
153
 
120
154
  function normalizeWorkType(value) {
121
- const text = String(value ?? '').trim().toLowerCase();
155
+ const text = String(value ?? "")
156
+ .trim()
157
+ .toLowerCase();
122
158
  if (!text) {
123
- return { raw: null, normalized: 'general' };
159
+ return { raw: null, normalized: "general" };
124
160
  }
125
- const normalized = text.replace(/\s+/gu, '-').replace(/[^a-z0-9-]/gu, '') || 'general';
161
+ const normalized =
162
+ text.replace(/\s+/gu, "-").replace(/[^a-z0-9-]/gu, "") || "general";
126
163
  return { raw: value, normalized };
127
164
  }
128
165
 
129
166
  function collectActivityTimestamps(context = {}, now = Date.now) {
130
- const nowValue = typeof now === 'function' ? now() : now;
131
- const fromList = [context.activity_timestamps, context.activityTimestamps, context.timestamps]
167
+ const nowValue = typeof now === "function" ? now() : now;
168
+ const fromList = [
169
+ context.activity_timestamps,
170
+ context.activityTimestamps,
171
+ context.timestamps,
172
+ ]
132
173
  .filter(Array.isArray)
133
174
  .flat()
134
175
  .map(toTimestamp)
@@ -136,17 +177,24 @@ function collectActivityTimestamps(context = {}, now = Date.now) {
136
177
  const singles = [context.timestamp, context.started_at, context.startedAt]
137
178
  .map(toTimestamp)
138
179
  .filter((entry) => entry != null);
139
- return fromList.length || singles.length ? [...fromList, ...singles] : [Number(nowValue)];
180
+ return fromList.length || singles.length
181
+ ? [...fromList, ...singles]
182
+ : [Number(nowValue)];
140
183
  }
141
184
 
142
185
  function classifyHour(hour) {
143
186
  const safeHour = Number.isFinite(Number(hour)) ? Number(hour) : 0;
144
- const matched = TIME_WINDOWS.find((window) => safeHour >= window.start && safeHour <= window.end);
145
- return matched?.name || 'overnight';
187
+ const matched = TIME_WINDOWS.find(
188
+ (window) => safeHour >= window.start && safeHour <= window.end,
189
+ );
190
+ return matched?.name || "overnight";
146
191
  }
147
192
 
148
193
  function buildWindowHistogram(timestamps = []) {
149
- const histogram = TIME_WINDOWS.reduce((acc, window) => ({ ...acc, [window.name]: 0 }), {});
194
+ const histogram = TIME_WINDOWS.reduce(
195
+ (acc, window) => ({ ...acc, [window.name]: 0 }),
196
+ {},
197
+ );
150
198
  for (const timestamp of timestamps) {
151
199
  const bucket = classifyHour(new Date(timestamp).getHours());
152
200
  histogram[bucket] = (histogram[bucket] || 0) + 1;
@@ -155,14 +203,16 @@ function buildWindowHistogram(timestamps = []) {
155
203
  }
156
204
 
157
205
  function dominantWindow(histogram) {
158
- const sorted = Object.entries(histogram)
159
- .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
160
- return sorted[0]?.[0] || 'overnight';
206
+ const sorted = Object.entries(histogram).sort(
207
+ (left, right) => right[1] - left[1] || left[0].localeCompare(right[0]),
208
+ );
209
+ return sorted[0]?.[0] || "overnight";
161
210
  }
162
211
 
163
212
  function resolveTimezoneName(context = {}) {
164
- const fromContext = typeof context.timezone === 'string' ? context.timezone.trim() : '';
165
- const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
213
+ const fromContext =
214
+ typeof context.timezone === "string" ? context.timezone.trim() : "";
215
+ const fromIntl = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
166
216
  return fromContext || fromIntl;
167
217
  }
168
218
 
@@ -199,11 +249,13 @@ function getMemoryStoreMap(store) {
199
249
 
200
250
  function readFingerprintFromStore(store, scope) {
201
251
  if (!store) return null;
202
- if (typeof store.loadAdaptiveFingerprint === 'function') {
252
+ if (typeof store.loadAdaptiveFingerprint === "function") {
203
253
  return store.loadAdaptiveFingerprint(scope);
204
254
  }
205
255
  if (store.db?.prepare) {
206
- const row = store.db.prepare('SELECT value FROM _meta WHERE key = ?').get(metaKey(scope));
256
+ const row = store.db
257
+ .prepare("SELECT value FROM _meta WHERE key = ?")
258
+ .get(metaKey(scope));
207
259
  return row?.value ? JSON.parse(row.value) : null;
208
260
  }
209
261
  return clone(getMemoryStoreMap(store).get(normalizeScope(scope)) || null);
@@ -211,11 +263,12 @@ function readFingerprintFromStore(store, scope) {
211
263
 
212
264
  function writeFingerprintToStore(store, scope, record) {
213
265
  if (!store) return clone(record);
214
- if (typeof store.saveAdaptiveFingerprint === 'function') {
266
+ if (typeof store.saveAdaptiveFingerprint === "function") {
215
267
  return store.saveAdaptiveFingerprint(scope, clone(record));
216
268
  }
217
269
  if (store.db?.prepare) {
218
- store.db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)')
270
+ store.db
271
+ .prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)")
219
272
  .run(metaKey(scope), JSON.stringify(record));
220
273
  return clone(record);
221
274
  }
@@ -225,7 +278,7 @@ function writeFingerprintToStore(store, scope, record) {
225
278
 
226
279
  function buildHealthSnapshot(base, patch = {}) {
227
280
  return {
228
- state: patch.state || base.state || 'healthy',
281
+ state: patch.state || base.state || "healthy",
229
282
  retry: { ...base.retry },
230
283
  last_success_at: patch.last_success_at ?? base.last_success_at ?? null,
231
284
  last_failure_at: patch.last_failure_at ?? base.last_failure_at ?? null,
@@ -235,7 +288,7 @@ function buildHealthSnapshot(base, patch = {}) {
235
288
 
236
289
  function createInitialHealth(retryOptions) {
237
290
  return {
238
- state: 'healthy',
291
+ state: "healthy",
239
292
  retry: { ...retryOptions },
240
293
  last_success_at: null,
241
294
  last_failure_at: null,
@@ -244,7 +297,7 @@ function createInitialHealth(retryOptions) {
244
297
  }
245
298
 
246
299
  function resolveNowValue(now) {
247
- return typeof now === 'function' ? now() : now;
300
+ return typeof now === "function" ? now() : now;
248
301
  }
249
302
 
250
303
  function mergeFingerprintSnapshot(previous, computed, scope) {
@@ -258,7 +311,7 @@ function mergeFingerprintSnapshot(previous, computed, scope) {
258
311
 
259
312
  function markHealthyHealth(base, now) {
260
313
  return buildHealthSnapshot(base, {
261
- state: 'healthy',
314
+ state: "healthy",
262
315
  last_success_at: toIsoTimestamp(resolveNowValue(now)),
263
316
  last_error: null,
264
317
  });
@@ -266,11 +319,11 @@ function markHealthyHealth(base, now) {
266
319
 
267
320
  function markDegradedHealth(base, now, error) {
268
321
  return buildHealthSnapshot(base, {
269
- state: 'degraded',
322
+ state: "degraded",
270
323
  last_failure_at: toIsoTimestamp(resolveNowValue(now)),
271
324
  last_error: {
272
- name: error?.name || 'Error',
273
- message: error?.message || 'unknown adaptive fingerprint error',
325
+ name: error?.name || "Error",
326
+ message: error?.message || "unknown adaptive fingerprint error",
274
327
  },
275
328
  });
276
329
  }
@@ -279,7 +332,9 @@ export function buildAdaptiveFingerprint(sessionContext = {}, options = {}) {
279
332
  const now = options.now ?? Date.now;
280
333
  const capturedAt = toIsoTimestamp(resolveNowValue(now));
281
334
  const pathPattern = collectPathPattern(sessionContext);
282
- const workType = normalizeWorkType(sessionContext.work_type ?? sessionContext.workType);
335
+ const workType = normalizeWorkType(
336
+ sessionContext.work_type ?? sessionContext.workType,
337
+ );
283
338
  const timezonePattern = collectTimezonePattern(sessionContext, now);
284
339
  const fingerprintId = computeFingerprintSignature({
285
340
  path_pattern: pathPattern,
@@ -303,10 +358,18 @@ export async function loadAdaptiveFingerprint(store, scope = DEFAULT_SCOPE) {
303
358
  return clone(loaded);
304
359
  }
305
360
 
306
- export async function saveAdaptiveFingerprint(store, scope, fingerprint, options = {}) {
361
+ export async function saveAdaptiveFingerprint(
362
+ store,
363
+ scope,
364
+ fingerprint,
365
+ options = {},
366
+ ) {
307
367
  const retryOptions = normalizeRetryOptions(options.retryOptions);
308
368
  const normalizedScope = normalizeScope(scope);
309
- const write = async () => Promise.resolve(writeFingerprintToStore(store, normalizedScope, fingerprint));
369
+ const write = async () =>
370
+ Promise.resolve(
371
+ writeFingerprintToStore(store, normalizedScope, fingerprint),
372
+ );
310
373
  const saved = await withRetry(write, { ...retryOptions });
311
374
  return clone(saved);
312
375
  }
@@ -324,7 +387,9 @@ export function createAdaptiveFingerprintService(options = {}) {
324
387
  const merged = mergeFingerprintSnapshot(previous, computed, scope);
325
388
 
326
389
  try {
327
- const saved = await saveAdaptiveFingerprint(store, scope, merged, { retryOptions });
390
+ const saved = await saveAdaptiveFingerprint(store, scope, merged, {
391
+ retryOptions,
392
+ });
328
393
  health = markHealthyHealth(health, now);
329
394
  return saved;
330
395
  } catch (error) {
@@ -343,7 +408,8 @@ export function createAdaptiveFingerprintService(options = {}) {
343
408
 
344
409
  return Object.freeze({
345
410
  captureFingerprint: capture,
346
- computeFingerprint: (sessionContext = {}) => buildAdaptiveFingerprint(sessionContext, { now }),
411
+ computeFingerprint: (sessionContext = {}) =>
412
+ buildAdaptiveFingerprint(sessionContext, { now }),
347
413
  loadFingerprint: read,
348
414
  getHealth,
349
415
  });
package/hub/state.mjs CHANGED
@@ -1,20 +1,34 @@
1
- import { execSync } from 'node:child_process';
2
- import { mkdirSync, openSync, closeSync, unlinkSync, writeFileSync, readFileSync, renameSync, existsSync, statSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
- import { dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url));
8
- const PID_FILE_NAME = 'hub.pid';
9
- const LEGACY_STATE_FILE_NAME = 'hub-state.json';
10
- const LOCK_FILE_NAME = 'hub-start.lock';
1
+ import { execSync } from "node:child_process";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ statSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const PROJECT_ROOT = fileURLToPath(new URL("..", import.meta.url));
18
+ const PID_FILE_NAME = "hub.pid";
19
+ const LEGACY_STATE_FILE_NAME = "hub-state.json";
20
+ const LOCK_FILE_NAME = "hub-start.lock";
11
21
 
12
22
  let heldLockPath = null;
13
23
  let heldLockFd = null;
14
24
  let cachedVersionHash = null;
15
25
 
16
26
  function getStateDir(options = {}) {
17
- return options.stateDir || process.env.TFX_HUB_STATE_DIR?.trim() || join(homedir(), '.claude', 'cache', 'tfx-hub');
27
+ return (
28
+ options.stateDir ||
29
+ process.env.TFX_HUB_STATE_DIR?.trim() ||
30
+ join(homedir(), ".claude", "cache", "tfx-hub")
31
+ );
18
32
  }
19
33
 
20
34
  function getStatePath(options = {}) {
@@ -55,25 +69,32 @@ function safeReplaceFile(tempPath, targetPath) {
55
69
  try {
56
70
  renameSync(tempPath, targetPath);
57
71
  } catch (error) {
58
- if (!['EEXIST', 'EPERM', 'EACCES'].includes(error?.code)) {
59
- try { unlinkSync(tempPath); } catch {}
72
+ if (!["EEXIST", "EPERM", "EACCES"].includes(error?.code)) {
73
+ try {
74
+ unlinkSync(tempPath);
75
+ } catch {}
60
76
  throw error;
61
77
  }
62
- try { unlinkSync(targetPath); } catch {}
78
+ try {
79
+ unlinkSync(targetPath);
80
+ } catch {}
63
81
  renameSync(tempPath, targetPath);
64
82
  }
65
83
  }
66
84
 
67
85
  function writeJsonFile(targetPath, payload) {
68
86
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
69
- writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
87
+ writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
88
+ encoding: "utf8",
89
+ mode: 0o600,
90
+ });
70
91
  safeReplaceFile(tempPath, targetPath);
71
92
  }
72
93
 
73
94
  function readJsonFile(filePath) {
74
95
  try {
75
96
  if (!existsSync(filePath)) return null;
76
- return parseJson(readFileSync(filePath, 'utf8'), null);
97
+ return parseJson(readFileSync(filePath, "utf8"), null);
77
98
  } catch {
78
99
  return null;
79
100
  }
@@ -82,7 +103,7 @@ function readJsonFile(filePath) {
82
103
  /**
83
104
  * 허브의 현재 상태(PID, 포트, 버전 등)를 파일에 기록합니다.
84
105
  * 원자적(atomic) 쓰기를 위해 임시 파일을 생성한 후 교체하는 방식을 사용합니다.
85
- *
106
+ *
86
107
  * @param {object} payload - 상태 데이터
87
108
  * @param {number} payload.pid - 허브 프로세스 ID
88
109
  * @param {number} payload.port - 허브 서버 포트
@@ -93,31 +114,39 @@ function readJsonFile(filePath) {
93
114
  * @param {string} [options.stateDir] - 상태 파일이 저장될 디렉토리
94
115
  * @returns {object} 기록된 상태 데이터
95
116
  */
96
- export function writeState({ pid, port, version, sessionId, startedAt, ...rest }, options = {}) {
117
+ export function writeState(
118
+ { pid, port, version, sessionId, startedAt, ...rest },
119
+ options = {},
120
+ ) {
97
121
  const stateDir = getStateDir(options);
98
122
  const statePath = getStatePath(options);
99
123
  const payload = { pid, port, version, sessionId, startedAt, ...rest };
100
124
 
101
125
  mkdirSync(stateDir, { recursive: true });
102
126
  writeJsonFile(statePath, payload);
103
- try { unlinkSync(getLegacyStatePath(options)); } catch {}
127
+ try {
128
+ unlinkSync(getLegacyStatePath(options));
129
+ } catch {}
104
130
  return payload;
105
131
  }
106
132
 
107
133
  /**
108
134
  * 파일로부터 허브의 현재 상태를 읽어옵니다.
109
- *
135
+ *
110
136
  * @param {object} [options] - 옵션
111
137
  * @param {string} [options.stateDir] - 상태 파일이 저장된 디렉토리
112
138
  * @returns {object|null} 읽어온 상태 데이터 또는 실패 시 null
113
139
  */
114
140
  export function readState(options = {}) {
115
- return readJsonFile(getStatePath(options)) ?? readJsonFile(getLegacyStatePath(options));
141
+ return (
142
+ readJsonFile(getStatePath(options)) ??
143
+ readJsonFile(getLegacyStatePath(options))
144
+ );
116
145
  }
117
146
 
118
147
  /**
119
148
  * 지정된 포트에서 실행 중인 허브 서버의 헬스 체크를 수행합니다.
120
- *
149
+ *
121
150
  * @param {number|string} port - 서버 포트
122
151
  * @param {object} [options] - 옵션
123
152
  * @param {number} [options.timeoutMs=1000] - 요청 타임아웃
@@ -133,7 +162,7 @@ export async function isServerHealthy(port, options = {}) {
133
162
 
134
163
  try {
135
164
  const response = await fetch(`${baseUrl}/health`, {
136
- method: 'GET',
165
+ method: "GET",
137
166
  signal: AbortSignal.timeout(timeoutMs),
138
167
  });
139
168
  if (!response.ok) return false;
@@ -147,7 +176,7 @@ export async function isServerHealthy(port, options = {}) {
147
176
  /**
148
177
  * 현재 프로젝트의 버전 해시를 생성합니다.
149
178
  * package.json의 버전과 Git commit SHA를 조합합니다.
150
- *
179
+ *
151
180
  * @param {object} [options] - 옵션
152
181
  * @param {boolean} [options.force=false] - 캐시를 무시하고 새로 생성할지 여부
153
182
  * @returns {string} 버전 해시 문자열
@@ -155,21 +184,21 @@ export async function isServerHealthy(port, options = {}) {
155
184
  export function getVersionHash(options = {}) {
156
185
  if (cachedVersionHash && !options.force) return cachedVersionHash;
157
186
 
158
- const packageJsonPath = join(PROJECT_ROOT, 'package.json');
159
- const pkg = parseJson(readFileSync(packageJsonPath, 'utf8'), {});
160
- const version = String(pkg?.version || '0.0.0').trim();
187
+ const packageJsonPath = join(PROJECT_ROOT, "package.json");
188
+ const pkg = parseJson(readFileSync(packageJsonPath, "utf8"), {});
189
+ const version = String(pkg?.version || "0.0.0").trim();
161
190
 
162
- let sha = String(process.env.TFX_HUB_GIT_SHA || '').trim();
191
+ let sha = String(process.env.TFX_HUB_GIT_SHA || "").trim();
163
192
  if (!sha) {
164
193
  try {
165
- sha = execSync('git rev-parse --short HEAD', {
194
+ sha = execSync("git rev-parse --short HEAD", {
166
195
  cwd: PROJECT_ROOT,
167
- encoding: 'utf8',
168
- stdio: ['ignore', 'pipe', 'ignore'],
196
+ encoding: "utf8",
197
+ stdio: ["ignore", "pipe", "ignore"],
169
198
  windowsHide: true,
170
199
  }).trim();
171
200
  } catch {
172
- sha = '';
201
+ sha = "";
173
202
  }
174
203
  }
175
204
 
@@ -180,7 +209,7 @@ export function getVersionHash(options = {}) {
180
209
  /**
181
210
  * 허브 시작 시 중복 실행을 방지하기 위한 잠금(lock)을 획득합니다.
182
211
  * 이미 실행 중인 다른 프로세스가 있는지 확인하고 유효한 잠금을 획득할 때까지 재시도합니다.
183
- *
212
+ *
184
213
  * @param {object} [options] - 옵션
185
214
  * @param {number} [options.timeoutMs=3000] - 최대 대기 시간
186
215
  * @param {number} [options.pollMs=50] - 재시도 간격
@@ -202,27 +231,37 @@ export async function acquireLock(options = {}) {
202
231
 
203
232
  while (Date.now() <= deadline) {
204
233
  try {
205
- const fd = openSync(lockPath, 'wx', 0o600);
206
- writeFileSync(fd, `${JSON.stringify({
207
- pid: process.pid,
208
- createdAt: new Date().toISOString(),
209
- }, null, 2)}\n`, 'utf8');
234
+ const fd = openSync(lockPath, "wx", 0o600);
235
+ writeFileSync(
236
+ fd,
237
+ `${JSON.stringify(
238
+ {
239
+ pid: process.pid,
240
+ createdAt: new Date().toISOString(),
241
+ },
242
+ null,
243
+ 2,
244
+ )}\n`,
245
+ "utf8",
246
+ );
210
247
  heldLockFd = fd;
211
248
  heldLockPath = lockPath;
212
249
  return { path: lockPath };
213
250
  } catch (error) {
214
- if (error?.code !== 'EEXIST') {
251
+ if (error?.code !== "EEXIST") {
215
252
  throw error;
216
253
  }
217
254
 
218
255
  try {
219
- const raw = readFileSync(lockPath, 'utf8');
256
+ const raw = readFileSync(lockPath, "utf8");
220
257
  const data = parseJson(raw, {});
221
258
  const stats = statSync(lockPath);
222
259
  const staleByPid = !isPidAlive(data?.pid);
223
260
  const staleByAge = Date.now() - stats.mtimeMs > timeoutMs;
224
261
  if (staleByPid || staleByAge) {
225
- try { unlinkSync(lockPath); } catch {}
262
+ try {
263
+ unlinkSync(lockPath);
264
+ } catch {}
226
265
  continue;
227
266
  }
228
267
  } catch {}
@@ -236,7 +275,7 @@ export async function acquireLock(options = {}) {
236
275
 
237
276
  /**
238
277
  * 획득했던 잠금을 해제합니다. 잠금 파일을 삭제하고 관련 리소스를 정리합니다.
239
- *
278
+ *
240
279
  * @param {object} [options] - 옵션
241
280
  * @param {string} [options.lockPath] - 명시적인 잠금 파일 경로
242
281
  */
@@ -244,7 +283,9 @@ export function releaseLock(options = {}) {
244
283
  const lockPath = options.lockPath || heldLockPath || getLockPath(options);
245
284
 
246
285
  if (heldLockFd !== null) {
247
- try { closeSync(heldLockFd); } catch {}
286
+ try {
287
+ closeSync(heldLockFd);
288
+ } catch {}
248
289
  heldLockFd = null;
249
290
  }
250
291