Polyhydra Skills  /  Agent Workflow

projects

Navigate ~/projects -- list hubs with purpose and health, initialize a new hub (single-repo, or multi-repo via project-home), print a briefing block for a new conversation, give a "catch me up" recent/horizon/goals summary for one project, recommend what to work on next (git-dirty plus PR plus live-session signal), launch a session in a project, capture a stray idea with no home yet, park or activate backlog entries. Use for "what should I work on", "list my projects", "set me up on X", "start a session in X", "new day", "catch me up on X", "standup", "what happened on X", "new idea", "jot this down", "note this idea for later", or naming a project under ~/projects.

Agent Workflow

Drop this in — save the block below as ~/.claude/skills/projects/SKILL.md, or run:

mkdir -p ~/.claude/skills/projects
cat > ~/.claude/skills/projects/SKILL.md <<'EOF'
# (paste the full source block below into this file)
EOF

Full source

SKILL.md — copy everything inside
# projects

Helper for navigating the `~/projects` hub convention (see
`~/.claude/skills/project-home/SKILL.md` for the scaffolding rules this skill
delegates to, and `~/projects/LOCAL-WORK-RUNBOOK.md` for the shared local-work
contract every hub links to).

This skill has seven actions: **list**, **init**, **context**, **new-day**,
**recommend**, **launch**, **idea** (plus park/activate shortcuts). Figure
out which the user wants from phrasing; if genuinely ambiguous, offer them as
`AskUserQuestion` options rather than guessing.

## Shared data sources

Two commands feed everything below. Run them once per invocation and reuse
the output rather than re-running per project.

1. **Audit** (git-dirty / PR-workspace / broken-link signal per hub) —
   requires `~/code/dev-forge` to be checked out; this skill does not have a
   fallback if it's missing, so say so plainly and skip signal-dependent
   parts (`recommend`, health flags) rather than guessing:

   ```sh
   cd ~/code/dev-forge && python3 tools/project_focus.py \
     --projects-root ~/projects audit --no-github \
     --output-dir "$CLAUDE_JOB_DIR/tmp/project-focus" >/dev/null
   ```

   Then read `$CLAUDE_JOB_DIR/tmp/project-focus/project-focus.json` (or
   `/tmp/...` outside a background job — see the memory rule about scratch
   space). Key fields per entry in `active`/`backlog`: `name`, `path`,
   `dirty_repository_count`, `pr_workspace_count`, `broken_links` (list),
   `repositories` (each with `branch`, `dirty`, `github_slug`, `path`).
   `--github` is slow/needs API access; only add it back if the user
   explicitly wants GitHub issue-hotspot data folded in.

2. **Live sessions**:

   ```sh
   claude agents --json --all
   ```

   Each entry has `cwd`, `kind` (`interactive`/`background`), `status`
   (`busy`/`idle`, interactive only) or `state` (`done`, background only),
   and `name`. Match to a project by `cwd` starting with the project's path
   (a project can have zero, one, or several sessions open across its
   repos).

3. **Project codes** — `~/projects/project-codes.yaml` maps each hub's
   directory name to a hand-curated 4-letter code (e.g. `dev-forge` →
   `DVFG`, `rpgs` → `RPGS`, `xu4` → `XU4X`). It exists so background-job
   labels are short, memorable, and scannable in `claude agents` — see
   **launch → Background** below for how it's applied. Read this file
   whenever composing a `-n` label; don't invent a code inline.

## Classify a hub's shape

Needed by `list`, `init`, and `context`. The flat-vs-structured taxonomy
(what `workspace.yaml` means) is `project-home`'s § "Before scaffolding
anything: check for the two-tier reality" — not re-derived here. This adds
what that scaffolding-time framing doesn't need: a read-time "single-repo
hub" shape and the purpose-extraction rule per shape, for `~/projects/<name>`:

- **Structured project-home** — has `workspace.yaml`. Purpose comes from its
  `purpose:` field; repo list from `repositories:`.
- **Single-repo hub** — has a `main` symlink (or one repo-named symlink,
  either directly at the hub root or as the sole entry under a `repos/`
  dir — e.g. `rook`, `peeete`) + `LOCAL-WORK.md`, no `workspace.yaml`.
  Purpose comes from the first non-blank paragraph of `README.md`/`AGENTS.md`
  if present, else none. A `repos/<name>` layout with exactly one symlink
  and no `workspace.yaml` is still this shape, not "structured" — the
  `repos/` dir alone isn't the signal, `workspace.yaml` is.
- **Flat hub** — multiple top-level repo symlinks, no `workspace.yaml`, no
  single `main`. Purpose from `README.md` if present.
- **Other** — doesn't match any of the above (rare; describe what's actually
  there rather than forcing a label).

`_backlog/` is not a project — it's project_focus.py's parking lot. List its
contents separately, not interleaved with active hubs.

## list

1. `ls ~/projects` (skip `_backlog`, `AGENTS.md`, `LOCAL-WORK-RUNBOOK.md`).
2. Classify each hub's shape (above) and pull its one-line purpose.
3. Cross-reference the audit JSON for that hub's `name` to get dirty-repo
   count, PR-workspace count, and `broken_links`.
4. Cross-reference live sessions by `cwd` prefix to show if something's
   actively running there right now.
5. Render as a table: Name | Shape | Purpose | Signal (e.g. "2 dirty, 1 PR
   workspace" / "quiet" / "⚠ broken link: X") | Session (busy/idle/—).
6. Also list `_backlog/` contents separately as "parked".

## init

First check both flat-hub and structured-project-home conventions aren't
already a better fit than starting from scratch — re-read
`~/.claude/skills/project-home/SKILL.md` Step 1–2 if there's any chance this
overlaps an existing hub.

- **One repo** → lightweight scaffold, matching the existing
  frotz/meme-forge/channel-cheevos convention:

  ```sh
  mkdir -p ~/projects/<name>
  ln -s ~/code/<repo> ~/projects/<name>/main
  ln -s ../LOCAL-WORK-RUNBOOK.md ~/projects/<name>/LOCAL-WORK.md
  ```

  Write a short `README.md` with the purpose (ask the user for one line if
  it's not obvious from the repo's own README). Don't pre-create `prs/`,
  `.claude/`, or other directories that only make sense once they're
  actually needed — those get created by the workflows that need them.

  Also assign the new hub a 4-letter code: pick something memorable derived
  from the name, check it against the existing values under `codes:` in
  `~/projects/project-codes.yaml`, and add the new `<name>: <CODE>` entry
  (bump a letter if it collides — don't silently reuse one). Do this for
  both scaffold paths below, not just the lightweight one.

- **Multiple repos** → don't duplicate the scaffolding logic here. Invoke
  the `project-home` skill (`Skill` tool, `skill: "project-home"`) and let
  it run its full candidate-finding / dedup / workspace.yaml flow.

If it's not obvious which shape applies (e.g. "set up a hub for X" with an
unclear repo count), ask via `AskUserQuestion` before scaffolding — per the
answered scoping question, default to the lightweight single-repo path only
when there's genuinely one repo in scope.

## idea <text>

Low-friction capture for a stray idea that has no home yet -- don't scaffold
a hub, don't write a GitHub issue, just record it in one line so it isn't
lost.

1. If `~/projects/_ideas.md` doesn't exist yet, create it with:

   ```markdown
   # Ideas

   Append-only. One line per idea: `- [ ] YYYY-MM-DD: <idea text>`. Check
   off (`[x]`) when promoted or dropped -- add ` (-> <hub/issue>)` or
   ` (dropped)` after checking. Never delete a line; the checked-off history
   is the log of what got looked at.
   ```

2. Append one line: `- [ ] <today's date>: <idea text>`, using the user's
   own phrasing -- don't polish it (same rule as dev-forge's
   `workflows/work-intake.md`).
3. That's the whole action -- no confirmation prompt for the append itself,
   that would defeat "low friction". Only reach for the standard end-of-turn
   picker if there's a real next question (e.g. promote now vs leave it).

### Promoting an idea

When an idea from `_ideas.md` is ready to become real work, pick a lane:

- **New project hub** -- run `init` above, then check the line off with
  `(-> <hub-name>)`.
- **GitHub issue in an existing repo** -- feed the idea's text into
  dev-forge's intake template (`~/code/dev-forge/workflows/work-intake.md`)
  as the `Context` field, then check the line off with
  `(-> <repo>#<issue>)`.
- **Dropped** -- check it off with `(dropped)` and a short reason if it's
  not obvious why.

## context <project>

Produces a briefing to prime a new conversation on a project — either to
paste as an opening message, or to just orient the current conversation
before starting work.

1. Resolve `~/projects/<name>`, confirm it exists (offer `list` output if
   not, don't guess a close match silently).
2. Classify shape, get purpose, get repo list (see above).
3. For each repo: current branch + dirty status (from the audit JSON if it's
   fresh, else `git -C <path> status --short --branch` directly).
4. Any live sessions already open there (from `claude agents --json`) — if
   one exists, lead with that; opening a second session on the same dirty
   state is a common way to create merge conflicts with yourself.
5. Note PR workspaces / worktrees present (`pr_workspace_count`,
   `worktrees/` dir listing).
6. Compose a compact briefing: purpose, repo(s) + branch + dirty state,
   active sessions, anything from `LOCAL-WORK-RUNBOOK.md` §1 that's specific
   to this hub (e.g. `main` vs `~/code/<repo>` resolution), and any
   project-specific runbook file if one exists (`AGENTS.md`, `PORTAL.md`,
   `*-WORK-LOG.md` in the hub root).

## new-day <project>

"Catch me up" structured summary for one project (hub or single repo):
what shipped recently, what's open, and what the goal structure looks like.
Complements `context` — `context` primes a *session* with current state
(branch, dirty, active sessions) to start work; `new-day` looks backward
(recent commits) and forward (open issues, goal docs) to orient a *person*
at the start of a day. Reuses the same resolve/classify/audit steps as
`context`; don't re-derive them.

1. Resolve `~/projects/<name>` and classify shape (as in `context` steps
   1–2). Confirm repo list.
2. **Recent** — per repo, against the upstream tracking branch, not just
   local `HEAD`: `git -C <repo> fetch -q` then
   `git -C <repo> log @{u} --oneline --since="7 days ago"` (fall back to the
   local branch if there's no upstream). A local checkout that's behind
   origin will otherwise silently under-report recent activity — confirmed
   live on `dread-tv`, where local `HEAD` showed 1 commit in 7 days but
   `origin/main` showed 17. If the log is empty even against upstream, fall
   back to `git -C <repo> log @{u} --oneline -n 10` and say explicitly
   "quiet for >7 days, showing last 10 commits" rather than rendering a
   blank section. Group by repo when the hub has more than one.
3. **Horizon** — per repo with a `github_slug` (from the audit JSON):
   `gh issue list --state open --limit 20` and `gh pr list --state open`.
   If the hub tracks a GitHub Project/milestone that `quest-log` already
   renders, invoke that skill (`Skill` tool, `skill: "quest-log"`) for the
   dungeon-map view instead of re-listing issues by hand — it already does
   prioritized issue rendering; don't duplicate its logic here. Fall back to
   the raw `gh` listing only when quest-log has nothing for that repo.
4. **Goals** — synthesize, don't dump verbatim:
   - `workspace.yaml` `purpose:` (structured hubs) or README's first
     paragraph (single-repo hubs).
   - `~/projects/REMAINING-WORK-SOW.md`'s section for this hub, if present.
   - `<hub>/DELIVERY-PLAN.md`, if present.
   Label this section as a snapshot, not authority — same caveat
   `~/projects/CLAUDE.md` puts on these docs itself. If it materially
   disagrees with what **Recent**/**Horizon** just showed (e.g. the SOW
   calls something "not started" but there's a merged PR closing it), flag
   the mismatch instead of silently picking one.
5. **Hygiene** — dirty repos, PR workspace count, broken links (audit JSON,
   same source as `list`/`recommend`), plus any live session on this hub
   from `claude agents --json`.
6. Render via `tools/new_day_render.py`, don't hand-draw the page. This is
   the one deliberate exception to this user's usual compact-tables
   preference — `new-day` is meant to be read start-to-finish like
   `quest-log`'s dungeon-map screen — but the box-drawing/alignment itself
   is delegated to a script for the same reason `quest_log.py` is: freehand
   ASCII art from the model drifts (column widths, wrap points) and burns
   reasoning on layout instead of content. Assemble the findings from steps
   2–5 into this JSON shape and pipe it in:

   ```json
   {
     "project": "<name>", "purpose": "<one-line purpose>",
     "recent": {"window": "7d, origin/main", "note": "<optional, e.g. local checkout was N behind>",
                "items": ["<commit summary>", "..."], "total": 17},
     "horizon": {"issues_open": 0, "prs_open": 1,
                 "prs": [{"number": 351, "state": "draft", "title": "..."}]},
     "goals": {"source": "REMAINING-WORK-SOW.md", "primary_outcome": "<text>",
               "notes": [{"ok": true, "text": "<confirmation, ✓>"},
                         {"ok": false, "text": "<mismatch found, ⚠>"}]},
     "hygiene": {"working_tree": {"ok": true, "text": "clean"},
                 "vs_origin": {"ok": false, "text": "16 commits behind (not pulled)"},
                 "pr_workspaces": 1, "live_sessions": 1}
   }
   ```

   ```sh
   echo '<json>' | python3 ~/.claude/skills/projects/tools/new_day_render.py
   ```

   `notes[].ok` drives the `✓`/`⚠` marker — that's the "friendly ascii art
   draws the eye" part of the brief; put anything needing attention
   (dirty tree, behind origin, stale goal doc) in an `ok: false` entry so it
   stands out against the clean `✓` lines. Print the script's stdout
   verbatim as the response — don't re-wrap or re-indent it afterward.
7. End in the standard `AskUserQuestion` picker (see § Ending a turn) — real
   next actions: dig into one item, launch a session here, promote an idea
   from `_ideas.md` if one's tied to this project, stop here.

## recommend

Combine the audit JSON with live sessions to rank what's worth attention,
not just what's busy:

0. **Open ideas** -- if `~/projects/_ideas.md` exists, count unchecked
   (`- [ ] `) lines and surface the count plus the single oldest open one as
   a low-priority nudge. Don't fetch or analyze anything else about it --
   this is a cheap grep, not part of the audit JSON. Skip silently if the
   file doesn't exist yet.
1. **Needs attention** — dirty repos or PR workspaces with *no* live
   session. This is state that could get forgotten; surface first.
2. **Resume** — an *idle* session sitting on a project with dirty state.
   Picking this back up is cheap since the session's still warm.
3. **In progress** — a *busy* session. Informational only; don't suggest
   switching away from active work without reason.
4. **Parking candidates** — quiet locally, no session, present in the
   audit's own `parking_candidates`/`next_action` fields. Low priority, but
   worth naming so `_backlog` doesn't silently grow stale.

Broken links (`broken_links` non-empty) are a separate flag, not a priority
tier — mention them wherever they show up, they're a hygiene issue not a
work signal.

Present the ranked list, then end (per this user's standing MAID
preference) with `AskUserQuestion` offering the top 2-3 candidates plus
"show full list" and "none of these" as tappable options — not prose to
reply to.

## launch

Two independent choices: **where** the session lands, and **how** it runs.
Settle "where" first — it's the one with consequences.

### Where: hub root vs repo

cwd at launch is the only thing that scopes a session (there is no `--cwd`
flag), and it keys `claude -c`, `/resume`, the session's memory dir under
`~/.claude/projects/`, and CLAUDE.md/AGENTS.md discovery.

- **Hub root** (`~/projects/<name>`) — cross-repo context: the hub's
  `README.md`, `AGENTS.md`, `PORTAL.md` are all in scope. As of 2026-08-10
  `~/projects` is itself a git repo (`Polyhydra-Games/projects-hub`), but it
  versions **only hub-level documents** — READMEs, `AGENTS.md`, `PORTAL.md`,
  `workspace.yaml`. It does not track the `repos/` symlinks or any code. So a
  hub-root session can commit planning and doc changes, and gets no git over
  any actual code repo. Right for planning, triage, and cross-repo
  coordination.
- **Repo** (`~/code/<repo>`, bridged with `--add-dir ~/projects/<name>`) —
  git over the code itself: worktree isolation, commits, `/code-review`, and
  background jobs that can isolate. The hub docs stay in scope via
  `--add-dir`. Right for anything that will touch code.

Pick the repo form whenever the opening prompt implies edits. When it's
genuinely a toss-up, offer both via `AskUserQuestion`.

**The symlink trap:** `cd ~/projects/<name>/repos/<repo> && claude` is *not*
a hub-scoped session. Claude Code takes the physical cwd, so it silently
resolves to `~/code/<repo>` — the hub's docs drop out of context and history
lands under a different project dir than expected. Never rely on the symlink
path; go to `~/code/<repo>` and pass `--add-dir` explicitly.

### How: background vs interactive

- **Background** — can be run directly from here. Shell functions are *not*
  available to the Bash tool (they're guarded to interactive shells), so use
  the full commands:
  ```sh
  cd ~/projects/<name> && claude --bg -n "<CODE>-<slug>" "<opening prompt>"
  cd ~/code/<repo> && claude --bg -n "<CODE>-<slug>" --add-dir ~/projects/<name> "<opening prompt>"
  ```
  Always pass `-n` — without a label the agents are hard to tell apart in
  `claude agents`, which is how they're managed afterward. Look `<CODE>` up
  in `~/projects/project-codes.yaml` for the hub the job belongs to (add an
  entry per the **init** step if the hub predates the registry and is
  missing one); `<slug>` is a short kebab-case description of the task, e.g.
  `DVFG-fix-swarm-tag-shadowing`. The code is what makes a busy `claude
  agents --all` list scannable by project and lets threads about the same
  project be tied together on sight even across separate sessions — the
  label itself is not a session ID, so two jobs on the same hub can share a
  code with different slugs.
- **Interactive** — needs the user's own terminal; a skill can't hand off a
  TTY, so print a command for them to run. Prefer their launcher shortcuts
  (defined in `~/.config/claude-projects-shell.sh`, tab-completing on hub and
  repo names) over raw `cd` lines:

  | | shortcut | equivalent |
  |---|---|---|
  | hub root | `p <name>` | `cd ~/projects/<name> && claude` |
  | repo | `pw <name> <repo>` | `cd ~/code/<repo> && claude --add-dir ~/projects/<name>` |
  | just cd | `pcd <name>` | `cd ~/projects/<name>` |

  `pw`'s repo argument is optional when the hub has exactly one repo, and
  trailing arguments pass through to `claude` (`p <name> -c`, `p <name>
  --bg -n <label> "<prompt>"`).

## park / activate

Thin wrappers over project_focus.py's own commands — don't reimplement the
move logic:

```sh
cd ~/code/dev-forge && python3 tools/project_focus.py --projects-root ~/projects park <name> [--dry-run]
cd ~/code/dev-forge && python3 tools/project_focus.py --projects-root ~/projects activate <name> [--dry-run]
```

Run with `--dry-run` first and show the user what would move before doing it
for real — moving a hub's top-level directory is exactly the kind of thing
worth a quick confirmation, even though it's a `shutil.move` (reversible)
rather than a delete.

## Ending a turn

Every handback here still ends in a picker — see CLAUDE.md's MAID section
for the rule, not restated here. Real next actions for this skill: open a
project, launch a session, park/activate, refine the recommendation, stop
here.