@triflux/core 10.1.0 → 10.25.1

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 (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +20 -8
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +267 -35
  7. package/hooks/hook-registry.json +343 -256
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +254 -33
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +831 -290
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -4
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -18
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +167 -38
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +285 -111
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +87 -13
  75. package/hub/research.mjs +33 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +127 -61
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +72 -71
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +391 -247
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1281 -115
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +70 -10
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +32 -7
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,261 @@
1
+ // hub/routing-snapshot.mjs
2
+ //
3
+ // PRD: .omx/plans/prd-dynamic-routing-engine.md
4
+ //
5
+ // Phase 0 — pure read-only context projector. Wraps HUD providers and the
6
+ // AccountBroker public snapshot into a single deterministic snapshot that
7
+ // `evaluateDynamicRoute(request, snapshot, policy)` consumes.
8
+ //
9
+ // Phase 0 contract:
10
+ // - no callers yet (dormant). Only `evaluateDynamicRoute()` is meant to read this.
11
+ // - lazy import of HUD providers + broker so this module imports cleanly even
12
+ // when those surfaces are absent in tests.
13
+ // - never mutates upstream state — strictly read-only.
14
+ // - all output fields are derivable from `publicSnapshot()` / HUD redacted API,
15
+ // so writing the resulting snapshot to disk does not leak credentials.
16
+
17
+ const CLAUDE_HUD_MODULE = "../hud/providers/claude.mjs";
18
+ const CODEX_HUD_MODULE = "../hud/providers/codex.mjs";
19
+ const BROKER_MODULE = "./account-broker.mjs";
20
+
21
+ async function loadHudClaude() {
22
+ try {
23
+ return await import(CLAUDE_HUD_MODULE);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ async function loadHudCodex() {
30
+ try {
31
+ return await import(CODEX_HUD_MODULE);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ async function loadBroker() {
38
+ try {
39
+ const mod = await import(BROKER_MODULE);
40
+ return mod.broker ?? mod.default ?? null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function normalizeClaudeUsage(raw, now) {
47
+ if (!raw || typeof raw !== "object") return null;
48
+ const data = raw.data ?? raw;
49
+ if (!data) return null;
50
+ return {
51
+ fiveHourPercent: numericOrNull(data.fiveHourPercent),
52
+ weeklyPercent: numericOrNull(data.weeklyPercent),
53
+ fiveHourResetsAt: toEpoch(data.fiveHourResetsAt),
54
+ weeklyResetsAt: toEpoch(data.weeklyResetsAt),
55
+ observedAt: toEpoch(data.observedAt) ?? now,
56
+ isStale: Boolean(raw.isStale ?? data.isStale),
57
+ source: data.source ?? "hud/providers/claude.mjs",
58
+ };
59
+ }
60
+
61
+ function normalizeCodexUsage(raw, now) {
62
+ if (!raw || typeof raw !== "object") return null;
63
+ const buckets = raw.buckets ?? raw.data?.buckets ?? raw;
64
+ if (!buckets || typeof buckets !== "object") return null;
65
+ const entries = Object.entries(buckets);
66
+ if (entries.length === 0) return null;
67
+ const out = {};
68
+ for (const [id, bucket] of entries) {
69
+ if (!bucket || typeof bucket !== "object") continue;
70
+ out[id] = {
71
+ primary: normalizeCodexBucketLeg(bucket.primary),
72
+ secondary: normalizeCodexBucketLeg(bucket.secondary),
73
+ };
74
+ }
75
+ return {
76
+ buckets: out,
77
+ observedAt: toEpoch(raw.observedAt) ?? now,
78
+ isStale: Boolean(raw.shouldRefresh === false ? false : raw.isStale),
79
+ source: raw.source ?? "hud/providers/codex.mjs",
80
+ };
81
+ }
82
+
83
+ function normalizeCodexBucketLeg(leg) {
84
+ if (!leg || typeof leg !== "object") return null;
85
+ return {
86
+ usedPercent: numericOrNull(leg.used_percent ?? leg.usedPercent),
87
+ resetsAt: toEpoch(leg.resets_at ?? leg.resetsAt),
88
+ };
89
+ }
90
+
91
+ function normalizeBrokerSnapshot(raw) {
92
+ if (!Array.isArray(raw)) return null;
93
+ const grouped = { codex: [], claude: [], gemini: [] };
94
+ for (const entry of raw) {
95
+ if (!entry || typeof entry !== "object") continue;
96
+ const provider = entry.provider;
97
+ if (!grouped[provider]) continue;
98
+ grouped[provider].push({
99
+ id: entry.id,
100
+ tier: entry.tier ?? "unknown",
101
+ busy: Boolean(entry.busy),
102
+ cooldownUntil: toEpoch(entry.cooldownUntil),
103
+ lastUsedAt: toEpoch(entry.lastUsedAt),
104
+ remainingMs: numericOrNull(entry.remainingMs),
105
+ circuitState: entry.circuitState ?? "closed",
106
+ failureCount: numericOrNull(entry.failureCount) ?? 0,
107
+ expiresAt: toEpoch(entry.expiresAt),
108
+ drainAfter: toEpoch(entry.drainAfter),
109
+ });
110
+ }
111
+ return grouped;
112
+ }
113
+
114
+ function numericOrNull(v) {
115
+ if (v === null || v === undefined) return null;
116
+ const n = Number(v);
117
+ return Number.isFinite(n) ? n : null;
118
+ }
119
+
120
+ function toEpoch(v) {
121
+ if (v === null || v === undefined) return null;
122
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
123
+ const parsed = Date.parse(v);
124
+ return Number.isNaN(parsed) ? null : parsed;
125
+ }
126
+
127
+ export async function buildRoutingSnapshot({
128
+ forceRefresh = false,
129
+ now = Date.now(),
130
+ } = {}) {
131
+ const [hudClaude, hudCodex, broker] = await Promise.all([
132
+ loadHudClaude(),
133
+ loadHudCodex(),
134
+ loadBroker(),
135
+ ]);
136
+
137
+ let claude = null;
138
+ if (hudClaude?.readClaudeUsageSnapshot) {
139
+ try {
140
+ claude = normalizeClaudeUsage(
141
+ hudClaude.readClaudeUsageSnapshot({ forceRefresh }),
142
+ now,
143
+ );
144
+ } catch (err) {
145
+ claude = {
146
+ error: String(err?.message ?? err),
147
+ observedAt: now,
148
+ isStale: true,
149
+ };
150
+ }
151
+ }
152
+
153
+ let codex = null;
154
+ if (hudCodex?.readCodexRateLimitSnapshot) {
155
+ try {
156
+ codex = normalizeCodexUsage(
157
+ hudCodex.readCodexRateLimitSnapshot({ forceRefresh }),
158
+ now,
159
+ );
160
+ } catch (err) {
161
+ codex = {
162
+ error: String(err?.message ?? err),
163
+ observedAt: now,
164
+ isStale: true,
165
+ };
166
+ }
167
+ }
168
+
169
+ let brokerState = null;
170
+ if (broker?.publicSnapshot) {
171
+ try {
172
+ brokerState = normalizeBrokerSnapshot(broker.publicSnapshot());
173
+ } catch (err) {
174
+ brokerState = { error: String(err?.message ?? err) };
175
+ }
176
+ }
177
+
178
+ return {
179
+ schemaVersion: 1,
180
+ observedAt: now,
181
+ claude,
182
+ codex,
183
+ broker: brokerState,
184
+ };
185
+ }
186
+
187
+ // Phase 1 — in-process snapshot cache.
188
+ // 의도: caller (Phase 1+ conductor / swarm-hypervisor) 가 spawn loop 에서
189
+ // 매번 HUD/broker IO 를 트리거하지 않도록 정해진 TTL 안에서 같은 snapshot
190
+ // 을 재사용한다. cache 는 모듈-local 이라 process 단위 (테스트 격리 위해
191
+ // `resetSnapshotCache()` 노출).
192
+
193
+ const DEFAULT_CACHE_TTL_MS = 900_000; // policy freshness max 와 정합
194
+
195
+ let _cache = null; // { snapshot, fetchedAt }
196
+
197
+ export function resetSnapshotCache() {
198
+ _cache = null;
199
+ }
200
+
201
+ /**
202
+ * Cached snapshot shortcut. cache TTL 안이면 재사용, 아니면 새로 빌드.
203
+ *
204
+ * @param {{
205
+ * forceRefresh?: boolean, // true 면 cache 무시하고 새로 빌드
206
+ * maxAgeMs?: number, // override cache TTL (default 900_000)
207
+ * now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
208
+ * builder?: function, // 테스트용 builder 주입 (default buildRoutingSnapshot)
209
+ * }} [opts]
210
+ * @returns {Promise<object>}
211
+ */
212
+ export async function getDefaultSnapshot(opts = {}) {
213
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
214
+ const maxAgeMs =
215
+ typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
216
+ const builder = opts.builder || buildRoutingSnapshot;
217
+ const forceRefresh = Boolean(opts.forceRefresh);
218
+
219
+ if (!forceRefresh && _cache && now - _cache.fetchedAt < maxAgeMs) {
220
+ return _cache.snapshot;
221
+ }
222
+
223
+ const snapshot = await builder({ forceRefresh, now });
224
+ _cache = { snapshot, fetchedAt: now };
225
+ return snapshot;
226
+ }
227
+
228
+ /**
229
+ * Synchronous cache shortcut. cache TTL 안에 fresh snapshot이 있으면 즉시 반환,
230
+ * 아니면 null. caller (sync hot path — e.g. conductor.spawnSession) 가 broker.lease
231
+ * atomic 인변량을 깨지 않고 dynamic routing decision 을 sync 로 평가할 수 있도록 한다.
232
+ * builder 호출(IO)은 하지 않는다 — cache miss 면 caller 가 fallback decision 사용.
233
+ *
234
+ * @param {{
235
+ * maxAgeMs?: number, // override cache TTL (default 900_000)
236
+ * now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
237
+ * }} [opts]
238
+ * @returns {object|null} cache fresh이면 snapshot, 아니면 null
239
+ */
240
+ export function tryGetCachedSnapshot(opts = {}) {
241
+ const now = typeof opts.now === "number" ? opts.now : Date.now();
242
+ const maxAgeMs =
243
+ typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
244
+ if (_cache && now - _cache.fetchedAt < maxAgeMs) {
245
+ return _cache.snapshot;
246
+ }
247
+ return null;
248
+ }
249
+
250
+ // Test helpers — pure normalizers exposed so unit tests can build snapshots
251
+ // from mock provider responses without running real HUD I/O.
252
+ export const __internal__ = {
253
+ normalizeClaudeUsage,
254
+ normalizeCodexUsage,
255
+ normalizeBrokerSnapshot,
256
+ // Phase 1 cache 테스트 helpers
257
+ getCacheStateForTest: () => (_cache ? { ..._cache } : null),
258
+ setCacheForTest: (state) => {
259
+ _cache = state ? { ...state } : null;
260
+ },
261
+ };
@@ -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,10 +408,11 @@ 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
  });
350
416
  }
351
417
 
352
- export default createAdaptiveFingerprintService;
418
+ export default createAdaptiveFingerprintService;