From shelf
Sync issues, docs, and tech tags from GitHub and the local repo to Obsidian. Creates/updates issue notes with templates and tags, closes archived issues, syncs PRD summaries as doc notes, and refreshes tech stack tags on the dashboard.
How this skill is triggered — by the user, by Claude, or both
Slash command
/shelf:shelf-syncThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Pull open issues from GitHub and `.kiln/issues/` into Obsidian as individual issue notes. Updates existing notes, closes archived ones, syncs PRD summaries as doc notes, refreshes tech stack tags, and skips anything unchanged since the last sync.
Pull open issues from GitHub and .kiln/issues/ into Obsidian as individual issue notes. Updates existing notes, closes archived ones, syncs PRD summaries as doc notes, refreshes tech stack tags, and skips anything unchanged since the last sync.
$ARGUMENTS
No arguments required. This command is fully automatic.
Determine the project slug and base path. Priority order: explicit argument > .shelf-config > git remote defaults.
.shelf-config exists in the repo root:
a. Parse it: skip lines starting with # (comments) and blank lines; split each remaining line on the first = to get key and value; trim whitespace from both
b. Extract base_path and slug values
c. If both are present and non-empty: use them as $BASE_PATH and $SLUG — do NOT derive from git remote or prompt the user (FR-006). Skip to substep 4
d. If either is missing or empty: warn ".shelf-config is malformed — missing {key}. Falling back to defaults." and continue to substep 2.shelf-config:
a. If the user provided a project name as an argument: use it as $SLUG
b. Otherwise: run git remote get-url origin and extract the repo name (last path segment, strip .git suffix) as $SLUG
c. Set $BASE_PATH = "projects" (default){$BASE_PATH}/{$SLUG}/...mcp__obsidian-projects__read_file({ path: "{base_path}/{slug}/{slug}.md" })
/shelf-create first" and STOPRun:
gh issue list --state all --json number,title,state,labels,body,updatedAt --limit 100
gh is not installed or not authenticated: warn "GitHub CLI not authenticated — skipping GitHub issues" and continue with backlog-only syncCheck if .kiln/issues/ directory exists:
ls .kiln/issues/*.md 2>/dev/null
.md file and extract title, type, severity from frontmatterList current issue notes in Obsidian:
mcp__obsidian-projects__list_files({ path: "{base_path}/{slug}/issues" })
For each existing note (excluding .gitkeep):
mcp__obsidian-projects__read_file({ path: "{base_path}/{slug}/issues/{filename}" })
Extract last_synced and source from frontmatter to determine which issues need updating.
If MCP fails: warn and continue with what we have (NFR-004)
For each issue (GitHub + backlog), compare with existing Obsidian notes:
updatedAt > last_synced): updateupdatedAt <= last_synced): skip (FR-017)status: closed (FR-016)Track counters: created, updated, closed, skipped
For each issue that needs a note, generate a human-readable slug from the title:
.mdExample: "Fix sidebar overflow on mobile" -> fix-sidebar-overflow-on-mobile.md
Template resolution (FR-004): Read the issue template. First check if .shelf/templates/issue.md exists in the repo. If it does, use that. Otherwise, use plugin-shelf/templates/issue.md.
Tag derivation (FR-006, FR-008): For each issue, derive tags using the algorithm from plugin-shelf/tags.md:
source/* — GitHub issues -> source/github, backlog -> source/backlogseverity/* — From labels or frontmatter. Default: severity/mediumtype/* — From labels or frontmatter type field. Default: type/improvementcategory/* — Infer from content: mentions skills -> category/skills, agents -> category/agents, hooks -> category/hooks, templates -> category/templates, scaffold -> category/scaffold, else -> category/workflowFor each issue that needs creating or updating, replace placeholders in the template:
For GitHub issues (source: github):
{title} — issue title{status} — open or closed{severity} — derived from labels (bug=high, enhancement=medium, else=medium){source} — GitHub #{number}{github_number} — the issue number{slug} — project slug (for project: "[[{slug}]]" backlink, FR-005){source_tag} — source/github{severity_tag} — derived severity tag{type_tag} — derived type tag{category_tag} — derived category tag{body} — issue body text{sync_footer} — *Synced from GitHub issue #{number}*{last_synced} — ISO 8601 timestampFor backlog issues (source: backlog):
{source} — backlog:{filename}{github_number} — null{source_tag} — source/backlogmcp__obsidian-projects__create_file or update_file({
path: "{base_path}/{slug}/issues/{slug-from-title}.md",
content: "{rendered issue template}"
})
If any individual MCP call fails: warn for that issue, increment an error counter, and continue with the rest (NFR-004)
After issue sync, check for archived backlog items that should be closed in Obsidian.
For each existing Obsidian issue note with source: "backlog:*":
source field (e.g., backlog:my-issue.md -> my-issue.md).kiln/issues/{filename}:
ls .kiln/issues/{filename} 2>/dev/null
.kiln/issues/, check .kiln/issues/completed/:
ls .kiln/issues/completed/{filename} 2>/dev/null
completed/: update the Obsidian note to status: closed via MCPclosed counter for each note marked closed (FR-010)If MCP fails for any note: warn and continue with the rest (NFR-004)
Scan for feature PRDs and create/update doc notes in Obsidian.
Find all PRDs:
ls docs/features/*/PRD.md 2>/dev/null
For each PRD found:
a. Read the PRD file
b. Extract the feature slug from the directory name (e.g., docs/features/2026-04-03-shelf-sync-v2/PRD.md -> shelf-sync-v2)
c. Extract the title from the first # heading
d. Extract 1-2 sentence summary from the ## Problem Statement section (first paragraph). If not found, fall back to ## Background first paragraph
e. Count FR- occurrences for fr_count
f. Count NFR- occurrences for nfr_count
g. Extract Status: field value for doc_status
Template resolution (FR-004): Read the doc template. First check if .shelf/templates/doc.md exists in the repo. If it does, use that. Otherwise, use plugin-shelf/templates/doc.md.
Tag derivation (FR-007, FR-008): Derive 3 tags per the contracts:
doc/* — PRD -> doc/prdstatus/* — Map status: Draft -> status/open, Approved -> status/implemented, else -> status/in-progresscategory/* — Infer from content using same logic as issue tagsSkip unchanged (FR-013): Check if doc note already exists in Obsidian:
mcp__obsidian-projects__read_file({ path: "{base_path}/{slug}/docs/{feature-slug}.md" })
If it exists and the content would be identical, skip it. Track docs_skipped counter.
Create or update the doc note:
mcp__obsidian-projects__create_file or update_file({
path: "{base_path}/{slug}/docs/{feature-slug}.md",
content: "{rendered doc template}"
})
Track counters: docs_created, docs_updated, docs_skipped
If MCP fails for any doc: warn and continue (NFR-004)
Re-detect the tech stack and update dashboard tags if they've changed.
Re-detect tech stack (FR-014): Run the same detection as /shelf-create Step 3:
ls package.json tsconfig.json Cargo.toml pyproject.toml requirements.txt go.mod Gemfile Dockerfile docker-compose.yml 2>/dev/null
If package.json exists, read it to detect framework tags.
Use the canonical lookup table from contracts:
| File | Tags |
|---|---|
package.json | Parse deps for: language/javascript or language/typescript, framework/react, framework/next, framework/vue, framework/express, framework/fastify |
tsconfig.json | language/typescript |
Cargo.toml | language/rust |
pyproject.toml or requirements.txt | language/python |
go.mod | language/go |
Gemfile | language/ruby |
Dockerfile or docker-compose.yml | infra/docker |
.github/workflows/ | infra/github-actions |
Read current dashboard tags (FR-015): Parse the tags: field from the dashboard frontmatter (already read in Step 2).
Compare and update (FR-015): If detected tags differ from current dashboard tags:
detected - current) and removed (current - detected)tags: field with the new detected tags via MCP:
mcp__obsidian-projects__update_file({
path: "{base_path}/{slug}/{slug}.md",
content: "{dashboard with updated tags}"
})
tags_added and tags_removed counters (FR-016)If tags are unchanged: set tags_unchanged = true (FR-016)
If MCP fails: warn "Could not update dashboard tags" and continue (NFR-004)
Print the enhanced sync summary with all counters:
Sync complete for '{slug}'.
Issues: {N} created, {N} updated, {N} closed, {N} skipped
Docs: {N} created, {N} updated, {N} skipped
Tags: {+N added, -N removed | unchanged}
{if errors: "Errors: {N} failed (see warnings above)"}
Sources:
- GitHub: {N} issues ({N} open, {N} closed)
- Backlog: {N} items from .kiln/issues/
- Docs: {N} PRDs from docs/features/
project: "[[{slug}]]" (FR-005)Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub yoshisada/ai-repo-template --plugin shelf