Skip to content

Commit 3549802

Browse files
Kasper JungeKasper Junge
authored andcommitted
release: v0.8.4b1
1 parent 29c25c0 commit 3549802

21 files changed

Lines changed: 1253 additions & 99 deletions

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
## [Unreleased]
44

5+
## [0.8.4b1] - 2026-04-12
6+
7+
### Added
8+
- Package/bundle dependencies can now expand and install transitive skill and ralph dependencies, including nested packages.
9+
- `add`, `remove`, `sync`, and `upgrade` now keep package, skill, and ralph lockfile entries in sync across transitive dependency changes.
10+
11+
### Changed
12+
- Lockfile handling now tracks package parent relationships for entries with one or more parent packages.
13+
- Package conflict detection now distinguishes resource types with the same installed name.
14+
- Shared install and lockfile paths were refactored to support package dependency expansion consistently across commands.
15+
16+
### Fixed
17+
- `remove` now removes package-owned transitive dependencies without removing direct dependencies that share the same installed name.
18+
- `sync` and `upgrade` now preserve and refresh package parent metadata for transitive lockfile entries.
19+
- Bare names, trailing slashes, and local dependency paths now match more consistently during remove and upgrade operations.
20+
- Package expansion now rejects remote package local paths that resolve outside the downloaded repository.
21+
- Remote package local paths are converted to same-repository remote handles when they point to top-level resource directories.
22+
- Lockfile commit SHAs and remote handle components receive stricter validation to prevent unsafe substitutions and path traversal.
23+
524
## [0.8.3] - 2026-04-12
625

726
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ everything with one command:
7575
dependencies = [
7676
{handle = "anthropics/skills/frontend-design", type = "skill"},
7777
{handle = "anthropics/skills/pdf", type = "skill"},
78+
{handle = "your-team/agent-resources/dev-workflow", type = "package"},
7879
]
7980
```
8081

agr.lock

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,38 @@ source = "github"
1919
commit = "a0d5bfd4d9658073029d33f979ac5a027568caec"
2020
content-hash = "sha256:75e47183c30bc8651e76286680eddac88a3024a7ee5a7f1bc486d4d3fdee34ce"
2121
installed-name = "playwright-cli"
22+
23+
[[skill]]
24+
handle = "computerlovetech/skills/github-issue-triage"
25+
source = "github"
26+
commit = "8bff044d76f9304cbd4981fb3ce4f837fd4eb5a2"
27+
content-hash = "sha256:35c62982b1bee8c0164a5cd43264b554f9b37697d6fda23f502107956a0ad889"
28+
installed-name = "github-issue-triage"
29+
30+
[[ralph]]
31+
handle = "computerlovetech/research/conduct-research"
32+
source = "github"
33+
commit = "637025a5e221264845bff7e6a89aaba7a7bdb919"
34+
content-hash = "sha256:3a9c1ef64234ccb61d9b681afc4305ffadad5735f0536f77759a41366e75c3a2"
35+
installed-name = "conduct-research"
36+
37+
[[ralph]]
38+
handle = "computerlovetech/ralphs/improve-codebase"
39+
source = "github"
40+
commit = "70a0dabf5a59ae1a3acaf50a9e619c83e174a35f"
41+
content-hash = "sha256:0afc55d5b2d4061fa82c270a147c66c8bc21589ab62c47d9747ddb5f382c708d"
42+
installed-name = "improve-codebase"
43+
44+
[[ralph]]
45+
handle = "computerlovetech/ralphs/bug-hunter"
46+
source = "github"
47+
commit = "70a0dabf5a59ae1a3acaf50a9e619c83e174a35f"
48+
content-hash = "sha256:ab2e4924cbb999f677523211af9cfda48ce8433b17545854bd770f29d7664c64"
49+
installed-name = "bug-hunter"
50+
51+
[[ralph]]
52+
handle = "computerlovetech/ralphs/security"
53+
source = "github"
54+
commit = "70a0dabf5a59ae1a3acaf50a9e619c83e174a35f"
55+
content-hash = "sha256:2b025b4e5546f2a04367a79e5f4201892bc155d22f53a55cfc0cd21071922de1"
56+
installed-name = "security"

agr.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
1+
# Source to fetch skills from
12
default_source = "github"
3+
4+
# Tools to install skills to (e.g. "claude", "cursor", "codex")
25
tools = ["claude", "codex", "opencode"]
6+
7+
# Primary tool for instruction sync (must be listed in tools)
8+
# default_tool = "claude"
9+
10+
# Default GitHub owner for short handles (e.g. "skill" resolves to "computerlovetech/skill")
11+
default_owner = "computerlovetech"
12+
13+
# Default GitHub repo for two-part handles (e.g. "owner/skill" resolves to "owner/skills/skill")
14+
default_repo = "skills"
15+
16+
# Sync instruction files across tools
17+
# sync_instructions = false
18+
19+
# Which tool's instruction file is the source of truth
20+
# canonical_instructions = "CLAUDE.md"
21+
322
dependencies = [
423
{path = "skills/agr-release", type = "skill"},
524
{handle = "maragudk/code-review", type = "skill"},
625
{handle = "microsoft/playwright-cli/playwright-cli", type = "skill"},
26+
{handle = "computerlovetech/research/conduct-research", type = "ralph"},
27+
{handle = "computerlovetech/skills/github-issue-triage", type = "skill"},
28+
{handle = "computerlovetech/ralphs/improve-codebase", type = "ralph"},
29+
{handle = "computerlovetech/ralphs/bug-hunter", type = "ralph"},
30+
{handle = "computerlovetech/ralphs/security", type = "ralph"},
731
]
832

933
[[source]]

agr/_install_common.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
checkout_full,
1919
checkout_sparse_paths,
2020
downloaded_repo,
21-
get_head_commit_full,
2221
git_list_files,
22+
safe_get_head_commit,
2323
)
2424
from agr.handle import ParsedHandle, iter_repo_candidates
2525
from agr.metadata import (
@@ -437,10 +437,7 @@ def locate_remote_dep(
437437
dep_source = prepare_fn(repo_dir, handle.name)
438438
if dep_source is None:
439439
continue
440-
try:
441-
commit = get_head_commit_full(repo_dir)
442-
except AgrError:
443-
commit = None
440+
commit = safe_get_head_commit(repo_dir)
444441
yield RemoteDepLocation(
445442
repo_dir=repo_dir,
446443
source_path=dep_source,

agr/commands/add.py

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""agr add command implementation."""
22

3+
from dataclasses import dataclass
34
from pathlib import Path
45

56
from agr.commands import CommandResult
@@ -21,7 +22,7 @@
2122
format_install_error,
2223
)
2324
from agr._install_common import InstallResult
24-
from agr.package import expand_packages, has_package_section
25+
from agr.package import detect_conflicts, expand_packages, has_package_section
2526
from agr.ralph_installer import fetch_and_install_ralph
2627
from agr.skill_installer import fetch_and_install_to_tools, list_remote_repo_skills
2728
from agr.handle import ParsedHandle, parse_handle
@@ -38,6 +39,25 @@
3839
from agr.tool import ToolConfig
3940

4041

42+
@dataclass
43+
class AddInstallResult:
44+
"""Result of installing one requested dependency."""
45+
46+
installed_paths: list[str]
47+
install_result: InstallResult
48+
dep_type: str
49+
lock_entries: list[tuple[str, LockedEntry]] | None = None
50+
51+
52+
def _parent_fields(parent_ids: set[str] | None) -> tuple[str | None, list[str] | None]:
53+
if not parent_ids:
54+
return None, None
55+
sorted_ids = sorted(parent_ids)
56+
if len(sorted_ids) == 1:
57+
return sorted_ids[0], None
58+
return None, sorted_ids
59+
60+
4161
def _detect_local_type(source_path: Path) -> str:
4262
"""Detect whether a local path is a skill, ralph, or package.
4363
@@ -100,7 +120,7 @@ def _install_dependency(
100120
default_repo: str | None,
101121
*,
102122
config: AgrConfig | None = None,
103-
) -> tuple[list[str], InstallResult, str]:
123+
) -> AddInstallResult:
104124
"""Install a dependency and return paths, metadata, and resolved type.
105125
106126
For local deps, installs directly as the detected type. For remote deps,
@@ -137,7 +157,7 @@ def _install_dependency(
137157
installed_paths = [
138158
f"{name}: {path}" for name, path in installed_paths_dict.items()
139159
]
140-
return installed_paths, install_result, dep_type
160+
return AddInstallResult(installed_paths, install_result, dep_type)
141161

142162
if handle.is_local:
143163
installed_path, install_result = fetch_and_install_ralph(
@@ -148,7 +168,7 @@ def _install_dependency(
148168
source=source,
149169
default_repo=default_repo,
150170
)
151-
return [str(installed_path)], install_result, dep_type
171+
return AddInstallResult([str(installed_path)], install_result, dep_type)
152172

153173
# Remote — try as skill first, fall back to ralph.
154174
try:
@@ -165,7 +185,7 @@ def _install_dependency(
165185
installed_paths = [
166186
f"{name}: {path}" for name, path in installed_paths_dict.items()
167187
]
168-
return installed_paths, install_result, DEPENDENCY_TYPE_SKILL
188+
return AddInstallResult(installed_paths, install_result, DEPENDENCY_TYPE_SKILL)
169189
except SkillNotFoundError:
170190
pass
171191

@@ -178,11 +198,23 @@ def _install_dependency(
178198
source=source,
179199
default_repo=default_repo,
180200
)
181-
return [str(installed_path)], install_result, DEPENDENCY_TYPE_RALPH
201+
return AddInstallResult(
202+
[str(installed_path)], install_result, DEPENDENCY_TYPE_RALPH
203+
)
182204
except RalphNotFoundError:
183-
raise SkillNotFoundError(
184-
f"'{handle.name}' not found as a skill or ralph in any configured source."
185-
) from None
205+
pass
206+
207+
return _install_package(
208+
handle,
209+
repo_root,
210+
tools,
211+
overwrite,
212+
resolver,
213+
source,
214+
skills_dirs,
215+
default_repo,
216+
config=config,
217+
)
186218

187219

188220
def _install_package(
@@ -196,7 +228,7 @@ def _install_package(
196228
default_repo: str | None,
197229
*,
198230
config: AgrConfig | None = None,
199-
) -> tuple[list[str], InstallResult, str]:
231+
) -> AddInstallResult:
200232
"""Expand a package and install its transitive leaf deps."""
201233
if config is None:
202234
config = AgrConfig()
@@ -214,9 +246,26 @@ def _install_package(
214246
config.default_owner,
215247
config.default_repo,
216248
)
249+
direct_ids = {dep.identifier for dep in config.dependencies}
250+
direct_ids.add(pkg_dep.identifier)
251+
direct_leaf_deps = [dep for dep in config.dependencies if not dep.is_package]
252+
resolved_deps = detect_conflicts(
253+
[*direct_leaf_deps, *expanded.dependencies], expanded.parents, direct_ids
254+
)
255+
resolved_keys = {(dep.type, dep.identifier) for dep in resolved_deps}
256+
direct_leaf_keys = {(dep.type, dep.identifier) for dep in direct_leaf_deps}
257+
expanded.dependencies = [
258+
dep
259+
for dep in expanded.dependencies
260+
if (dep.type, dep.identifier) in resolved_keys
261+
and (dep.type, dep.identifier) not in direct_leaf_keys
262+
]
217263

218264
installed_paths: list[str] = []
219265
first_result: InstallResult | None = None
266+
lock_entries: list[tuple[str, LockedEntry]] = [
267+
("package", entry) for entry in expanded.package_entries
268+
]
220269
for dep in expanded.dependencies:
221270
sub_handle = dep.to_parsed_handle(config.default_owner)
222271
sub_source = dep.resolve_source_name(config.default_source)
@@ -246,23 +295,68 @@ def _install_package(
246295
)
247296
if first_result is None:
248297
first_result = result
298+
parent_id = expanded.parents.get(dep.identifier)
299+
parent, parents = _parent_fields(
300+
expanded.parent_sets.get(dep.identifier)
301+
or ({parent_id} if parent_id else None)
302+
)
303+
if dep.is_local:
304+
lock_entries.append(
305+
(
306+
dep.type,
307+
LockedEntry(
308+
path=dep.path,
309+
installed_name=dep.installed_name,
310+
parent=parent,
311+
parents=parents,
312+
),
313+
)
314+
)
315+
else:
316+
lock_entries.append(
317+
(
318+
dep.type,
319+
LockedEntry(
320+
handle=dep.handle,
321+
source=result.source_name,
322+
commit=result.commit,
323+
content_hash=result.content_hash,
324+
installed_name=dep.installed_name,
325+
parent=parent,
326+
parents=parents,
327+
),
328+
)
329+
)
249330

250331
if first_result is None:
251332
first_result = InstallResult()
252333

253-
return installed_paths, first_result, DEPENDENCY_TYPE_PACKAGE
334+
return AddInstallResult(
335+
installed_paths,
336+
first_result,
337+
DEPENDENCY_TYPE_PACKAGE,
338+
lock_entries=lock_entries,
339+
)
254340

255341

256342
def _update_lockfile_for_adds(
257-
lockfile_updates: list[tuple[ParsedHandle, str, InstallResult, str]],
343+
lockfile_updates: list[
344+
tuple[
345+
ParsedHandle, str, InstallResult, str, list[tuple[str, LockedEntry]] | None
346+
]
347+
],
258348
config_path: Path,
259349
) -> None:
260350
"""Write install results to the lockfile."""
261351
if not lockfile_updates:
262352
return
263353
lockfile_path = build_lockfile_path(config_path)
264354
lockfile = load_lockfile(lockfile_path) or Lockfile()
265-
for handle, ref, install_result, dep_type in lockfile_updates:
355+
for handle, ref, install_result, dep_type, entries in lockfile_updates:
356+
if entries is not None:
357+
for kind, entry in entries:
358+
lockfile.update_entry(entry, kind=kind)
359+
continue
266360
if handle.is_local:
267361
lockfile.update_entry(
268362
LockedEntry(path=ref, installed_name=handle.name),
@@ -305,7 +399,11 @@ def run_add(
305399
# Track results for summary
306400
results: list[CommandResult] = []
307401
# Track install results for lockfile: (handle, ref, install_result, dep_type)
308-
lockfile_updates: list[tuple[ParsedHandle, str, InstallResult, str]] = []
402+
lockfile_updates: list[
403+
tuple[
404+
ParsedHandle, str, InstallResult, str, list[tuple[str, LockedEntry]] | None
405+
]
406+
] = []
309407

310408
for ref in refs:
311409
try:
@@ -325,7 +423,7 @@ def run_add(
325423
dep_type = DEPENDENCY_TYPE_SKILL # default, may change below
326424

327425
# Install
328-
installed_paths, install_result, dep_type = _install_dependency(
426+
install = _install_dependency(
329427
handle,
330428
dep_type,
331429
repo_root,
@@ -337,6 +435,9 @@ def run_add(
337435
config.default_repo,
338436
config=config,
339437
)
438+
installed_paths = install.installed_paths
439+
install_result = install.install_result
440+
dep_type = install.dep_type
340441

341442
# Add to config
342443
if handle.is_local:
@@ -356,7 +457,9 @@ def run_add(
356457

357458
# Track for lockfile update
358459
lockfile_ref = path_value if handle.is_local else ref
359-
lockfile_updates.append((handle, lockfile_ref, install_result, dep_type))
460+
lockfile_updates.append(
461+
(handle, lockfile_ref, install_result, dep_type, install.lock_entries)
462+
)
360463
results.append(CommandResult(ref, True, ", ".join(installed_paths)))
361464

362465
except SkillNotFoundError as e:

0 commit comments

Comments
 (0)