@lythos/cold-pool 0.9.48 → 0.9.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lythos/cold-pool",
3
- "version": "0.9.48",
3
+ "version": "0.9.49",
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",
package/src/cli.ts CHANGED
File without changes
package/src/fetch-plan.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  * `executeFetchPlan` will refuse to clone (status: 'failed').
11
11
  */
12
12
  import { existsSync } from 'node:fs'
13
+ import { execFileSync } from 'node:child_process'
13
14
  import type { ColdPool } from './cold-pool.js'
14
15
  import type { Locator, FetchPlan, FetchResult, FetchIO } from './types.js'
15
16
  import { gitClone } from './git-io.js'
@@ -29,7 +30,7 @@ export function buildFetchPlan(
29
30
  locator,
30
31
  cloneUrl,
31
32
  targetDir,
32
- ref: opts?.ref,
33
+ ref: opts?.ref ?? locator.ref, // explicit opts override locator's #ref
33
34
  alreadyExists: existsSync(targetDir),
34
35
  }
35
36
  }
@@ -48,6 +49,19 @@ export function executeFetchPlan(plan: FetchPlan, io?: FetchIO): FetchResult {
48
49
  }
49
50
 
50
51
  if (exists(plan.targetDir)) {
52
+ if (plan.ref) {
53
+ if (plan.ref.startsWith('-')) {
54
+ log(`⚠️ Ref "${plan.ref}" starts with dash — refusing to avoid git option injection`)
55
+ } else {
56
+ try {
57
+ log(`🔄 checking out ${plan.ref} in ${plan.targetDir}`)
58
+ execFileSync('git', ['-C', plan.targetDir, 'fetch', '--depth', '1', 'origin', plan.ref], { stdio: 'pipe' })
59
+ execFileSync('git', ['-C', plan.targetDir, 'checkout', plan.ref], { stdio: 'pipe' })
60
+ } catch {
61
+ log(`⚠️ Could not checkout ${plan.ref} — using current HEAD`)
62
+ }
63
+ }
64
+ }
51
65
  log(`✓ already present: ${plan.targetDir}`)
52
66
  return {
53
67
  status: 'already-present',
@@ -11,6 +11,7 @@ describe('parseLocator — accepted forms', () => {
11
11
  repo: 'skills',
12
12
  skill: 'skills/pdf',
13
13
  isLocalhost: false,
14
+ ref: null,
14
15
  })
15
16
  })
16
17
 
@@ -35,6 +36,7 @@ describe('parseLocator — accepted forms', () => {
35
36
  repo: 'design-doc-mermaid',
36
37
  skill: null,
37
38
  isLocalhost: false,
39
+ ref: null,
38
40
  })
39
41
  })
40
42
 
@@ -52,6 +54,7 @@ describe('parseLocator — accepted forms', () => {
52
54
  repo: 'my-skill',
53
55
  skill: null,
54
56
  isLocalhost: true,
57
+ ref: null,
55
58
  })
56
59
  })
57
60
 
@@ -99,6 +102,7 @@ describe('parseLocator — rejected forms (per ADR-20260502012643244 FQ-only)',
99
102
  repo: 'skills',
100
103
  skill: 'my-skill',
101
104
  isLocalhost: true,
105
+ ref: null,
102
106
  })
103
107
  })
104
108
 
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Three accepted forms (everything else returns null):
5
5
  * - `host.tld/owner/repo[/skill]` — remote skill
6
+ * - `host.tld/owner/repo[/skill]#ref` — remote skill at branch/tag/commit
6
7
  * - `host.tld/owner/repo` — remote standalone (skill = null)
7
8
  * - `localhost/owner/repo[/skill]` — local skill, same shape as remote
8
9
  *
@@ -13,6 +14,9 @@
13
14
  *
14
15
  * Single-name `localhost/<name>` is rejected — that's a post-compaction
15
16
  * agent invention, not the canonical form.
17
+ *
18
+ * `#ref` suffix (branch/tag/commit) is compatible with skills.sh's
19
+ * `parseFragmentRef`. The ref is passed to gitClone for checkout.
16
20
  */
17
21
  import type { Locator } from './types.js'
18
22
 
@@ -20,7 +24,22 @@ export function parseLocator(input: string): Locator | null {
20
24
  const trimmed = input.trim()
21
25
  if (!trimmed) return null
22
26
 
23
- const parts = trimmed.split('/').filter(Boolean)
27
+ // Extract #ref suffix (branch/tag/commit)
28
+ let ref: string | null = null
29
+ let pathPart = trimmed
30
+ const hashIdx = trimmed.indexOf('#')
31
+ if (hashIdx >= 0) {
32
+ pathPart = trimmed.slice(0, hashIdx)
33
+ ref = trimmed.slice(hashIdx + 1) || null
34
+ // Reject refs that look like git option injection or path traversal
35
+ if (ref && (ref.startsWith('-') || ref.includes('..'))) {
36
+ return null
37
+ }
38
+ }
39
+
40
+ const parts = pathPart.split('/')
41
+ // Reject empty segments (double slashes) and path traversal
42
+ if (parts.some(p => p === '' || p === '..' || p === '.')) return null
24
43
  // Need at least host/owner/repo (3 segments) for any FQ form
25
44
  if (parts.length < 3) return null
26
45
 
@@ -34,6 +53,7 @@ export function parseLocator(input: string): Locator | null {
34
53
  owner: parts[1],
35
54
  repo: parts[2],
36
55
  skill: parts.length > 3 ? parts.slice(3).join('/') : null,
56
+ ref,
37
57
  isLocalhost,
38
58
  }
39
59
  }
@@ -41,5 +61,6 @@ export function parseLocator(input: string): Locator | null {
41
61
  /** Recompose an FQ locator string from a parsed `Locator`. */
42
62
  export function formatLocator(locator: Locator): string {
43
63
  const base = `${locator.host}/${locator.owner}/${locator.repo}`
44
- return locator.skill ? `${base}/${locator.skill}` : base
64
+ const withSkill = locator.skill ? `${base}/${locator.skill}` : base
65
+ return locator.ref ? `${withSkill}#${locator.ref}` : withSkill
45
66
  }
package/src/types.ts CHANGED
@@ -22,6 +22,7 @@ export interface Locator {
22
22
  readonly owner: string
23
23
  readonly repo: string
24
24
  readonly skill: string | null
25
+ readonly ref: string | null // optional branch/tag/commit (#ref suffix)
25
26
  readonly isLocalhost: boolean
26
27
  }
27
28