@lythos/cold-pool 0.17.10 → 0.18.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.
package/README.md CHANGED
@@ -15,10 +15,10 @@ deck / curator / arena consume it instead of running `git clone` themselves.
15
15
 
16
16
  ```bash
17
17
  # Scan cold pool for unreferenced repos
18
- bunx @lythos/cold-pool@0.17.10 prune [--yes] [--dry-run]
18
+ bunx @lythos/cold-pool@0.18.0 prune [--yes] [--dry-run]
19
19
 
20
20
  # Validate lock file against cold pool
21
- bunx @lythos/cold-pool@0.17.10 validate [--lock ./skill-deck.lock]
21
+ bunx @lythos/cold-pool@0.18.0 validate [--lock ./skill-deck.lock]
22
22
  ```
23
23
 
24
24
  ## Architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/cold-pool",
3
- "version": "0.17.10",
3
+ "version": "0.18.0",
4
4
  "description": "Cold pool service layer — dedicated resource holder for skill repositories with intent/plan/execute primitives. Single owner of git side-effects; consumed by deck/curator/arena.",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "homepage": "https://github.com/lythos-labs/lythoskill/tree/main/packages/lythoskill-cold-pool#readme",
44
44
  "dependencies": {
45
- "@lythos/infra": "^0.17.10",
45
+ "@lythos/infra": "^0.18.0",
46
46
  "simple-git": "^3.36.0"
47
47
  },
48
48
  "engines": {
package/src/git-io.ts CHANGED
@@ -15,7 +15,7 @@ import { dirname, resolve } from 'node:path'
15
15
  export type GitExec = typeof execFileSync
16
16
 
17
17
  /** Read LYTHOS_SOCKS_PROXY env var and return git-config args if present. */
18
- function socksProxyArgs(): string[] {
18
+ export function socksProxyArgs(): string[] {
19
19
  const proxy = process.env.LYTHOS_SOCKS_PROXY?.trim()
20
20
  if (!proxy) return []
21
21
  const url = proxy.startsWith('socks5://') ? proxy : `socks5://${proxy}`
@@ -50,78 +50,168 @@ describe('probeConnectivity', () => {
50
50
  }
51
51
  }
52
52
 
53
- // ── Tracer Bullet ──
54
- it('direct reachable → returns direct path', async () => {
55
- mockFetch(
56
- new Map([
57
- ['https://example.com/skill', new Response(null, { status: 200 })],
58
- ]),
59
- )
53
+ /** execFileSync fake: per-URL git ls-remote success/failure; curl never succeeds. */
54
+ function mockGitExec(okUrls: string[] = []) {
55
+ const calls: Array<{ file: string; args: string[] }> = []
56
+ const exec = (file: unknown, args: unknown) => {
57
+ const a = (args as string[]).map(String)
58
+ calls.push({ file: String(file), args: a })
59
+ if (String(file) === 'git') {
60
+ const url = a[a.length - 1]!
61
+ if (okUrls.includes(url)) return ''
62
+ throw new Error(`git ls-remote failed for ${url}`)
63
+ }
64
+ throw new Error('curl failed')
65
+ }
66
+ return { exec: exec as any, calls }
67
+ }
68
+
69
+ /** execFileSync fake where git can never answer (e.g. binary missing). */
70
+ function gitUnavailable() {
71
+ return (() => {
72
+ throw new Error('spawnSync git ENOENT')
73
+ }) as any
74
+ }
75
+
76
+ const DIRECT = 'https://example.com/skill'
77
+ const MIRROR = 'https://my-mirror.example.com'
78
+ const MIRROR_URL = `${MIRROR}/${DIRECT}`
79
+ const infoRefs = (u: string) => `${u}/info/refs?service=git-upload-pack`
80
+
81
+ // ── Tier 1: git-verified ──────────────────────────────────────────
60
82
 
61
- const result = await probeConnectivity('https://example.com/skill')
83
+ it('direct ls-remote ok → git-verified direct, fetch never called', async () => {
84
+ const { exec } = mockGitExec([DIRECT])
85
+ mockFetch(new Map([[infoRefs(DIRECT), new Response(null, { status: 200 })]]))
86
+
87
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: exec })
62
88
 
63
89
  expect(result).toMatchObject({
64
90
  path: 'direct',
65
- url: 'https://example.com/skill',
91
+ url: DIRECT,
66
92
  latencyMs: expect.any(Number),
93
+ confidence: 'git-verified',
67
94
  })
95
+ expect(fetchCalls.length).toBe(0)
68
96
  })
69
97
 
70
- // ── Vertical Slice 2 ──
71
- it('direct fails, user mirror ok → returns mirror path', async () => {
72
- process.env.LYTHOS_GH_MIRROR = 'https://my-mirror.example.com'
73
- mockFetch(
74
- new Map([
75
- ['https://example.com/skill', new Response(null, { status: 500 })],
76
- ['https://my-mirror.example.com/https://example.com/skill', new Response(null, { status: 200 })],
77
- ]),
78
- )
98
+ it('direct ls-remote fails, mirror ls-remote ok → git-verified mirror', async () => {
99
+ process.env.LYTHOS_GH_MIRROR = MIRROR
100
+ const { exec } = mockGitExec([MIRROR_URL])
101
+ mockFetch(new Map())
79
102
 
80
- const result = await probeConnectivity('https://example.com/skill')
103
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: exec })
81
104
 
82
105
  expect(result).toMatchObject({
83
106
  path: 'mirror',
84
- url: 'https://my-mirror.example.com/https://example.com/skill',
85
- latencyMs: expect.any(Number),
107
+ url: MIRROR_URL,
108
+ confidence: 'git-verified',
86
109
  })
110
+ // direct failure recorded for diagnostics
111
+ expect(result?.failures?.some((f) => f.url === DIRECT)).toBe(true)
87
112
  })
88
113
 
89
- // ── Vertical Slice 3 ──
90
- it('all paths fail → returns undefined', async () => {
91
- mockFetch(new Map())
114
+ it('HEAD-blocking mirror is NOT rejected when ls-remote succeeds (the K3 false negative)', async () => {
115
+ // ghfast.top-style mirror: rejects HEAD/GET on bare URL with 403, but
116
+ // serves git smart-HTTP fine — ls-remote (clone's handshake) succeeds.
117
+ process.env.LYTHOS_GH_MIRROR = MIRROR
118
+ const { exec } = mockGitExec([MIRROR_URL])
119
+ globalThis.fetch = async () => new Response(null, { status: 403 })
92
120
 
93
- const result = await probeConnectivity('https://example.com/skill')
121
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: exec })
94
122
 
95
- expect(result).toBeUndefined()
123
+ expect(result).toBeDefined()
124
+ expect(result?.path).toBe('mirror')
125
+ expect(result?.confidence).toBe('git-verified')
126
+ expect(result?.authRequired).toBeUndefined()
96
127
  })
97
128
 
98
- // ── Vertical Slice 4 ──
99
- it('404 is treated as reachable (server alive)', async () => {
100
- mockFetch(
101
- new Map([
102
- ['https://example.com/skill', new Response(null, { status: 404 })],
103
- ]),
104
- )
129
+ it('LYTHOS_SOCKS_PROXY set → ls-remote injects -c http.proxy flags (same as clone)', async () => {
130
+ process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
131
+ const { exec, calls } = mockGitExec([DIRECT])
132
+
133
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: exec })
134
+
135
+ expect(result?.confidence).toBe('git-verified')
136
+ const gitCall = calls.find((c) => c.file === 'git')!
137
+ expect(gitCall.args).toContain('http.proxy=socks5://127.0.0.1:1080')
138
+ expect(gitCall.args).toContain('https.proxy=socks5://127.0.0.1:1080')
139
+ })
140
+
141
+ // ── Tier 2: http-signal (git cannot answer) ──────────────────────
142
+
143
+ it('git unavailable, GET info/refs 200 → http-signal direct', async () => {
144
+ mockFetch(new Map([[infoRefs(DIRECT), new Response(null, { status: 200 })]]))
145
+
146
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
147
+
148
+ expect(result).toMatchObject({
149
+ path: 'direct',
150
+ url: DIRECT,
151
+ confidence: 'http-signal',
152
+ })
153
+ expect(fetchCalls.length).toBe(1)
154
+ expect(fetchCalls[0].url).toBe(infoRefs(DIRECT))
155
+ expect(fetchCalls[0].options.method).toBe('GET')
156
+ })
157
+
158
+ it('403 on GET info/refs → reachable with authRequired (not "blocked")', async () => {
159
+ mockFetch(new Map([[infoRefs(DIRECT), new Response(null, { status: 403 })]]))
160
+
161
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
162
+
163
+ expect(result).toMatchObject({ path: 'direct', confidence: 'http-signal', authRequired: true })
164
+ })
105
165
 
106
- const result = await probeConnectivity('https://example.com/skill')
166
+ it('401 on GET info/refs → reachable with authRequired', async () => {
167
+ mockFetch(new Map([[infoRefs(DIRECT), new Response(null, { status: 401 })]]))
168
+
169
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
170
+
171
+ expect(result?.authRequired).toBe(true)
172
+ })
173
+
174
+ it('404 on GET info/refs → reachable (host alive), no authRequired', async () => {
175
+ mockFetch(new Map([[infoRefs(DIRECT), new Response(null, { status: 404 })]]))
176
+
177
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
107
178
 
108
179
  expect(result?.path).toBe('direct')
180
+ expect(result?.authRequired).toBeUndefined()
109
181
  })
110
182
 
111
- // ── Vertical Slice 5: Racing behavior ──
112
- it('probes race concurrently, not sequentially', async () => {
113
- process.env.LYTHOS_GH_MIRROR = 'https://my-mirror.example.com'
183
+ it('git unavailable, direct HTTP fails, mirror HTTP ok → http-signal mirror', async () => {
184
+ process.env.LYTHOS_GH_MIRROR = MIRROR
185
+ mockFetch(new Map([[infoRefs(MIRROR_URL), new Response(null, { status: 200 })]]))
186
+
187
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
188
+
189
+ expect(result).toMatchObject({ path: 'mirror', url: MIRROR_URL, confidence: 'http-signal' })
190
+ })
191
+
192
+ // ── Both tiers fail everywhere → undefined ───────────────────────
193
+
194
+ it('undefined only when both tiers fail on all URLs', async () => {
195
+ process.env.LYTHOS_GH_MIRROR = MIRROR
196
+ mockFetch(new Map()) // every fetch → ENOTFOUND
197
+
198
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
199
+
200
+ expect(result).toBeUndefined()
201
+ })
202
+
203
+ // ── Tier-2 racing behavior ───────────────────────────────────────
204
+
205
+ it('tier-2 HTTP probes race concurrently, not sequentially', async () => {
206
+ process.env.LYTHOS_GH_MIRROR = MIRROR
114
207
  const start = performance.now()
115
208
 
116
209
  mockFetch(
117
- new Map([
118
- ['https://example.com/skill', new Response(null, { status: 500 })],
119
- ['https://my-mirror.example.com/https://example.com/skill', new Response(null, { status: 200 })],
120
- ]),
210
+ new Map([[infoRefs(MIRROR_URL), new Response(null, { status: 200 })]]),
121
211
  50, // each mock fetch takes 50ms
122
212
  )
123
213
 
124
- const result = await probeConnectivity('https://example.com/skill')
214
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
125
215
 
126
216
  const elapsed = performance.now() - start
127
217
 
@@ -130,23 +220,7 @@ describe('probeConnectivity', () => {
130
220
  expect(elapsed).toBeLessThan(150)
131
221
  })
132
222
 
133
- // ── Vertical Slice 6: No user mirror set → only probes direct ──
134
- it('without user mirror, only probes direct', async () => {
135
- mockFetch(
136
- new Map([
137
- ['https://example.com/skill', new Response(null, { status: 200 })],
138
- ]),
139
- )
140
-
141
- const result = await probeConnectivity('https://example.com/skill')
142
-
143
- expect(result?.path).toBe('direct')
144
- expect(fetchCalls.length).toBe(1)
145
- expect(fetchCalls[0].url).toBe('https://example.com/skill')
146
- })
147
-
148
- // ── Vertical Slice 7: Timeout honored ──
149
- it('timeout aborts slow probes', async () => {
223
+ it('timeout aborts slow tier-2 probes', async () => {
150
224
  globalThis.fetch = async (_input, init?) => {
151
225
  return new Promise((_, reject) => {
152
226
  const timer = setTimeout(
@@ -161,83 +235,96 @@ describe('probeConnectivity', () => {
161
235
  }
162
236
 
163
237
  const start = performance.now()
164
- const result = await probeConnectivity('https://example.com/skill', 100)
238
+ const result = await probeConnectivity(DIRECT, 100, { execFileSync: gitUnavailable() })
165
239
  const elapsed = performance.now() - start
166
240
 
167
241
  expect(result).toBeUndefined()
168
242
  expect(elapsed).toBeLessThan(500) // 100ms timeout + overhead
169
243
  })
170
244
 
171
- // ── Vertical Slice 8: SOCKS proxy routing ──
172
- it('SOCKS proxy set, curl succeeds → returns direct path', async () => {
245
+ // ── Tier-2 SOCKS proxy routing (curl) ────────────────────────────
246
+
247
+ it('SOCKS proxy set, git unavailable, curl ok → http-signal direct via curl', async () => {
173
248
  process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
174
249
  const execCalls: Array<{ file: string; args: string[] }> = []
175
250
 
176
251
  const mockExec = (file: unknown, args: unknown) => {
177
- execCalls.push({ file: String(file), args: args as string[] })
178
- return ''
252
+ const a = (args as string[]).map(String)
253
+ execCalls.push({ file: String(file), args: a })
254
+ if (String(file) === 'curl') return '200'
255
+ throw new Error('spawnSync git ENOENT')
179
256
  }
180
- const result = await probeConnectivity('https://example.com/skill', 3000, {
181
- execFileSync: mockExec as any,
182
- })
257
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: mockExec as any })
183
258
 
184
259
  expect(result).toMatchObject({
185
260
  path: 'direct',
186
- url: 'https://example.com/skill',
261
+ url: DIRECT,
187
262
  latencyMs: expect.any(Number),
263
+ confidence: 'http-signal',
188
264
  })
189
- expect(execCalls.length).toBe(1)
190
- expect(execCalls[0].file).toBe('curl')
191
- expect(execCalls[0].args).toContain('--proxy')
192
- expect(execCalls[0].args).toContain('socks5://127.0.0.1:1080')
265
+ const curlCalls = execCalls.filter((c) => c.file === 'curl')
266
+ expect(curlCalls.length).toBe(1)
267
+ expect(curlCalls[0].args).toContain('--proxy')
268
+ expect(curlCalls[0].args).toContain('socks5://127.0.0.1:1080')
269
+ expect(curlCalls[0].args[curlCalls[0].args.length - 1]).toBe(infoRefs(DIRECT))
193
270
  })
194
271
 
195
- // ── Vertical Slice 9: SOCKS proxy with socks5:// prefix ──
196
272
  it('SOCKS proxy already has socks5:// prefix → does not double-prefix', async () => {
197
273
  process.env.LYTHOS_SOCKS_PROXY = 'socks5://proxy.example.com:1080'
198
274
  const execCalls: Array<{ file: string; args: string[] }> = []
199
275
 
200
- const mockExec2 = (file: unknown, args: unknown) => {
201
- execCalls.push({ file: String(file), args: args as string[] })
202
- return ''
276
+ const mockExec = (file: unknown, args: unknown) => {
277
+ const a = (args as string[]).map(String)
278
+ execCalls.push({ file: String(file), args: a })
279
+ if (String(file) === 'curl') return '200'
280
+ throw new Error('spawnSync git ENOENT')
281
+ }
282
+ await probeConnectivity(DIRECT, 3000, { execFileSync: mockExec as any })
283
+
284
+ const curlCall = execCalls.find((c) => c.file === 'curl')!
285
+ expect(curlCall.args).toContain('socks5://proxy.example.com:1080')
286
+ expect(curlCall.args).not.toContain('socks5://socks5://proxy.example.com:1080')
287
+ })
288
+
289
+ it('SOCKS curl gets 403 → http-signal direct with authRequired', async () => {
290
+ process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
291
+
292
+ const mockExec = (file: unknown) => {
293
+ if (String(file) === 'curl') return '403'
294
+ throw new Error('spawnSync git ENOENT')
203
295
  }
204
- await probeConnectivity('https://example.com/skill', 3000, {
205
- execFileSync: mockExec2 as any,
206
- })
296
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: mockExec as any })
207
297
 
208
- expect(execCalls[0].args).toContain('socks5://proxy.example.com:1080')
209
- expect(execCalls[0].args).not.toContain('socks5://socks5://proxy.example.com:1080')
298
+ expect(result).toMatchObject({ path: 'direct', confidence: 'http-signal', authRequired: true })
210
299
  })
211
300
 
212
- // ── Vertical Slice 10: SOCKS proxy fails → no automatic unproxied fallback ──
213
- it('SOCKS proxy fails → no automatic unproxied fallback', async () => {
301
+ it('SOCKS proxy fails (git + curl) → no automatic unproxied fallback', async () => {
214
302
  process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
215
303
 
216
- const result = await probeConnectivity('https://example.com/skill', 3000, {
217
- execFileSync: () => {
304
+ const result = await probeConnectivity(DIRECT, 3000, {
305
+ execFileSync: (() => {
218
306
  throw new Error('curl failed')
219
- },
307
+ }) as any,
220
308
  fetch: async () => new Response(null, { status: 200 }),
221
309
  })
222
310
 
223
- // Design choice: when SOCKS proxy is configured but curl fails, the direct
224
- // probe fails. No automatic fallback to unproxied fetch — the user
225
- // explicitly chose to route traffic through the proxy, so we honor that
226
- // choice rather than silently bypassing it.
311
+ // Design choice: when SOCKS proxy is configured but both git and curl
312
+ // fail, the direct probe fails. No automatic fallback to unproxied
313
+ // fetch — the user explicitly chose to route traffic through the
314
+ // proxy, so we honor that choice rather than silently bypassing it.
227
315
  expect(result).toBeUndefined()
228
316
  })
229
317
 
230
- // ── Vertical Slice 11: SOCKS proxy only affects direct, mirror still works ──
231
- it('SOCKS proxy fails but mirror succeeds', async () => {
318
+ it('SOCKS proxy fails but mirror succeeds → mirror; only one curl call', async () => {
232
319
  process.env.LYTHOS_SOCKS_PROXY = '127.0.0.1:1080'
233
- process.env.LYTHOS_GH_MIRROR = 'https://my-mirror.example.com'
320
+ process.env.LYTHOS_GH_MIRROR = MIRROR
234
321
  const execCalls: Array<{ file: string; args: string[] }> = []
235
322
 
236
- const result = await probeConnectivity('https://example.com/skill', 3000, {
237
- execFileSync: (file, args) => {
238
- execCalls.push({ file: String(file), args: args as string[] })
239
- throw new Error('curl failed')
240
- },
323
+ const result = await probeConnectivity(DIRECT, 3000, {
324
+ execFileSync: ((file: unknown, args: unknown) => {
325
+ execCalls.push({ file: String(file), args: (args as string[]).map(String) })
326
+ throw new Error('proxy unreachable')
327
+ }) as any,
241
328
  fetch: async (input) => {
242
329
  const url = String(input)
243
330
  if (url.includes('my-mirror')) {
@@ -249,7 +336,7 @@ describe('probeConnectivity', () => {
249
336
 
250
337
  // SOCKS proxy is only used for direct probes; mirror probes use native fetch
251
338
  expect(result?.path).toBe('mirror')
252
- expect(execCalls.length).toBe(1) // only one curl call for direct probe
339
+ expect(execCalls.filter((c) => c.file === 'curl').length).toBe(1)
253
340
  })
254
341
 
255
342
  // ── Backward compat: legacy env var name still works ──
@@ -265,12 +352,11 @@ describe('probeConnectivity', () => {
265
352
 
266
353
  mockFetch(
267
354
  new Map([
268
- ['https://example.com/skill', new Response(null, { status: 500 })],
269
- ['https://legacy-mirror.example.com/https://example.com/skill', new Response(null, { status: 200 })],
355
+ [infoRefs(`https://legacy-mirror.example.com/${DIRECT}`), new Response(null, { status: 200 })],
270
356
  ]),
271
357
  )
272
358
 
273
- const result = await probeConnectivity('https://example.com/skill')
359
+ const result = await probeConnectivity(DIRECT, 3000, { execFileSync: gitUnavailable() })
274
360
 
275
361
  console.warn = originalWarn
276
362
 
package/src/mirror.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { execFileSync } from 'node:child_process'
17
+ import { socksProxyArgs } from './git-io.js'
17
18
 
18
19
  export function getMirror(): string | undefined {
19
20
  let v = process.env.LYTHOS_GH_MIRROR?.trim()
@@ -52,6 +53,15 @@ export interface ProbeResult {
52
53
  path: 'direct' | 'mirror'
53
54
  url: string
54
55
  latencyMs: number
56
+ /**
57
+ * Evidence tier. 'git-verified' = `git ls-remote` succeeded — the exact
58
+ * handshake a clone performs, on the same network stack. 'http-signal' =
59
+ * only an HTTP response was observed (fallback tier when git cannot
60
+ * answer); weaker evidence, so callers should treat it as advisory.
61
+ */
62
+ confidence: 'git-verified' | 'http-signal'
63
+ /** Set when the HTTP tier got 401/403 — host reachable, credentials likely required. */
64
+ authRequired?: boolean
55
65
  }
56
66
 
57
67
  export interface ProbeFailure {
@@ -64,42 +74,88 @@ export interface ProbeDeps {
64
74
  execFileSync?: typeof execFileSync
65
75
  }
66
76
 
77
+ /** Short, single-line reason from an exec/fetch error (stderr first line wins). */
78
+ function errReason(err: unknown): string {
79
+ const stderr = (err as { stderr?: { toString: () => string } })?.stderr?.toString().trim()
80
+ if (stderr) return stderr.split('\n')[0]!
81
+ return err instanceof Error ? err.message : String(err)
82
+ }
83
+
67
84
  /**
68
- * Quick connectivity probe: race direct + user mirror (if set) concurrently.
69
- * Uses short timeout (default 3s) to fail fast instead of waiting for
70
- * the full git clone / fetch timeout.
85
+ * Tiered connectivity probe: direct first, user mirror (if set) alongside.
71
86
  *
72
- * When LYTHOS_SOCKS_PROXY is set, direct probes route through the SOCKS
73
- * proxy via curl (Bun fetch does not support socks5://).
87
+ * Tier 1 — `git ls-remote --heads <url>`: the same first handshake a clone
88
+ * performs, on the same network stack — it inherits the user's git config
89
+ * (http.proxy, url.insteadOf, http.sslVerify, credentials) and gets the same
90
+ * LYTHOS_SOCKS_PROXY `-c` flags that git-io's clone/pull inject. This is the
91
+ * only tier with real predictive power for clone success.
74
92
  *
75
- * Returns the first success, or undefined with failures recorded.
93
+ * Tier 2 (fallback when no ls-remote succeeds — e.g. git binary missing) —
94
+ * HTTP `GET <url>/info/refs?service=git-upload-pack`, the request a clone
95
+ * actually sends first (NOT a bare-URL HEAD, which git smart-HTTP reverse
96
+ * proxies like ghfast.top reject with 403/405). Any HTTP response proves the
97
+ * host is reachable (as opposed to DNS/TLS failure); 401/403 is classified
98
+ * as auth-required, not "blocked". The direct tier keeps the
99
+ * LYTHOS_SOCKS_PROXY curl route — Bun fetch cannot speak socks5://, and
100
+ * git ls-remote already inherits git proxy config, so SOCKS-via-curl is
101
+ * only an HTTP-tier concern.
102
+ *
103
+ * Returns the first success with per-URL failures recorded, or undefined
104
+ * only when BOTH tiers fail on ALL URLs.
76
105
  */
77
106
  export async function probeConnectivity(
78
107
  url: string,
79
108
  timeoutMs = 3000,
80
109
  deps?: ProbeDeps,
81
110
  ): Promise<ProbeResult & { failures?: ProbeFailure[] } | undefined> {
82
- const start = performance.now()
83
111
  const failures: ProbeFailure[] = []
84
112
  const _fetch = deps?.fetch ?? globalThis.fetch
85
113
  const _exec = deps?.execFileSync ?? execFileSync
114
+ const targets: Array<{ targetUrl: string; pathLabel: 'direct' | 'mirror' }> = [
115
+ { targetUrl: url, pathLabel: 'direct' },
116
+ ...mirrorUrls(url).map((m) => ({ targetUrl: m, pathLabel: 'mirror' as const })),
117
+ ]
118
+
119
+ // ── Tier 1: git ls-remote (ground truth) ───────────────────────────
120
+ // execFileSync is synchronous, so tier-1 probes run sequentially — the
121
+ // per-probe timeout bounds each attempt.
122
+ for (const { targetUrl, pathLabel } of targets) {
123
+ const t0 = performance.now()
124
+ try {
125
+ _exec('git', [...socksProxyArgs(), 'ls-remote', '--heads', targetUrl], {
126
+ encoding: 'utf-8',
127
+ timeout: timeoutMs,
128
+ stdio: ['ignore', 'pipe', 'pipe'],
129
+ })
130
+ return {
131
+ path: pathLabel,
132
+ url: targetUrl,
133
+ latencyMs: Math.round(performance.now() - t0),
134
+ confidence: 'git-verified',
135
+ failures,
136
+ }
137
+ } catch (err) {
138
+ failures.push({ url: targetUrl, reason: errReason(err) })
139
+ }
140
+ }
86
141
 
87
- async function tryFetch(
142
+ // ── Tier 2: HTTP signal (degraded path when git cannot answer) ─────
143
+ async function tryHttp(
88
144
  targetUrl: string,
89
145
  pathLabel: 'direct' | 'mirror',
90
146
  ): Promise<ProbeResult | undefined> {
91
147
  const t0 = performance.now()
148
+ const probeUrl = `${targetUrl.replace(/\/+$/, '')}/info/refs?service=git-upload-pack`
92
149
  try {
93
150
  // Route direct probes through SOCKS proxy when configured
94
151
  if (pathLabel === 'direct') {
95
152
  const socksProxy = process.env.LYTHOS_SOCKS_PROXY?.trim()
96
153
  if (socksProxy) {
97
154
  const proxyUrl = socksProxy.startsWith('socks5://') ? socksProxy : `socks5://${socksProxy}`
98
- _exec(
155
+ const out = _exec(
99
156
  'curl',
100
157
  [
101
158
  '--silent',
102
- '--head',
103
159
  '--location',
104
160
  '--proxy',
105
161
  proxyUrl,
@@ -107,38 +163,48 @@ export async function probeConnectivity(
107
163
  String(Math.ceil(timeoutMs / 1000)),
108
164
  '--max-time',
109
165
  String(Math.ceil(timeoutMs / 1000)),
110
- targetUrl,
166
+ '--output',
167
+ '/dev/null',
168
+ '--write-out',
169
+ '%{http_code}',
170
+ probeUrl,
111
171
  ],
112
172
  { encoding: 'utf-8', timeout: timeoutMs + 500 },
113
173
  )
114
- return { path: pathLabel, url: targetUrl, latencyMs: Math.round(performance.now() - t0) }
174
+ const code = Number(String(out).trim())
175
+ return {
176
+ path: pathLabel,
177
+ url: targetUrl,
178
+ latencyMs: Math.round(performance.now() - t0),
179
+ confidence: 'http-signal',
180
+ authRequired: code === 401 || code === 403 || undefined,
181
+ }
115
182
  }
116
183
  }
117
184
 
118
- const res = await _fetch(targetUrl, {
119
- method: 'HEAD',
185
+ const res = await _fetch(probeUrl, {
186
+ method: 'GET',
120
187
  signal: AbortSignal.timeout(timeoutMs),
121
188
  redirect: 'follow',
122
189
  })
123
- if (res.ok || res.status === 404) {
124
- // 404 means server reachable, resource may not exist
125
- return { path: pathLabel, url: targetUrl, latencyMs: Math.round(performance.now() - t0) }
190
+ // Any HTTP response means the host is reachable — even a 4xx/5xx
191
+ // (a 4xx only says this URL/method is not accepted, unlike a
192
+ // DNS/TLS failure which means the host cannot be reached at all).
193
+ return {
194
+ path: pathLabel,
195
+ url: targetUrl,
196
+ latencyMs: Math.round(performance.now() - t0),
197
+ confidence: 'http-signal',
198
+ authRequired: res.status === 401 || res.status === 403 || undefined,
126
199
  }
127
- failures.push({ url: targetUrl, reason: `HTTP ${res.status}` })
128
200
  } catch (err) {
129
- failures.push({
130
- url: targetUrl,
131
- reason: err instanceof Error ? err.message : String(err),
132
- })
201
+ failures.push({ url: targetUrl, reason: errReason(err) })
133
202
  }
134
203
  return undefined
135
204
  }
136
205
 
137
- // Build all probe promises (direct + mirrors) and race for first success
138
- const probes = [tryFetch(url, 'direct')]
139
- for (const mirrorUrl of mirrorUrls(url)) {
140
- probes.push(tryFetch(mirrorUrl, 'mirror'))
141
- }
206
+ // Race direct + mirrors concurrently; first success wins
207
+ const probes = targets.map((t) => tryHttp(t.targetUrl, t.pathLabel))
142
208
 
143
209
  let result: ProbeResult | undefined
144
210
  let pending = probes.length