# Jot Gh

> Capture, triage, and plan work on GitHub issues from natural language, across one or many repos. Auto-triggers on phrases like "capture this:", "what should I work on next", "what's on my plate today", "tidy the backlog", "find duplicate issues", "all <X> bugs to @<user>", and explicit `/jot-gh <cmd>`, `jot:`, or `jot-gh:` prefixes. Use when the user wants to turn brain-dump text into a well-formed GitHub issue, decide what to work on next, run dedup/stale passes, or bulk-edit issues by natural-language intent. Requires the `gh` CLI authenticated via `gh auth login`.

- Skill: `andresd/jot-gh` (Agent Skill)
- Install (CLI): `npx skillmds@latest add andresd/jot-gh`
- Raw SKILL.md: https://api.skillmd.com/api/skills/andresd/jot-gh/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: andresd (https://skillmd.com/u/andresd)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/andresd/jot-gh

---


# jot-gh

A GitHub-only port of the `jot` CLI loop: capture brain-dumps into well-formed issues, decide what to work on next, and keep the backlog tidy — all through `gh` calls. No tracker abstraction (GitHub only), no licensing, no persistent SQLite. Auth is whatever `gh auth status` already provides.

## When to invoke

**Explicit (always invoke):**
- `/jot-gh <command>` — slash form
- `jot: <command>` or `jot-gh: <command>` — prefix form

**Auto-invoke (use judgment, prefer explicit when unsure):**
- "capture this: ..." / "capture: ..." / "log this: ..."
- "what should I work on (next)?" / "what's next?"
- "what's on my plate today?" / "what am I doing today?"
- "tidy the backlog" / "find duplicate issues" / "find stale issues"
- "(re)assign all <X> bugs to @<user>"
- "make all <X> issues P0/P1/P2/P3"
- "what does the backlog say about <topic>?"

**Do NOT invoke for:**
- General GitHub questions unrelated to issue triage ("what's the API for X?", "how do I fork?")
- Pull request review or code review (that belongs to other skills)
- Anything that does not touch the issue lifecycle (issues, labels, assignees, comments)

## Prerequisites

Before any command, verify `gh` is authed:

```bash
gh auth status
```

If the exit code is non-zero, stop and tell the user:

> Run `gh auth login` first. jot-gh uses your `gh` credentials directly and does not store its own auth.

Also ensure `jq` is available for parsing `gh --json` output (`command -v jq` — error clearly if missing).

## Repo resolution

Most commands operate on **one repo at a time**. To pick:

1. If the user passed `--repo OWNER/REPO`, use that.
2. Otherwise, run `gh repo view --json nameWithOwner -q .nameWithOwner` from the current directory — if it returns a repo, use it.
3. Otherwise, ask the user which repo via `AskUserQuestion`. Offer the 3 most-recent repos from `gh repo list --json nameWithOwner --limit 10 -q '.[].nameWithOwner'` as options.

**Multi-repo flows** (`next` by default, `ask` if no repo context): use `gh search issues` with `--owner @me` or assignee filters. Each issue carries its repo in the result; treat writes per-repo. Do not run `tidy`, `assign`, or `reprioritize` cross-repo — those are single-repo only.

## Memory file: `.github/jot.md`

If `.github/jot.md` exists in the target repo, load it before any LLM-y step (extraction, ranking, dedup decision). It holds repo-specific rules: glossary, priority defaults, label conventions, "all bugs filed by @alice are P1", "anything tagged regression is P0", etc.

Read it via:

```bash
gh api "repos/OWNER/REPO/contents/.github/jot.md" --jq .content 2>/dev/null | base64 -d
```

Cap at ~4KB. If larger, take the first 4KB and tell the user the file was truncated.

Apply memory rules when:
- Extracting structured fields in `capture` (priority, labels)
- Ranking issues in `next`
- Deciding dedup similarity in `capture` and `tidy`

If memory rules conflict with the user's explicit request in this turn, the user wins.

## Cache: `~/.cache/jot-gh/`

Cache recent issue digests at `~/.cache/jot-gh/<owner>__<repo>.json` so `next`/`tidy` don't re-fetch 500 issues per invocation.

- **TTL**: 10 minutes. Compare file `mtime` to now; if older, refetch.
- **Bypass**: any command with `--no-cache`.
- **Miss path**: `gh issue list --repo OWNER/REPO --state open --limit 200 --json number,title,labels,assignees,updatedAt,body,url` → write to cache file.
- Cache is **not load-bearing**. Safe to delete at any time. Never treat a cached value as authoritative for writes — re-fetch before `gh issue edit`.

## Action log: `~/.cache/jot-gh/actions.jsonl`

Every successful write appends one JSON line:

```json
{"ts":"2026-05-20T10:00:00Z","cmd":"capture","repo":"andresd/jot","action":"created","number":42,"url":"https://github.com/andresd/jot/issues/42","inverse":{"op":"close","number":42}}
```

- Keep only the last 100 lines (trim head on each append).
- Used exclusively by `undo`.
- Skip the log for previews, dry-runs, and read-only commands.

---

## Commands

### `capture <text>`

Natural-language brain-dump → well-formed issue, with dedup check first.

**Flow:**

1. Resolve repo. Load `.github/jot.md` if present.
2. Pull existing labels once: `gh label list --repo OWNER/REPO --json name --limit 100`.
3. Extract 3–5 keywords from `<text>` (noun phrases, key verbs).
4. Dedup search:
   ```bash
   gh search issues --repo OWNER/REPO --state open --json number,title,body,url --limit 10 -- "<keywords joined with OR>"
   ```
5. Score top 5 candidates for semantic similarity vs. `<text>`. If best score reads as "likely duplicate" (same bug/feature, not just same topic), surface that.
6. Extract structured fields, constrained by the existing-labels list and memory rules:
   - **title**: concise, imperative for actions ("Fix login redirect loop"), declarative for observations ("Search returns 500 on empty query").
   - **body**: original text, then a blank line, then `_Captured via jot-gh on YYYY-MM-DD._`
   - **labels**: pick zero-or-more from the existing labels. **Always** include exactly one of `P0|P1|P2|P3` (default `P2` unless memory rules say otherwise). Include exactly one of `bug|feature|chore|docs` if clear from text.
   - **assignee**: only if `<text>` explicitly names a person via `@handle`.
7. Preview via `AskUserQuestion`:
   - `Create issue` (default)
   - `Comment on dup #N` (only if a dup was found)
   - `Edit fields first`
   - `Cancel`
8. On `Create issue`:
   ```bash
   gh issue create --repo OWNER/REPO --title "..." --body "..." --label "P2,bug" --assignee @me
   ```
   Append to action log with `inverse: {op: "close", number: <new_number>}`.
9. On `Comment on dup #N`:
   ```bash
   gh issue comment <N> --repo OWNER/REPO --body "<captured text>\n\n_Linked via jot-gh dedup._"
   ```
   Log with `inverse: {op: "delete_comment", number: <N>, comment_id: <id>}`.
10. On `Edit fields first`: ask which field (title/body/labels/assignee), update, re-prompt.

**Do not invent labels** that don't already exist on the repo. If the repo is missing `P0..P3`, tell the user and offer to create them (one-shot: `gh label create P0 --color B60205` etc.).

### `next [--for @user] [--scope <filter>] [-n N] [--commit] [--repo OWNER/REPO]`

Top-N ranked open issues with a one-line rationale per item. Defaults: `--for @me`, `n=5`, multi-repo.

**Flow:**

1. List candidates:
   ```bash
   # multi-repo (default)
   gh search issues --assignee @me --state open --json repository,number,title,labels,updatedAt,body,url --limit 100
   # single-repo when --repo passed
   gh issue list --repo OWNER/REPO --assignee @me --state open --limit 100 --json number,title,labels,updatedAt,body,url
   ```
2. For each distinct repo in the results, load `.github/jot.md` (cache the load per turn).
3. Rank by, in order:
   - Priority label: `P0 > P1 > P2 > P3 > (none)`
   - `jot-today` label present (boost)
   - Age (newer wins for `P0/P1`, older wins for `P2/P3`)
   - Memory-rule hints
4. Output a numbered list. Each line: `[OWNER/REPO#N]` prefix, title, priority badge, one-sentence rationale, then URL on the next line.
5. If `--commit`: confirm via a single `AskUserQuestion` (`Commit these N to today / Cancel`), then for each chosen item:
   ```bash
   gh issue edit <N> --repo OWNER/REPO --add-label jot-today
   ```
   Log each. `inverse: {op: "remove_label", label: "jot-today", number: <N>}`.

### `tidy [--repo OWNER/REPO]`

Interactive backlog hygiene. **Single-repo only** (multi-repo tidy is too noisy).

**Flow:**

1. Resolve repo.
2. Fetch all open issues: `gh issue list --repo OWNER/REPO --state open --limit 500 --json number,title,labels,assignees,updatedAt,body,url`.
3. Run four passes:
   - **dup** — cluster by title+body similarity. Report any cluster of 2+, recommending the oldest as canonical and the rest to close-with-link.
   - **stale** — `updatedAt > 30 days ago` and not in an open milestone. Recommend close with comment, or label `stale`.
   - **stale-assignment** — has assignee, no comment or edit in last 14 days. Recommend unassign and ping.
   - **missing-priority** — no `P0|P1|P2|P3` label. Recommend `P2` as default (or memory-rule default).
4. Output a single numbered list (text mode — not `AskUserQuestion`; tidy routinely produces 20+ findings):
   ```
   1. [dup]      #42 looks like dup of #38 — close #42 with link?
   2. [stale]    #15 — no activity since 2026-04-12 (38d). Close with comment?
   3. [stale-asn] #71 — @bob assigned 22d ago, no movement. Unassign?
   4. [priority] #88 — missing P-label. Add P2?
   ...
   ```
5. Prompt the user in plain text:
   > Apply which? Reply with numbers (e.g. `1,3,5`), `all`, or `cancel`.
6. Apply each accepted finding via the appropriate `gh issue edit` / `gh issue close` / `gh issue comment`. Log every write.

### `ask <question> [--repo OWNER/REPO]`

NL Q&A over the backlog with `#N` citations. Read-only.

**Flow:**

1. Resolve repo (or stay multi-repo if none given).
2. Search both open and closed:
   ```bash
   gh search issues --repo OWNER/REPO --state all --json number,title,body,state,labels,closedAt,url --limit 30 -- "<keywords>"
   ```
3. Answer in 2–4 sentences. Cite each referenced issue as `[#42](URL)` (markdown link).
4. No writes. No action log.

### `assign <NL intent> [--repo OWNER/REPO]`

Bulk reassignment by natural language. **Single-repo only.**

Example intents:
- "all open auth bugs to @alice"
- "@bob's open issues to @carol"
- "everything labeled regression to @me"

**Flow:**

1. Parse the intent into `{ label?, assignee_old?, assignee_new, state? }`. If parsing is uncertain, ask via `AskUserQuestion` with the best-guess filter.
2. List candidates:
   ```bash
   gh issue list --repo OWNER/REPO --state open --label "<label>" --assignee "<old_or_*>" --limit 200 --json number,title,assignees,url
   ```
3. Show the numbered candidate list (text mode).
4. Prompt:
   > Reassign these to @\<new\>? Reply with numbers, `all`, or `cancel`. Numbers SKIPPED will keep their current assignee.
5. For each chosen item, use TOCTOU best-effort:
   - Re-read the issue (fresh, no cache).
   - If `<new>` is already assigned, skip with a note.
   - Else: `gh issue edit <N> --repo OWNER/REPO --add-assignee <new>` (and `--remove-assignee <old>` if specified).
   - On HTTP 422/409 (concurrent edit), warn and continue.
6. Log each. `inverse: {op: "set_assignees", number: <N>, assignees: [<prev list>]}`.

### `reprioritize <NL intent> [--repo OWNER/REPO]`

Bulk priority change by natural language. **Single-repo only.**

Example intents:
- "all launch-blocker label → P0"
- "all P3 bugs older than 90 days → close as stale" (this overlaps with `tidy`; route to `tidy` if the intent is hygiene, not promotion)
- "@alice's open issues to P1"

Same flow as `assign`, but operating on `P0|P1|P2|P3` labels:

- For each accepted item: `gh issue edit <N> --repo OWNER/REPO --remove-label "<old P>" --add-label "<new P>"`.
- Log with `inverse: {op: "swap_label", number: <N>, from: <new>, to: <old>}`.

### `status [--team] [--repo OWNER/REPO]`

Default (no `--team`): list issues with `jot-today` label assigned to `@me`, across repos. Show priority and repo.

```bash
gh search issues --assignee @me --label jot-today --state open --json repository,number,title,labels,url
```

`--team`: list `jot-today` issues in the target repo across all assignees, grouped by assignee.

```bash
gh issue list --repo OWNER/REPO --label jot-today --state open --limit 100 --json number,title,assignees,labels,url
```

No writes.

### `clear-plan [--repo OWNER/REPO]`

Remove `jot-today` from all issues currently labeled with it for `@me`.

**Flow:**

1. List:
   ```bash
   gh search issues --assignee @me --label jot-today --state open --json repository,number,url
   ```
2. Show count and titles. Confirm via `AskUserQuestion`: `Clear N items / Cancel`.
3. For each: `gh issue edit <N> --repo OWNER/REPO --remove-label jot-today`. Log.

### `undo`

Reverse the most recent action within 24 hours.

**Flow:**

1. Read `~/.cache/jot-gh/actions.jsonl` (tail).
2. Find the most recent entry where `ts > now - 24h`. If none, tell the user no recent action is reversible.
3. Show the action and its planned inverse via `AskUserQuestion`: `Undo / Cancel`.
4. Re-read the affected issue's current state (no cache). If state has changed in a way that makes the inverse meaningless (e.g. issue already closed, label already gone), abort with a clear explanation rather than forcing it.
5. Apply the inverse:
   - `created #N` → `gh issue close <N> --comment "Reverted by jot-gh undo"`
   - `closed #N` → `gh issue reopen <N>`
   - `add_label X #N` → `gh issue edit <N> --remove-label X`
   - `remove_label X #N` → `gh issue edit <N> --add-label X`
   - `swap_label from=A to=B #N` → `gh issue edit <N> --add-label A --remove-label B`
   - `add_assignee U #N` → `gh issue edit <N> --remove-assignee U`
   - `set_assignees [list] #N` → `gh issue edit <N> --add-assignee <list...>` after clearing current
   - `commented #N (cmt_id)` → `gh api -X DELETE "repos/OWNER/REPO/issues/comments/<id>"`
6. On success, remove the entry from the log. Do not chain undos — one `undo` reverses one action.

### `help`

Print one line per command above. No `gh` calls. No log.

---

## Output conventions

- Cross-repo listings: always prefix items with `[OWNER/REPO#N]`. Single-repo listings: prefix with `#N`.
- Every write reports the resulting URL on the next line (fetch via `gh issue view <n> --repo OWNER/REPO --json url -q .url` if not already known).
- No emoji in skill output unless the user uses emoji first.
- `gh` errors surface verbatim — do not swallow them. If `gh` exits non-zero on a write, stop the batch and report which item failed.

## Confirmation patterns

- **Single write** (`capture` create, single-item `assign`, `undo`, `--commit`): `AskUserQuestion` with Confirm / Edit / Cancel.
- **Multi-write** (`tidy`, bulk `assign`, bulk `reprioritize`, `clear-plan`): numbered text list, free-text reply (`1,3,5` / `all` / `cancel`). `AskUserQuestion` only fits 4 options, so it does not scale here.
- **Read-only** (`status`, `ask`, `next` without `--commit`): no confirmation.

## Rate limits

`gh` handles auth and rate-limit retry on its own. Avoid hand-built pagination loops — use `--limit` and let `gh` paginate internally. If a write batch encounters a rate-limit error, stop, report progress so far, and tell the user to retry after the reset window shown in the error.

## Anti-patterns

- Don't call the REST API via `curl` / `gh api` unless `gh issue` / `gh search` / `gh label` / `gh repo` cannot express the operation. `gh` handles auth, retries, and pagination.
- Don't batch writes without showing a preview first, even when the user said "go ahead" earlier in the turn — previews are cheap insurance against an LLM misparse.
- Don't invent labels that aren't already on the repo. If a needed label is missing, ask before creating it.
- Don't write to closed issues unless the user explicitly named the issue.
- Don't store any auth, tokens, or secrets. `gh` is the source of truth.
- Don't treat the cache as authoritative for writes — always re-fetch the issue before `gh issue edit`.
- Don't run `tidy`, `assign`, or `reprioritize` cross-repo. They are single-repo by design.
- Don't chain undos. One `undo` reverses one action; if the user wants more, they invoke `undo` again.

