obsidian-pr-sync ยท diff

git:20260828.aaaffd4 to git:20260912.a3f45de

105 added, 7 removed. Audit A to A.

---
name: obsidian-pr-sync
description: >
Fetch open GitHub PRs where the user is an assignee or review-requested reviewer,
- then write or refresh a "## PRs to review" section in today's Obsidian daily note.
+ today's Google Calendar events, and auto-create People stubs for new meeting attendees.
+ Then write or refresh a "## PRs to review" section in today's Obsidian daily note.
Use this skill whenever the user asks to sync PRs to Obsidian, update their daily note
- with GitHub reviews, check what PRs need attention, or run a morning PR sync routine.
- Also suitable for scheduled/automated runs: fully idempotent (re-running replaces
- the section, never appends).
+ with GitHub reviews or calendar, check what needs attention, run a morning sync routine,
+ or create people notes from calendar meetings. Also suitable for scheduled/automated runs:
+ fully idempotent (re-running replaces the section, never appends; people stubs are only
+ created once).
license: MIT
allowed-tools: Bash, Read, Write, Edit
compatibility: Codex, Claude Code, Cursor, GitHub Copilot, Windsurf, Kiro, and other Agent Skills compatible tools. Requires the GitHub CLI (`gh`) authenticated and a daily note vault.
metadata:
targets: [_source-only]
author: Oleg Koval
tags:
- github
- obsidian
- daily-note
- pull-requests
+ - people
- productivity
- morning-routine
---
<!-- Generated by scripts/build-adapters.sh. Do not edit directly. -->
# Obsidian PR Sync
Fetch all open PRs where the user is assigned or requested as reviewer, filter out
noise (bots, drafts, self-authored), and write a clean grouped section into today's
- daily note.
+ daily note. The calendar pass also creates idempotent People stubs for attendees who
+ do not already have a matching note.
## Configuration
Before running, confirm:
- - `VAULT_DAILY`: absolute path to the daily notes folder (e.g. `/Users/you/obsidian/vault/Lead/Daily`)
+ - `vault_path`: absolute path to the Obsidian vault, from the plugin configuration
+ - `VAULT_DAILY`: derive as `${vault_path}/Lead/Daily`
+ - `VAULT_PEOPLE`: derive as `${vault_path}/Lead/People`
+ - `SELF_EMAIL`: the user's calendar email address, used to exclude the user from attendee processing
- `GH_USERNAME`: GitHub login to exclude self-authored PRs (e.g. `oleg-koval`)
- `ORG_PREFIX`: org name to group as "Work" (e.g. `Teifi-Digital`); everything else goes to "Personal"
If not specified by the user, infer from context (git config, existing vault files).
- ## Step 1: Fetch PRs
+ ## Step 1: Fetch PRs and calendar events
Run two `gh` queries and merge results, deduplicating by URL:
```bash
# Review-requested
gh search prs --review-requested=@me --state=open \
--json title,url,repository,author,createdAt,isDraft --limit 50
# Assigned
gh search prs --assignee=@me --state=open \
--json title,url,repository,author,createdAt,isDraft --limit 50
```
Merge both lists, deduplicate by `url`. The union is what needs attention.
+ Fetch today's calendar events once with the available calendar integration. Retain each
+ event's stable ID, title, and attendees (`displayName`, `email`, and `responseStatus`) as
+ `CALENDAR_EVENTS`, and pass that same collection to Part C. Do not fetch the events again.
+
## Step 2: Filter
Discard entries where:
- `isDraft` is `true`
- `author.login` matches any bot pattern: `dependabot`, `copilot`, `renovate`, `github-actions`, or `author.is_bot` is `true`
- `author.login` equals `GH_USERNAME`: self-authored PRs are not reviews; they're your own work
## Step 3: Enrich authors
For each unique author login, resolve a display name for the Obsidian wiki-link:
```bash
gh api /users/<login> --jq '.name // .login'
```
If the API returns a name, use it (e.g. `Rutger Klaassen`). If it returns null, fall
back to the login with first letter uppercased. This gives `[[Rutger Klaassen]]` or
`[[Merlijnmacgillavry]]`.
Batch these lookups: run them in parallel if possible to keep the sync fast.
## Step 4: Calculate age
For each PR: `days_old = (today - createdAt).days`. Use today's date in local time.
## Step 5: Build the section
Format the section exactly as:
```markdown
## PRs to review
### Work (<ORG_PREFIX>)
- [ ] [TITLE](url) by [[Author Name]] (N days old)
### Personal
- [ ] [TITLE](url) by [[Author Name]] (N days old)
```
Rules:
- **Work**: `repository.nameWithOwner` starts with `<ORG_PREFIX>/`
- **Personal**: everything else
- Within each group, sort by `createdAt` ascending (oldest first: most overdue at top)
- If a group has no PRs, omit that subsection entirely (don't render an empty `### Work` header)
- If there are zero PRs total, write: `## PRs to review\n\n_No open PRs, clear queue! ๐ŸŽ‰_`
- Add `โš ๏ธ` after the age if `days_old >= 7`
**Example line:**
```
- [ ] [RSL-108] Image sync from InRiver by [[Merlijn Mac Gillavry]] (12 days old โš ๏ธ)
```
## Step 6: Write to today's daily note
**Find today's note:**
```bash
TODAY=$(date +%Y-%m-%d)
NOTE="${VAULT_DAILY}/${TODAY}.md"
```
If the file doesn't exist, create it with standard daily template frontmatter first,
then append the section. If the file exists:
**Replacement logic (idempotent):**
- If `## PRs to review` already exists: replace everything from that line up to (but not
including) the next `## ` heading or end-of-file with the newly built section.
- If it doesn't exist: append the section at the end of the file (before the nav footer
line `*โ† [[...` if present, otherwise at the very end).
This ensures re-running the skill produces the same result, not a growing list.
+ ## Part C: People stubs for new meeting attendees
+
+ Run this with `CALENDAR_EVENTS` from Step 1. No additional calendar API calls are needed.
+
+ ### Step C1 โ€” Collect attendees
+
+ Collect attendees across all fetched events:
+
+ - Normalize `SELF_EMAIL` by trimming whitespace and lowercasing it.
+ - For each attendee, normalize `email` the same way. Use the normalized email as the
+ persistent attendee identity. If an attendee has no email, a collection layer may use
+ `event:<event-id>:attendee:<index>` as a transient collision-safe key, but skip that
+ attendee before deduplication, note lookup, or stub creation.
+ - Skip attendees whose normalized email equals normalized `SELF_EMAIL` (self).
+ - Skip attendees whose `responseStatus` is `declined`.
+ - Deduplicate by normalized email across all events.
+ - Normalize the display name by trimming it and collapsing repeated whitespace. If it is
+ empty, fall back to the part before `@` in the normalized email.
+
+ Keep the events each attendee appears in so every meeting can be written to the stub.
+
+ ### Step C2 โ€” Check for existing People notes
+
+ People notes live under `Lead/People/`, including `Peers/`, `Reports/`, and
+ `Stakeholders/`. Discover existing Markdown notes from the configured directory:
+
+ ```bash
+ find "$VAULT_PEOPLE" -type f -name "*.md"
+ ```
+
+ Do not match by filename containment. Treat a note as existing only when its title (the
+ first H1) exactly matches the attendee's normalized display name after case-folding and
+ whitespace normalization, and its recorded email exactly matches the normalized attendee
+ email. Create a stub when no note satisfies both checks.
+
+ ### Step C3 โ€” Create the stub
+
+ Create each new attendee at:
+
+ ```text
+ ${VAULT_PEOPLE}/<SafeDisplayName>--<IdentityHash>.md
+ ```
+
+ Derive `IdentityHash` from the normalized email so attendees with the same display name
+ cannot collide. Before constructing the path, sanitize the display name to a filename-safe
+ basename: remove control characters, normalize whitespace, and replace reserved filename
+ characters with `-`. Reject the original display name if it contains `/`, `\\`, or `..`,
+ and reject an empty or dot-only sanitized basename. Resolve `VAULT_PEOPLE` and the candidate
+ path, then create the file only if the candidate's resolved parent is exactly the resolved
+ `VAULT_PEOPLE` directory. Never overwrite an existing path.
+
+ Use this template, replacing the placeholders and adding one meeting line per event:
+
+ ```markdown
+ ---
+ type: person
+ role: ""
+ team: ""
+ last-1-1:
+ next-1-1:
+ ---
+
+ # <DisplayName>
+
+ ## Context
+
+ - Email: <email>
+
+ ## Strengths
+
+ ## Growth areas
+
+ ## Recent 1:1s
+
+ ## Running notes
+
+ ## Meetings
+
+ - [[Lead/Daily/<TODAY>]] - <Event title>
+
+ <!-- Classify: teifi.com email โ†’ Peers or Reports ยท external email โ†’ Stakeholders -->
+ ```
+
+ Do not add `## Code Review Signals`; that section is for direct reports only.
+ Stubs are idempotent: existing matching notes are skipped and never overwritten.
+
## Invocation patterns
**Manual (user-triggered):**
The user says something like "sync my PRs to today's note" or "update obsidian with my reviews".
Run the full flow and confirm how many PRs were written.
**Scheduled morning routine:**
Same flow. No user prompt needed: just run, update the note, and report a one-line
summary: `"PR sync: 3 work PRs, 2 personal PRs written to 2026-05-11.md"`.
**In a remote CCR agent:**
Clone the vault repo, run the sync, commit and push. The local Obsidian Git plugin
pulls the changes automatically.
## Output to user
After writing the file, show a brief confirmation:
```
Synced N PRs to Lead/Daily/YYYY-MM-DD.md
Work (<ORG_PREFIX>): X PRs
Personal: Y PRs
Skipped: Z bots/drafts
+ People: N new stubs created (or "all known")
```
If any `gh` call fails (e.g. auth expired), surface the error clearly rather than
writing a partial/corrupt section.