Skip to content

Commit 0311158

Browse files
khaliqgantRicky Schema Cascadeclaudecubic-dev-ai[bot]kjgbot
authored
feat(cli): persistent skill cache to skip prpm install on repeat launches (#124)
* feat(cli): persistent skill cache to skip prpm install on repeat launches Every `agentworkforce agent <persona>` previously spawned a fresh `npx prpm install …` per declared skill, even when the skill set hadn't changed — `npx` resolution + registry lookups + tarball fetches added several seconds of latency to every launch. Introduce a content-addressed cache under `~/.agentworkforce/workforce/cache/plugins/<fingerprint>/` keyed by a SHA-256 of `(harness, sorted skill sources, local file contents)`. On a hit, the install subprocess is skipped entirely: - claude reuses the cache dir directly as `--plugin-dir <cacheDir>`. - opencode / codex mirror the cache contents into the relayfile mount before launch (mount-ignored patterns keep this from syncing back to the user's repo). Invalidation is conservative: changing a skill source string (or editing a local `.md` skill) rotates the fingerprint and produces a fresh cache entry. To force-refresh past unchanged remote sources, pass `--refresh-skills`; to bypass the cache entirely, pass `--no-skill-cache` or set `AGENTWORKFORCE_NO_SKILL_CACHE=1`. `--install-in-repo` disengages caching — the user explicitly opted into having installs land in their working tree, so mirroring a cache back into the repo would silently re-introduce the leakage that flag is designed to surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): avoid duplicate 'Setting up sandbox mount' line on mount cache hit The pure cache-hit path is a near-instant cpSync that doesn't print its own spinner — but the surrounding setupSpinner.stop()/start() was redrawing the "Setting up sandbox mount…" line a second time. Only stop/start the setup spinner around branches that actually run an install spinner of their own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): opt-in upstream drift detection for the skill cache The source-key fingerprint never rotates on an upstream publish when the persona's `source` string is unchanged (floating refs like `@org/skill` or `github.com/org/repo#x`). This adds a cheap, opt-in second check so a new upstream version is actually consumed without a manual refresh. Marker schema bumped to v2 (v1 stays read-compatible, upgraded in place with no upstream records). At install time each remote skill's upstream identity is recorded from the installer lockfiles: - prpm → resolved `version` from prpm.lock - skill.sh → GitHub blob SHA of the installed SKILL.md, located via skills-lock.json `skillPath` On launch, if the marker's `lastUpstreamCheckAt` is older than the interval (default 24h), parallel lightweight probes compare recorded vs. current — prpm registry GET, GitHub Contents API with `If-None-Match` (304, no body, when unchanged). Any drift downgrades the cache hit to a miss so the reinstall picks up the new content; the marker then self-heals. Every probe fails OPEN: a network error, timeout, 4xx/5xx, or malformed body counts as "no drift" so a flaky registry never blocks or slows a launch beyond the timeout. Control surface: - `--check-upstream` force a probe this launch (ignore the TTL) - `--no-check-upstream` skip the probe this launch - `AGENTWORKFORCE_SKILL_CACHE_CHECK_INTERVAL` (24h default; `0`=always, `never`/`off`=disable; accepts ms/s/m/h/d, bare number = hours) Tests: 22 new persona-kit unit tests (mocked HTTP — prpm/github success, 304, 404/5xx fail-open, timeout, mixed sets, v1→v2 migration, marker round-trip, TTL math) + 1 CLI flag-parse test. Verified end-to-end against live prpm.dev and api.github.com: install records the resolved version, in-TTL launches skip probing, a tampered stale version triggers a live-detected reinstall (1.0.0 → 1.1.3), the marker self-heals, and --no-check-upstream bypasses. A decision trajectory for this work is recorded under .trajectories/completed/2026-05/traj_47ulsb0rwbid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): address PR #124 review — refresh-clean, install lock, never-interval Four reviewer findings (CodeRabbit / Devin / cubic): 1. `AGENTWORKFORCE_SKILL_CACHE_CHECK_INTERVAL=never` was ignored: `parseCheckInterval` returns `null` for never/off/false, but the `?? DEFAULT` coalesced `null` back to the 24h default, silently re-enabling drift checks the user disabled. New `resolveEnvCheckIntervalMs()` only falls back on `undefined`. 2. `--refresh-skills` now wipes the cache dir before reinstalling, so a refresh is a true rebuild — stale files from a prior skill version no longer linger, and a partial-failure reinstall can't leave the old marker behind to fake a later cache hit. Applied to both the claude pre-mount path and the mount-deferred path. 3. Added a per-fingerprint advisory install lock (`<cacheDir>.lock`, sibling so a refresh-wipe can't delete a live lock). Two concurrent launches of the same persona no longer both miss and install into the same dir. Stale/dead-pid locks are stolen; if the wait budget is exceeded we proceed unlocked (never hang a launch). After acquiring, validity is re-checked so a peer's just-finished install is reused instead of redundantly reinstalled. 4. Test isolation: the parseAgentArgs flag test now saves/clears/ restores AGENTWORKFORCE_NO_SKILL_CACHE so a machine with that env set can't flake the default-false assertions. Tests: +5 (resolveEnvCheckIntervalMs never/default/0; lock acquire- block-release, stale-steal, dead-pid-steal). Full repo 595/595 green. E2E re-verified: a planted stale file is gone after --refresh-skills and no lock file is left behind. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * stale lock check fix Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fix(cli): heartbeat skill cache locks --------- Co-authored-by: Ricky Schema Cascade <ricky@agent-relay.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: kjgbot <kjgbot@agentrelay.dev>
1 parent 2273032 commit 0311158

10 files changed

Lines changed: 2442 additions & 15 deletions

File tree

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
{
2+
"id": "traj_47ulsb0rwbid",
3+
"version": 1,
4+
"task": {
5+
"title": "Persistent skill cache + upstream drift detection"
6+
},
7+
"status": "completed",
8+
"startedAt": "2026-05-15T20:47:56.786Z",
9+
"agents": [
10+
{
11+
"name": "claude-skill-cache",
12+
"role": "lead",
13+
"joinedAt": "2026-05-15T20:47:56.808Z"
14+
}
15+
],
16+
"chapters": [
17+
{
18+
"id": "chap_p2cn73gpqanw",
19+
"title": "Initial work",
20+
"agentName": "claude-skill-cache",
21+
"startedAt": "2026-05-15T20:47:56.808Z",
22+
"events": [
23+
{
24+
"ts": 1778878092151,
25+
"type": "decision",
26+
"content": "Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/: Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/",
27+
"raw": {
28+
"question": "Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/",
29+
"chosen": "Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/",
30+
"alternatives": [
31+
{
32+
"option": "Per-session install (status quo",
33+
"reason": ""
34+
},
35+
{
36+
"option": "slow); global shared plugin dir (skill collisions across personas); TTL-only cache (still pays install on expiry)",
37+
"reason": ""
38+
}
39+
],
40+
"reasoning": "The reported slowness was npx prpm install / npx skills add re-running every launch. A persistent dir keyed by a stable fingerprint lets repeat launches skip the install entirely. Local .md sources fold their content hash in so edits auto-invalidate without a version bump."
41+
},
42+
"significance": "high"
43+
},
44+
{
45+
"ts": 1778878092386,
46+
"type": "decision",
47+
"content": "Never auto-invalidate on the source-key fingerprint; cover all three harnesses: Never auto-invalidate on the source-key fingerprint; cover all three harnesses",
48+
"raw": {
49+
"question": "Never auto-invalidate on the source-key fingerprint; cover all three harnesses",
50+
"chosen": "Never auto-invalidate on the source-key fingerprint; cover all three harnesses",
51+
"alternatives": [
52+
{
53+
"option": "Daily TTL on the fingerprint; claude-only scope with mount harnesses as follow-up",
54+
"reason": ""
55+
}
56+
],
57+
"reasoning": "User explicitly chose 'never auto-invalidate' for the fingerprint layer and 'all harnesses now' when asked. Claude reuses the cache dir as --plugin-dir; opencode/codex mirror it into the relayfile mount before launch (mount-ignored patterns stop syncback)."
58+
},
59+
"significance": "high"
60+
},
61+
{
62+
"ts": 1778878111454,
63+
"type": "decision",
64+
"content": "Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open: Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open",
65+
"raw": {
66+
"question": "Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open",
67+
"chosen": "Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open",
68+
"alternatives": [
69+
{
70+
"option": "Manual --refresh-skills only (user must remember); always-check (slows every launch); coarse repo-HEAD commit SHA for github (over-invalidates monorepos)",
71+
"reason": ""
72+
}
73+
],
74+
"reasoning": "User asked how a new upstream skill version is consumed when the source string is unchanged. Explored prpm info / registry HTTP API (latest_version.version) and skill.sh — both expose cheap version probes. A 24h TTL keeps most launches network-free; only the daily check launch pays ~150-500ms parallel probes. Fail-open so a flaky registry never blocks a launch."
75+
},
76+
"significance": "high"
77+
},
78+
{
79+
"ts": 1778878111681,
80+
"type": "decision",
81+
"content": "Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match: Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match",
82+
"raw": {
83+
"question": "Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match",
84+
"chosen": "Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match",
85+
"alternatives": [
86+
{
87+
"option": "repos/<o>/<r>/commits?per_page=1 repo-HEAD (1 call",
88+
"reason": ""
89+
},
90+
{
91+
"option": "but any push invalidates); re-download SKILL.md and hash (heavier",
92+
"reason": ""
93+
},
94+
{
95+
"option": "needs path anyway)",
96+
"reason": ""
97+
}
98+
],
99+
"reasoning": "skill.sh writes skills-lock.json with skillPath + computedHash per skill. Building the Contents API URL from skillPath gives per-file drift (a monorepo of 50 skills doesn't invalidate on an unrelated commit). The blob SHA is also the ETag, so If-None-Match returns 304 with no body — cheapest possible check."
100+
},
101+
"significance": "high"
102+
},
103+
{
104+
"ts": 1778878111884,
105+
"type": "decision",
106+
"content": "Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version: Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version",
107+
"raw": {
108+
"question": "Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version",
109+
"chosen": "Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version",
110+
"alternatives": [
111+
{
112+
"option": "Hard v2 cutover (invalidates all caches); separate sidecar file for upstream metadata (more files to keep consistent)",
113+
"reason": ""
114+
}
115+
],
116+
"reasoning": "Bumping the marker schema must not invalidate every cache entry in the wild. readSkillCacheMarker accepts v1+v2 and upgrades v1 in place with no upstream records (next drift pass captures identity). The fingerprint's internal 'v' stays 1 so existing dirs keep resolving."
117+
},
118+
"significance": "high"
119+
},
120+
{
121+
"ts": 1778878613570,
122+
"type": "reflection",
123+
"content": "Verified end-to-end against live prpm.dev + api.github.com: cache miss records resolved version; in-TTL launches skip probing; --check-upstream detects a tampered stale version (1.0.0→1.1.3) and reinstalls; marker self-heals; --no-check-upstream bypasses. GitHub 304 If-None-Match path confirmed.",
124+
"significance": "high"
125+
}
126+
],
127+
"endedAt": "2026-05-15T20:58:19.304Z"
128+
}
129+
],
130+
"commits": [],
131+
"filesChanged": [],
132+
"projectId": "/Users/khaliqgant/Projects/AgentWorkforce/workforce",
133+
"tags": [],
134+
"_trace": {
135+
"startRef": "09caf5b8db32f9d1c2c71735b7e231a7efc7ff8e",
136+
"endRef": "09caf5b8db32f9d1c2c71735b7e231a7efc7ff8e"
137+
},
138+
"completedAt": "2026-05-15T20:58:19.304Z",
139+
"retrospective": {
140+
"summary": "Extended the persistent skill-cache PR with opt-in upstream drift detection. Marker bumped to schema v2 (v1 read-compatible) recording per-skill upstream identity (prpm resolved version / GitHub blob SHA). TTL-gated (24h default) parallel probes on launch flip a cache hit to a reinstall when upstream moved; fail-open on any probe error. Added --check-upstream/--no-check-upstream + AGENTWORKFORCE_SKILL_CACHE_CHECK_INTERVAL. 22 new unit tests (mocked HTTP) + verified end-to-end against live prpm.dev and api.github.com.",
141+
"approach": "Reused installer lockfiles (prpm.lock version, skills-lock.json skillPath) for precise per-file identity; conditional GET (If-None-Match) for the cheapest GitHub check; mutable cache-hit flag downgraded by an awaited drift probe before the install decision.",
142+
"confidence": 0.86
143+
}
144+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Trajectory: Persistent skill cache + upstream drift detection
2+
3+
> **Status:** ✅ Completed
4+
> **Confidence:** 86%
5+
> **Started:** May 15, 2026 at 10:47 PM
6+
> **Completed:** May 15, 2026 at 10:58 PM
7+
8+
---
9+
10+
## Summary
11+
12+
Extended the persistent skill-cache PR with opt-in upstream drift detection. Marker bumped to schema v2 (v1 read-compatible) recording per-skill upstream identity (prpm resolved version / GitHub blob SHA). TTL-gated (24h default) parallel probes on launch flip a cache hit to a reinstall when upstream moved; fail-open on any probe error. Added --check-upstream/--no-check-upstream + AGENTWORKFORCE_SKILL_CACHE_CHECK_INTERVAL. 22 new unit tests (mocked HTTP) + verified end-to-end against live prpm.dev and api.github.com.
13+
14+
**Approach:** Reused installer lockfiles (prpm.lock version, skills-lock.json skillPath) for precise per-file identity; conditional GET (If-None-Match) for the cheapest GitHub check; mutable cache-hit flag downgraded by an awaited drift probe before the install decision.
15+
16+
---
17+
18+
## Key Decisions
19+
20+
### Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/
21+
- **Chose:** Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/
22+
- **Rejected:** Per-session install (status quo, slow); global shared plugin dir (skill collisions across personas); TTL-only cache (still pays install on expiry)
23+
- **Reasoning:** The reported slowness was npx prpm install / npx skills add re-running every launch. A persistent dir keyed by a stable fingerprint lets repeat launches skip the install entirely. Local .md sources fold their content hash in so edits auto-invalidate without a version bump.
24+
25+
### Never auto-invalidate on the source-key fingerprint; cover all three harnesses
26+
- **Chose:** Never auto-invalidate on the source-key fingerprint; cover all three harnesses
27+
- **Rejected:** Daily TTL on the fingerprint; claude-only scope with mount harnesses as follow-up
28+
- **Reasoning:** User explicitly chose 'never auto-invalidate' for the fingerprint layer and 'all harnesses now' when asked. Claude reuses the cache dir as --plugin-dir; opencode/codex mirror it into the relayfile mount before launch (mount-ignored patterns stop syncback).
29+
30+
### Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open
31+
- **Chose:** Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open
32+
- **Rejected:** Manual --refresh-skills only (user must remember); always-check (slows every launch); coarse repo-HEAD commit SHA for github (over-invalidates monorepos)
33+
- **Reasoning:** User asked how a new upstream skill version is consumed when the source string is unchanged. Explored prpm info / registry HTTP API (latest_version.version) and skill.sh — both expose cheap version probes. A 24h TTL keeps most launches network-free; only the daily check launch pays ~150-500ms parallel probes. Fail-open so a flaky registry never blocks a launch.
34+
35+
### Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match
36+
- **Chose:** Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match
37+
- **Rejected:** repos/<o>/<r>/commits?per_page=1 repo-HEAD (1 call, but any push invalidates); re-download SKILL.md and hash (heavier, needs path anyway)
38+
- **Reasoning:** skill.sh writes skills-lock.json with skillPath + computedHash per skill. Building the Contents API URL from skillPath gives per-file drift (a monorepo of 50 skills doesn't invalidate on an unrelated commit). The blob SHA is also the ETag, so If-None-Match returns 304 with no body — cheapest possible check.
39+
40+
### Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version
41+
- **Chose:** Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version
42+
- **Rejected:** Hard v2 cutover (invalidates all caches); separate sidecar file for upstream metadata (more files to keep consistent)
43+
- **Reasoning:** Bumping the marker schema must not invalidate every cache entry in the wild. readSkillCacheMarker accepts v1+v2 and upgrades v1 in place with no upstream records (next drift pass captures identity). The fingerprint's internal 'v' stays 1 so existing dirs keep resolving.
44+
45+
---
46+
47+
## Chapters
48+
49+
### 1. Initial work
50+
*Agent: claude-skill-cache*
51+
52+
- Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/: Content-addressed cache keyed by (harness, sorted skill sources, local-file SHA) under ~/.agentworkforce/workforce/cache/plugins/<fp>/
53+
- Never auto-invalidate on the source-key fingerprint; cover all three harnesses: Never auto-invalidate on the source-key fingerprint; cover all three harnesses
54+
- Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open: Add opt-in upstream drift detection: prpm registry GET + GitHub Contents API blob SHA, TTL-gated (24h default), fail-open
55+
- Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match: Precise per-file GitHub blob SHA via skills-lock.json skillPath, not coarse repo-HEAD; conditional GET with If-None-Match
56+
- Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version: Marker schema v2, v1 read-compatible; fingerprint content-version pinned to 1 independent of marker version
57+
- Verified end-to-end against live prpm.dev + api.github.com: cache miss records resolved version; in-TTL launches skip probing; --check-upstream detects a tampered stale version (1.0.0→1.1.3) and reinstalls; marker self-heals; --no-check-upstream bypasses. GitHub 304 If-None-Match path confirmed.

.trajectories/index.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"version": 1,
3-
"lastUpdated": "2026-05-01T19:08:35.768Z",
3+
"lastUpdated": "2026-05-15T20:58:19.420Z",
44
"trajectories": {
55
"traj_1775734701264_ba65c69b": {
66
"title": "finish-npm-provenance-persona-workflow",
@@ -29,6 +29,13 @@
2929
"startedAt": "2026-05-01T19:06:48.954Z",
3030
"completedAt": "2026-05-01T19:08:35.648Z",
3131
"path": "/Users/khaliqgant/Projects/AgentWorkforce/workforce/.trajectories/completed/2026-05/traj_cntjweljhmft.json"
32+
},
33+
"traj_47ulsb0rwbid": {
34+
"title": "Persistent skill cache + upstream drift detection",
35+
"status": "completed",
36+
"startedAt": "2026-05-15T20:47:56.786Z",
37+
"completedAt": "2026-05-15T20:58:19.304Z",
38+
"path": "/Users/khaliqgant/Projects/AgentWorkforce/workforce/.trajectories/completed/2026-05/traj_47ulsb0rwbid.json"
3239
}
3340
}
3441
}

0 commit comments

Comments
 (0)