Polyhydra Skills / Agent Workflow
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, 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 idea", "jot this down", "note this idea for later", or naming a project under ~/projects.
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
# 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 six actions: **list**, **init**, **context**, **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).
## 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.