441 lines
22 KiB
Markdown
441 lines
22 KiB
Markdown
# Phase 45: Content as Skills - Research
|
|
|
|
**Researched:** 2026-04-04
|
|
**Domain:** Nexus skill registry — local SKILL.md authoring, source seeding, group membership, generalist agent preloading
|
|
**Confidence:** HIGH
|
|
|
|
## Summary
|
|
|
|
Phase 45 bridges the content generation work of Phases 41-44 with the skill registry system already in place. Every content type (diagram, icon, theme, wallpaper, social, convert, pdf-document, brand-kit, presentation) must become an installable Markdown skill, the built-in "Creative" group must contain all of them, and the Generalist agent must ship pre-loaded with that group.
|
|
|
|
The skill registry infrastructure is complete. The registry stores skills in a libSQL database under `~/.paperclip/instances/default/skills/registry.db`. Skills are fetched from remote GitHub sources (`anthropic-marketplace` and `github-tree` types) or from native agent runtimes (Hermes). There is currently no local-filesystem source type. Phase 45 must add one, seed it on server startup, and wire the Creative group to the new skills.
|
|
|
|
The generalist agent preloading mechanism already exists: `index.ts` seeds `pendingSkillGroups: ["Creative"]` in agent metadata at creation time, and a fire-and-forget startup reconciler assigns the group (using `assignGroup`) to every agent that has that metadata flag. Phase 45 needs the Creative group to have members before the reconciler runs — which means the skill SKILL.md files and their registry entries must be in place before any agent is created.
|
|
|
|
**Primary recommendation:** Add a `"local-nexus-content"` source type to the skill registry fetcher. On server startup, register nine content SKILL.md files from `server/src/skills/content/` into the registry DB and add all of them to the `builtin/creative` group.
|
|
|
|
---
|
|
|
|
<user_constraints>
|
|
## User Constraints (from CONTEXT.md)
|
|
|
|
### Locked Decisions
|
|
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting.
|
|
|
|
### Claude's Discretion
|
|
All implementation choices are at Claude's discretion. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
|
|
|
|
### Deferred Ideas (OUT OF SCOPE)
|
|
None.
|
|
</user_constraints>
|
|
|
|
---
|
|
|
|
<phase_requirements>
|
|
## Phase Requirements
|
|
|
|
| ID | Description | Research Support |
|
|
|----|-------------|------------------|
|
|
| SKILL-01 | Each content type is implemented as an installable Nexus skill | Nine SKILL.md files authored; registered in skill registry DB via local-nexus-content source type; installable through existing `svc.install()` path |
|
|
| SKILL-02 | Generalist agent is pre-loaded with a "Creative" skill group | `builtin/creative` group already exists in DB seed; group members populated on startup; `pendingSkillGroups` reconciler already assigns the group to Generalist agents |
|
|
| SKILL-03 | Users can add or remove content type skills through the Skill Aggregator | Content skills appear in the existing SkillBrowser UI once registered; install/uninstall routes already work for any registered skill |
|
|
</phase_requirements>
|
|
|
|
---
|
|
|
|
## Standard Stack
|
|
|
|
### Core
|
|
|
|
| Library | Version | Purpose | Why Standard |
|
|
|---------|---------|---------|--------------|
|
|
| libSQL / drizzle-orm | Already installed | Skill registry persistence | All skill data lives in this SQLite DB — no new dep needed |
|
|
| node:fs/promises | Node built-in | Read SKILL.md files from disk | Used by existing skill cache logic |
|
|
| vitest | Already installed | Unit tests | Project standard in `server/` |
|
|
|
|
### Supporting
|
|
|
|
No new packages required. The full content generation stack (jobType dispatch, renderers, SSE) is already in place from Phases 40-44.
|
|
|
|
**Installation:** None required.
|
|
|
|
---
|
|
|
|
## Architecture Patterns
|
|
|
|
### Recommended Project Structure
|
|
|
|
```
|
|
server/src/
|
|
├── skills/
|
|
│ └── content/
|
|
│ ├── diagram.SKILL.md
|
|
│ ├── icon-set.SKILL.md
|
|
│ ├── theme-palette.SKILL.md
|
|
│ ├── wallpaper.SKILL.md
|
|
│ ├── social-post.SKILL.md
|
|
│ ├── convert.SKILL.md
|
|
│ ├── pdf-document.SKILL.md
|
|
│ ├── brand-kit.SKILL.md
|
|
│ └── presentation.SKILL.md
|
|
├── services/
|
|
│ └── skill-registry-fetcher.ts ← add local-nexus-content source type
|
|
│ └── skill-registry-db.ts ← seed Creative group members on init
|
|
└── index.ts ← call seedContentSkills on startup
|
|
```
|
|
|
|
### Pattern 1: Local-Filesystem Source Type
|
|
|
|
**What:** A new `"local-nexus-content"` entry in `SkillSourceConfig` union and a `fetchLocalNexusContent()` function that reads SKILL.md files from `server/src/skills/content/` and upserts them into the registry DB.
|
|
|
|
**When to use:** Server startup — called once after `getSkillRegistryDb()` initialises.
|
|
|
|
**Example:**
|
|
|
|
```typescript
|
|
// In skill-registry-fetcher.ts — extend the union type
|
|
export type SkillSourceConfig =
|
|
| { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string }
|
|
| { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string }
|
|
| { id: string; type: "local-nexus-content"; dir: string; label: string };
|
|
|
|
// New handler called by fetchAllSources when type === "local-nexus-content"
|
|
async function fetchLocalNexusContent(
|
|
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
|
|
db: SkillRegistryDb,
|
|
): Promise<number> {
|
|
const entries = await readdir(source.dir, { withFileTypes: true });
|
|
let fetched = 0;
|
|
for (const entry of entries) {
|
|
if (!entry.name.endsWith(".SKILL.md")) continue;
|
|
const slug = entry.name.replace(/\.SKILL\.md$/, "");
|
|
const skillId = `${source.id}/${slug}`;
|
|
const skillMdContent = await readFile(path.join(source.dir, entry.name), "utf-8");
|
|
const { name, description } = parseSkillFrontmatter(skillMdContent);
|
|
// Use a content hash as the version SHA (deterministic, no network needed)
|
|
const sha = crypto.createHash("sha1").update(skillMdContent).digest("hex");
|
|
await upsertSkill(db, { skillId, sourceId: source.id, name: name ?? slug, description, sourceUrl: "" });
|
|
await cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl: "" });
|
|
await upsertCommunityRatingsStub(db, skillId, source.id);
|
|
fetched++;
|
|
}
|
|
return fetched;
|
|
}
|
|
```
|
|
|
|
**BUILT_IN_SOURCES addition:**
|
|
|
|
```typescript
|
|
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
|
|
// … existing remote sources …
|
|
{
|
|
id: "nexus-content",
|
|
type: "local-nexus-content",
|
|
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
|
|
label: "Nexus Content Tools",
|
|
},
|
|
];
|
|
```
|
|
|
|
### Pattern 2: Creative Group Membership Seeding
|
|
|
|
**What:** After the local skills are registered, add them as members of the `builtin/creative` group. This should happen inside `getSkillRegistryDb()` (or a startup helper called right after) so the group has members before the `pendingSkillGroups` reconciler runs.
|
|
|
|
**When to use:** Server startup, idempotent (uses `INSERT OR IGNORE`).
|
|
|
|
```typescript
|
|
// In skill-registry-db.ts — extend seedBuiltinGroups or add seedCreativeGroupMembers
|
|
const NEXUS_CONTENT_SKILL_IDS = [
|
|
"nexus-content/diagram",
|
|
"nexus-content/icon-set",
|
|
"nexus-content/theme-palette",
|
|
"nexus-content/wallpaper",
|
|
"nexus-content/social-post",
|
|
"nexus-content/convert",
|
|
"nexus-content/pdf-document",
|
|
"nexus-content/brand-kit",
|
|
"nexus-content/presentation",
|
|
] as const;
|
|
|
|
async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
|
|
const now = Date.now();
|
|
for (const skillId of NEXUS_CONTENT_SKILL_IDS) {
|
|
await client.execute({
|
|
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
|
args: ["builtin/creative", skillId, now],
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern 3: SKILL.md Authoring Convention
|
|
|
|
Each content skill SKILL.md must follow the same frontmatter convention as existing skills:
|
|
|
|
```markdown
|
|
---
|
|
name: diagram
|
|
description: >
|
|
Generate diagrams from a natural language description. Produces Mermaid
|
|
syntax rendered to SVG/PNG. Supports architecture, flowchart, ERD,
|
|
sequence, and mind-map types. Use when a user asks to visualise a
|
|
system, process, or data structure as a diagram.
|
|
---
|
|
|
|
# Diagram Generation
|
|
|
|
Generates diagrams from a text description via the Nexus content API.
|
|
|
|
## Usage
|
|
|
|
Submit a `POST /api/content-jobs` with:
|
|
- `jobType: "diagram"`
|
|
- `input.description` — natural language description of the diagram
|
|
- `input.type` (optional) — `"flowchart"`, `"architecture"`, `"erd"`, `"sequence"`, or `"mindmap"`
|
|
|
|
The job returns a 202 with a `jobId`. Poll `GET /api/content-jobs/:jobId` or subscribe
|
|
to SSE `content_job.done` events to retrieve the resulting asset URL.
|
|
|
|
## Output
|
|
|
|
Returns an SVG file (also available as PNG via the export button in the UI).
|
|
```
|
|
|
|
### Startup Wiring
|
|
|
|
In `server/src/index.ts`, the existing startup block already calls `getSkillRegistryDb()` as fire-and-forget. Extend that block to also call `fetchAllSources()` with `BUILT_IN_SOURCES` immediately after DB init so content skills are seeded on every cold start:
|
|
|
|
```typescript
|
|
// [nexus] Initialize skill registry and seed content skills
|
|
void (async () => {
|
|
try {
|
|
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
|
|
await getSkillRegistryDb(); // creates tables + builtin groups
|
|
const { skillRegistryService } = await import("./services/skill-registry.js");
|
|
await skillRegistryService().fetchAll(); // seeds local-nexus-content skills
|
|
logger.info("skill registry database initialized and content skills seeded");
|
|
} catch (err) {
|
|
logger.error({ err }, "skill registry init failed");
|
|
}
|
|
})();
|
|
```
|
|
|
|
### Anti-Patterns to Avoid
|
|
|
|
- **Seeding Creative group members before the skills table rows exist:** `skill_group_members.skill_id` has no FK constraint, so INSERT succeeds even if the skill row is absent — but `resolveEffectiveSkills` then returns orphaned IDs. Always seed skill rows first, group members second.
|
|
- **Calling `fetchAll()` before `getSkillRegistryDb()`:** DB tables must exist before the local fetcher tries to upsert.
|
|
- **Using mutable remote SHA for local file versioning:** Local files don't have a git SHA. Use a SHA-1 of the file content instead — deterministic and cheap.
|
|
- **Blocking HTTP on `fetchAll()` at startup:** Keep as fire-and-forget; the external github-tree fetches can be slow. Content skills (local path) are synchronous but the remote sources may timeout.
|
|
|
|
---
|
|
|
|
## Don't Hand-Roll
|
|
|
|
| Problem | Don't Build | Use Instead | Why |
|
|
|---------|-------------|-------------|-----|
|
|
| Skill install/uninstall | Custom file copy logic | `skillRegistryService().install()` / `uninstall()` | Already handles version tracking, cache dirs, agentSkills table |
|
|
| Group assignment | Direct INSERT into agentSkillGroups | `skillGroupService().assignGroup()` | Handles effective skill resolution, per-skill install, dedup |
|
|
| Skill Browser display | New UI component | Existing SkillBrowser page at `/skills` | Already renders all registered skills with install/remove actions |
|
|
| Group membership seeding | New service | `INSERT OR IGNORE INTO skill_group_members` on DB init | Simpler than adding a new route; idempotent |
|
|
|
|
---
|
|
|
|
## Common Pitfalls
|
|
|
|
### Pitfall 1: Creative group populated but skills not yet in registry
|
|
**What goes wrong:** `pendingSkillGroups` reconciler runs at startup and calls `assignGroup("builtin/creative", agentId, skillsDir)`. `resolveEffectiveSkills` returns the 9 skill IDs, but `svc.install()` fails with "Skill not found" because local skills haven't been fetched yet.
|
|
**Why it happens:** The skill-registry init and the `pendingSkillGroups` reconciler are both fire-and-forget; their order is non-deterministic.
|
|
**How to avoid:** Seed creative group members only after `fetchAll()` completes (not at DB init time). Alternatively, sequence the two startup blocks: skill init → fetchAll → then let the reconciler run.
|
|
**Warning signs:** `skipped` array non-empty in `assignGroup` result; Generalist agent has group assigned but no SKILL.md files on disk.
|
|
|
|
### Pitfall 2: Local source dir resolution broken in production
|
|
**What goes wrong:** `path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content")` resolves to `server/src/skills/content` in dev but points to a non-existent path in a compiled/packaged build.
|
|
**Why it happens:** `import.meta.url` refers to the compiled output location.
|
|
**How to avoid:** In `tsconfig.json`, include `server/src/skills/content/*.SKILL.md` in the assets copied to dist, OR read the source dir via `process.cwd()` with a known repo-relative path. Check that the resolved dir exists before iterating; log a clear error if absent.
|
|
|
|
### Pitfall 3: SKILL.md frontmatter parse returns undefined name
|
|
**What goes wrong:** `parseSkillFrontmatter` fails silently on a multi-line YAML description block (`description: >\n line1\n line2`). The `name` field parses correctly but `description` is truncated to `>` literal.
|
|
**Why it happens:** `parseSkillFrontmatter` uses a single-line regex (`^description:\s*(.+)$`) — it does not handle YAML block scalars.
|
|
**How to avoid:** For the `name` field (used for registry display), keep it on a single line. For `description` use a single-line string or expand the frontmatter parser to handle folded scalars. Alternatively, accept the `>` literal as description — it won't break anything.
|
|
|
|
### Pitfall 4: Agent SKILL.md dir wrong for Generalist agent
|
|
**What goes wrong:** `assignGroup` copies skills to `agentSkillsDir` but the Generalist agent's adapter is `claude_local` — the skills dir is resolved by `resolveAdapterSkillConfig("claude_local").skillDir`. If the agent's workspace does not exist yet (new onboard), `cp` fails.
|
|
**Why it happens:** The workspace dir is created lazily when the agent first runs.
|
|
**How to avoid:** In `assignGroup`, `mkdir(agentSkillsDir, { recursive: true })` before attempting the copy. Inspect the existing code path — if this `mkdir` is already there, no action needed.
|
|
|
|
### Pitfall 5: Duplicate registration on restarts
|
|
**What goes wrong:** Every server restart calls `fetchAll()`, which re-registers all 9 local skills. Without idempotency guards, this creates duplicate `skill_versions` and `skill_files` rows.
|
|
**Why it happens:** The content hash SHA is deterministic, so `versionExists()` returns true and `cacheSkillVersion` is skipped. But `upsertSkill` uses `onConflictDoUpdate` — the name/description are updated but no duplicate is created.
|
|
**How to avoid:** The existing `versionExists()` check handles this. Confirm the content-hash versionId format matches: `${skillId}@${sha}`.
|
|
|
|
---
|
|
|
|
## Code Examples
|
|
|
|
### Registering skills from disk (local-nexus-content handler)
|
|
|
|
```typescript
|
|
// Source: server/src/services/skill-registry-fetcher.ts (new function)
|
|
import { readdir, readFile } from "node:fs/promises";
|
|
import crypto from "node:crypto";
|
|
|
|
async function fetchLocalNexusContent(
|
|
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
|
|
db: SkillRegistryDb,
|
|
): Promise<number> {
|
|
let entries: Awaited<ReturnType<typeof readdir>>;
|
|
try {
|
|
entries = await readdir(source.dir, { withFileTypes: true });
|
|
} catch {
|
|
// Directory doesn't exist (e.g. missing after a build step) — log and skip
|
|
return 0;
|
|
}
|
|
let fetched = 0;
|
|
for (const entry of entries) {
|
|
if (!entry.isFile() || !entry.name.endsWith(".SKILL.md")) continue;
|
|
const slug = entry.name.replace(/\.SKILL\.md$/, "");
|
|
const skillId = `${source.id}/${slug}`;
|
|
const content = await readFile(path.join(source.dir, entry.name), "utf-8");
|
|
const sha = crypto.createHash("sha1").update(content).digest("hex");
|
|
const { name, description } = parseSkillFrontmatter(content);
|
|
await upsertSkill(db, {
|
|
skillId, sourceId: source.id,
|
|
name: name ?? slug, description,
|
|
sourceUrl: "",
|
|
});
|
|
await cacheSkillVersion(db, { skillId, sha, skillMdContent: content, skillMdUrl: "" });
|
|
await upsertCommunityRatingsStub(db, skillId, source.id);
|
|
fetched++;
|
|
}
|
|
return fetched;
|
|
}
|
|
```
|
|
|
|
### Seeding Creative group membership after fetchAll
|
|
|
|
```typescript
|
|
// In skill-registry-db.ts — called after seedBuiltinGroups
|
|
async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
|
|
const now = Date.now();
|
|
const SKILLS = [
|
|
"nexus-content/diagram", "nexus-content/icon-set", "nexus-content/theme-palette",
|
|
"nexus-content/wallpaper", "nexus-content/social-post", "nexus-content/convert",
|
|
"nexus-content/pdf-document", "nexus-content/brand-kit", "nexus-content/presentation",
|
|
];
|
|
for (const skillId of SKILLS) {
|
|
await client.execute({
|
|
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
|
|
args: ["builtin/creative", skillId, now],
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### Ensuring Creative is assigned to a new Generalist agent
|
|
|
|
```typescript
|
|
// The existing path in index.ts (already present, no change needed):
|
|
await agentSvc.create(company.id, {
|
|
name: "Generalist",
|
|
role: "general",
|
|
adapterType: "claude_local",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
metadata: { pendingSkillGroups: ["Creative"], backfilled: true },
|
|
});
|
|
// The reconciler at server startup calls svc.assignGroup("builtin/creative", agentId, skillsDir)
|
|
// which installs all Creative group members to the agent's .claude/skills/ directory.
|
|
```
|
|
|
|
---
|
|
|
|
## State of the Art
|
|
|
|
| Old Approach | Current Approach | When Changed | Impact |
|
|
|--------------|------------------|--------------|--------|
|
|
| Skills only from remote GitHub repos | Skills can come from local filesystem too (new local-nexus-content type) | Phase 45 | Nexus-specific content skills don't need a public GitHub repo |
|
|
| Creative group is empty (seed only creates the group row) | Creative group has 9 content skill members after server init | Phase 45 | Generalist agents automatically get all content tools on first run |
|
|
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
1. **Should `cacheSkillVersion` write files to a tmpdir for local skills?**
|
|
- What we know: `cacheSkillVersion` writes `SKILL.md` to `~/.paperclip/instances/default/skills/cache/nexus-content/<slug>/<sha>/SKILL.md`, then `svc.install()` copies from that cache dir to the agent's skills dir.
|
|
- What's unclear: For local skills, caching to disk before installing is redundant — the source file is already on disk.
|
|
- Recommendation: Keep the cache write for consistency (install always reads from cache); the file is tiny. If performance matters, skip caching and have install copy the source file directly.
|
|
|
|
2. **Where exactly is the `server/src/skills/content/` dir resolved in compiled builds?**
|
|
- What we know: The server compiles TypeScript but does not currently bundle assets — source files are referenced at runtime via `import.meta.url`.
|
|
- What's unclear: Whether `.SKILL.md` files are included in the dist output.
|
|
- Recommendation: Add `server/src/skills/content/` to the list of directories copied during build (check the server's build script), or use `process.env.SKILL_CONTENT_DIR` as an override for production.
|
|
|
|
3. **Should the Skill Aggregator UI show a "Creative" filter tab?**
|
|
- What we know: `SkillBrowser.tsx` filters by adapter compatibility; the "Groups" tab shows group membership via `skillGroupsApi`. Content skills installed via the registry appear in the installed tab.
|
|
- What's unclear: Whether the browse experience needs a dedicated "Creative" section or if the existing group-based filtering is sufficient.
|
|
- Recommendation: No UI changes needed for SKILL-03. The existing SkillBrowser already supports install/remove for any registered skill. Adding a dedicated Creative tab is a future enhancement.
|
|
|
|
---
|
|
|
|
## Environment Availability
|
|
|
|
Step 2.6: SKIPPED (no new external dependencies — local filesystem only)
|
|
|
|
---
|
|
|
|
## Validation Architecture
|
|
|
|
### Test Framework
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| Framework | Vitest (node environment) |
|
|
| Config file | `server/vitest.config.ts` |
|
|
| Quick run command | `cd /opt/nexus/server && npx vitest run --reporter=verbose src/__tests__/skill-registry-content-skills.test.ts` |
|
|
| Full suite command | `cd /opt/nexus/server && npx vitest run --reporter=verbose` |
|
|
|
|
### Phase Requirements → Test Map
|
|
|
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
|
|--------|----------|-----------|-------------------|-------------|
|
|
| SKILL-01 | 9 SKILL.md files are seeded into registry DB via local-nexus-content source | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "seeds content skills"` | ❌ Wave 0 |
|
|
| SKILL-01 | Each skill is installable to an agent skills dir | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "installs content skill"` | ❌ Wave 0 |
|
|
| SKILL-02 | Creative group has 9 members after DB init | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "Creative group members"` | ❌ Wave 0 |
|
|
| SKILL-03 | Content skills appear in registry list | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "list includes content skills"` | ❌ Wave 0 |
|
|
|
|
### Sampling Rate
|
|
|
|
- **Per task commit:** `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts`
|
|
- **Per wave merge:** `cd /opt/nexus/server && npx vitest run`
|
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
|
|
|
### Wave 0 Gaps
|
|
|
|
- [ ] `server/src/__tests__/skill-registry-content-skills.test.ts` — covers SKILL-01, SKILL-02, SKILL-03
|
|
- [ ] `server/src/skills/content/*.SKILL.md` — 9 skill files (authored, not test infra)
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
### Primary (HIGH confidence)
|
|
|
|
- Direct source code inspection — `server/src/services/skill-registry-fetcher.ts`, `skill-registry-db.ts`, `skill-registry-groups.ts`, `skill-registry.ts`
|
|
- Direct source code inspection — `server/src/index.ts` lines 256-279 (ensureGeneralistAgents), 628-658 (pendingSkillGroups reconciler)
|
|
- Direct source code inspection — `server/src/adapters/registry.ts`, `server/src/routes/skill-registry.ts`, `server/src/routes/skill-registry-groups.ts`
|
|
|
|
### Secondary (MEDIUM confidence)
|
|
|
|
- Existing test patterns from `server/src/__tests__/skill-registry-install.test.ts` and `hermes-dual-source.test.ts` — confirms vitest + real temp dirs as test pattern
|
|
|
|
---
|
|
|
|
## Metadata
|
|
|
|
**Confidence breakdown:**
|
|
- Standard stack: HIGH — all libraries already installed; no new deps
|
|
- Architecture: HIGH — local source type follows identical pattern to existing github-tree handler
|
|
- Pitfalls: HIGH — race conditions and dir resolution issues verified against actual startup code
|
|
|
|
**Research date:** 2026-04-04
|
|
**Valid until:** 2026-05-04 (stable codebase)
|