mcpField
P
paseokit

paseokit

github.com/mimukitAgent workflows2205 installsUpdated 1d ago
Repository

About paseokit

paseokit is a skill for reconciling git worktrees with Paseo’s workspace registry. It separates registration from cleanup, previews destructive changes, and leaves worktree creation and removal to gitkit rather than managing agents.

Key features

  1. 01List workspace registry drift
  2. 02Register missing worktrees with sync
  3. 03Clean duplicate and finished entries
  4. 04Align Paseo worktree roots
  5. 05Preview and confirm cleanup actions

Use cases

  1. 01Register existing worktrees in Paseo
  2. 02Identify missing or duplicate workspace rows
  3. 03Remove landed worktrees after a merged PR
  4. 04Check where Paseo should place new worktrees

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/mimukit/skills --skill paseokit

After installing, type/paseokitin Claude Code to confirm it loaded.

Bundled filesmodes/align.mdmodes/clean.md

Details

From the project README

paseokit

Paseo shows one workspace per checkout, either a main repo or a git worktree under it, and hangs agents off each one. Worktrees themselves come from plain git worktree: gitkit's convention, issuekit's start, or your own hands.

Paseo registers nothing on its own. A worktree that git worktree add created is invisible to Paseo until something registers it. There is no discovery setting to turn on, so real work never appears in the sidebar by itself.

Paseo does prune one thing on its own, per directory pruning in the version seam: it archives every active row whose directory has gone. It archives nothing else: it never collapses duplicate rows, never judges whether work landed, and never touches a row whose directory still exists. So the drift that remains is the work that never appears, the duplicates, and the finished worktrees still sitting on disk.

paseokit owns exactly that reconciliation, split by direction: sync adds what is missing, and clean removes what is finished — the duplicate rows, and the worktrees whose work already landed. Each is scoped to the project you run it from — or the whole machine on request — and running either twice changes nothing the second time.

It is on-demand by design. Worktrees appear in Paseo when you run sync, not when they are created. That is a deliberate trade: no git hooks, no scheduler, no background process, nothing written into any repo.

The line paseokit does not cross

Git owns the worktree; Paseo owns the row. paseokit never runs paseo workspace create --isolation worktree to do real work, never invents a path, and never lets Paseo create a branch. Creating and removing a worktree is native git worktree under gitkit's convention, so a machine with no Paseo on it runs identical commands.

paseokit works one layer up, on the only thing Paseo alone knows: the registry row, meaning that a workspace exists for this path, under this project, with this title.

Two consequences worth stating plainly:

  • paseo workspace archive is not registry-only. It archives every agent in the workspace, kills every terminal in it, and deletes the backing worktree directory when Paseo counts that directory as its own and no other active row points at it. Only the row is reversible; the directory is not. Read The archive call deletes directories before you queue a single archive.
  • Removing a worktree is still gitkit's job. paseokit drives the teardown through gitkit and never lets an archive stand in for it, because an archive that deletes is a delete nobody previewed.
  • clean is the one mode that reaches the disk. It decides which worktrees are finished and it drives the teardown, but it runs no git worktree remove of its own when gitkit is installed, and it never invents a path or a branch rule. The decision is paseokit's; the removal is still gitkit's.

paseokit never touches the tracker. It reads issues and pull requests to build a title and judge a verdict; it never closes an issue, moves a label, or edits a pull request. A merged pull request whose issue is still open is tracker drift, and it says so, routing to issuekit close.

When this fires

  • list. "What does paseo think my worktrees are", "why isn't my worktree in the sidebar", "show me the drift". Read-only.
  • sync. "Sync my worktrees to paseo", "register my worktrees". Scoped to the current project by default; "sync all", "sync everything", or "all my projects" widens it to the whole machine — see Scope.
  • clean. "Clean up the worktrees whose PRs merged", "delete the merged worktrees and their sessions", "clean the dead rows out of my sidebar", "my machine is full of landed work". The only mode that archives a row or deletes from disk; it lists every planned archive and delete first, then confirms the archives as one set and each delete on its own.
  • align. "Where should paseo put new worktrees", "make paseo use my worktree root".

If no mode is clear, start with list. It is read-only and it names which rows sync would touch, so it is never the wrong first move.

sync versus clean. sync only ever adds: it registers, retitles, and restores, and it never archives a row, never collapses a duplicate, and never touches the disk. Every removal, from the archive of a dead row up to the deletion of a merged worktree, belongs to clean. clean previews both halves in one list and asks before it touches either; its disk half additionally proves the pull request merged before a worktree can appear in that list, and follows gitkit's removal rule of one confirmation per worktree.

Not this skill: creating a worktree (gitkit), closing an issue and tearing down after a merge (issuekit close), or driving agents, since paseo run, attach, send, logs, and the schedule surface all belong to the paseo CLI directly. paseokit is registry hygiene, not agent operation.

Preflight (every mode)

paseo status        # CLI installed? daemon running and reachable?
  • paseo not installed → this machine has no Paseo, so there is nothing to reconcile. Say exactly that and stop. Do not fall back to anything: the worktrees are already fine without Paseo, and nothing else in the workflow depends on this skill.
  • Daemon not running or unreachable → name paseo start and stop. Do not start a daemon on someone's machine unasked.
  • gh missing or unauthenticated → titles degrade to the branch name and the tracker column reads "unknown". Nothing else degrades in list, sync, and align. clean's disk half is the exception and it stops, because its whole precondition is a proven merge and only the tracker can prove one; its registry reap still runs, behind its own preview and confirmation.
  • A rejected paseo flag or subcommand → the CLI moves fast. Check paseo <command> --help before concluding an operation is unsupported, and compare the two version numbers below before concluding the CLI is at fault. The goal is the contract, meaning the row registered, the row archived, the title set; the exact flag spelling is not.

Every version-dependent fact sits in The version seam. Re-check that list against a newer version before trusting a write.

Read the two version numbers separately. paseo status prints CLI and Daemon Version as separate rows, and they drift apart: the CLI upgrades on the next install, and the daemon keeps running the code it started with until paseo restart. The CLI advertises every flag its own version knows, so a flag can parse locally and still fail at the daemon. When the two numbers differ, treat a rejected write as a version skew first and say so. Do not run paseo restart to close the gap. A restart kills every running agent, including the one reading this.

The version seam

Verified against Paseo CLI 0.8.0 and daemon 0.8.0. Every fact in this skill that depends on the Paseo version is in this list, and the rest of the skill cites these entries by name and states no version of its own. When the daemon reports a newer version, re-check each entry before trusting a write, and update this list in one place.

  • Directory pruning. A reconciliation pass runs at daemon start and every five minutes after. It archives every active row whose directory has gone, with the reason directory_missing, and nothing else. It arrived in 0.7.
  • The ownership flag. isPaseoOwnedWorktree runs the same <hash>/<slug> path test as the archive delete, and every reconciliation pass refreshes it. Before 0.7 a looser check set it, and it disagreed with the delete.
  • The project group. create, ls, rename, and delete are all implemented. A safe probe for any other verb is one the daemon accepts and acts on without matching anything, such as a well-formed id that exists nowhere.
  • No unarchive. The workspace subcommands are create, ls, rename, and archive, and nothing else.
  • daemon.autoArchiveAfterMerge gates. It acts only on a merged pull request, only on a clean tree, only when nothing is ahead of origin, and only on a Paseo-created worktree. These gates arrived in 0.7.

What Paseo knows about a workspace

Two surfaces, and the difference between them is load-bearing.

The CLI listing is a thin projection. paseo workspace ls --json returns only workspaceId, project (the display name, not the id), name, isolation, and cwd. It lists active workspaces only. That is enough to answer one question, which is whether any live row points at this path, and nothing else.

paseo project ls --json resolves the project id. It returns projectId, name, kind, and path for every live project, so the prj_… id that --project demands now comes from the CLI. Match on path. Use it in preference to the state file, and fall back to the file only when the command is missing or the daemon rejects it.

The state files carry what neither listing exposes. These live under ~/.paseo/projects/:

filewhat it uniquely providesneeded for
workspaces.jsonbranch, title, kind, projectId, createdAt, archivedAt, isPaseoOwnedWorktree, worktreeRoot, mainRepoRoottombstones, duplicate ordering, retitle safety, the delete forecast
projects.jsonprojectKey and archivedAt per projectstray-project detection, and the project ls fallback

kind reads local_checkout for a main checkout and worktree for a worktree.

This is a documented seam, not a stable API. workspace ls still omits branch, title, createdAt, and every archived row, so reading workspaces.json remains unavoidable rather than a shortcut. Treat both files as read-only: paseokit parses them and writes through the CLI, never into them.

If either file is missing, unreadable, or shaped unexpectedly, degrade every writing mode to read-only and say which file and why. Never guess at an id.

A workspace carries a title and a pin, and nothing else, with no issue link and no status field. So link-style enrichment has no equivalent here; the title is the entire surface.

The archive call deletes directories

paseo workspace archive <workspace-id> runs three steps, and only the first is a registry edit:

  1. It archives every agent the workspace owns, live and stored alike, and kills every terminal in it. It does not ask, and it does not refuse because an agent is running.
  2. It archives the workspace record. This part is reversible.
  3. It then tries to delete the backing directory, when no other active row points at it and the directory passes the ownership test below. The delete runs git worktree remove --force and then removes the directory. Git gives none of it back.

The delete has a path test, and the path shape is the whole test. The daemon accepts a directory only at <worktrees.root>/<hash>/<slug>, where <hash> is a short base-36 digest of the main checkout's absolute path. gitkit's convention is $WORKTREE_ROOT/<repo>/<branch>, which carries the repo name where the daemon wants the hash, so a gitkit worktree fails the test and the delete throws Refusing to delete non-Paseo worktree. Only a worktree Paseo created itself passes. The daemon catches that throw, logs it as a warning, and reports the archive as successful, so a refused delete is silent from the CLI.

isPaseoOwnedWorktree forecasts the delete, per the ownership flag in the version seam: true in workspaces.json forecasts a disk delete and false forecasts a registry-only archive. Confirm it against the path shape anyway before you queue an archive, because the stored value is only as fresh as the last pass: a directory whose parent name is the repo name is a git worktree, and a directory whose parent name is an opaque hash is Paseo's.

Three rules follow, and none of them is optional:

  • Treat an archive as a disk delete whenever the worktree sits under a hash directory. That is the only shape the daemon will remove. Preview it as a delete, never as a registry edit.
  • A dirty tree is no protection. The daemon passes --force to git worktree remove, so uncommitted work in a Paseo-created worktree dies with the directory. This is why clean proves the tree clean itself rather than trusting the call to refuse.
  • A live agent does not block the call. The archive stops and archives the agent instead, and it kills every terminal in the workspace. The busy check is therefore paseokit's own guard, not a safety net the CLI provides.

Workspace teardown commands run before the delete, on the backing path. A workspace with a configured teardown script runs it on every archive of a Paseo-owned worktree. A teardown command that fails cancels the directory delete and leaves the row archived, so the workspace and its directory then disagree.

One more thing the registry gets wrong

workspace create is not idempotent. Two identical calls on one path silently produce two rows with the same cwd. Every existence check before a registration is therefore load-bearing rather than an optimization, and it must consider archived rows too, or the tombstone rule below fails silently.

Mode: list

Read-only. Changes nothing, asks nothing, and is the right first move whenever the state is unclear.

Join three sources and match on absolute path, the only key both git and Paseo record:

paseo project ls --json                     # live projects, each with a projectId and a path
git -C "$REPO" worktree list --porcelain    # per live project
paseo workspace ls --json                   # active rows
paseo ls -g --json                          # agents across every directory, each with a cwd and a status

Two filters before anything gets a verdict:

  • No symbolic HEAD, no row. A porcelain record carries either branch refs/heads/<name> or detached; the detached ones never register. This is the same answer git symbolic-ref -q HEAD gives inside the worktree, and it is what keeps debugkit's bisect scratch out of the sidebar.
  • Agent cwd values come back tilde-abbreviated (~/projects/skills). Expand before comparing, or every busy check quietly returns false.

Then give every row a verdict:

verdictmeansfix
busya non-idle agent's cwd is inside this workspaceleave it
registeredworktree exists, exactly one active row points at itnothing
unregisteredworktree exists, no row points at itsync
orphanedan active row points at a path that no longer existswait; the daemon archives it within five minutes
duplicatetwo or more active rows share one cwdclean
tombstonedworktree exists, and its only row is archivedsync, on confirmation
unknown repoworktree under $WORKTREE_ROOT whose repo Paseo has never seensync, on confirmation
stray projecta project whose rootPath is a worktree rather than a main checkoutreported; paseo project delete removes it, on confirmation
reapablepull request merged, issue closed, tree cleanclean

Put busy rows first when any exist. Those are the rows where an action would interrupt live work.

An orphaned row is a timing artifact, per directory pruning. The daemon's own pass archives it, so report it and say when it will go. Queue one for archive only when the user asks to clear it now.

A stray project has a clear signature: its projectKey matches a real project's while its rootPath sits under $WORKTREE_ROOT. That is what a registration without --project produces, and it is invisible in workspace ls because the row beneath it may since have been archived.

Hand off

Write every hand-off in this skill in the procedural register: one instruction per sentence, active voice, present tense, no metaphor.

What changed. Nothing. Say so.

Where it landed. Give one table, with the counts per verdict.

Next. Crown sync when any row is unregistered or tombstoned. Crown clean when any row is duplicate or reapable. When both apply, crown clean first, so sync registers into a registry that is already free of dead rows. Say plainly that the sidebar already matches the disk when every row reads registered.

Mode: sync

The adding mode, and safe to run repeatedly by construction. It registers, retitles, and restores; it never archives a row and never touches a directory or a branch. Removal of any kind, including the archive of a dead row, belongs to clean.

Scope

sync has two scopes, and the words in the request pick one:

  • Project scope is the default. Resolve the main checkout from the current directory with git rev-parse --path-format=absolute --git-common-dir, then act only on that project: its worktrees on disk, and the registry rows whose projectId matches it in workspaces.json. Every other project's rows and worktrees stay untouched and unreported.
  • sync all is machine-wide. The user asks for it with "sync all", "sync everything", or "all my projects". It covers every project paseo project ls --json returns, and it is the only scope that runs the unknown-repo walk below, because that walk is a machine-wide sweep by nature.

Outside a git repository, project scope has no referent. Say so, and name sync all as the way to sweep the machine. Do not silently widen the scope the user did not ask for.

Run list's join first, filtered to the chosen scope, because sync acts on exactly those verdicts.

Straight through, no confirmation

Every operation below is non-destructive and proven so, so none of them asks.

Register each unregistered worktree:

paseo workspace create --isolation local --path "$WT" \
  --project "$PROJECT_ID" --title "$TITLE" --json

--isolation local is correct even though the target is a worktree: it tells Paseo to adopt the checkout at $WT rather than create one. Paseo introspects git and records kind: "worktree", the branch, and mainRepoRoot on its own.

--project is mandatory. Without it Paseo creates a duplicate project rooted at the worktree path, the stray project above. Resolve $PROJECT_ID with paseo project ls --json, matching path to the main checkout, and fall back to projects.json by rootPath with archivedAt null. If the id cannot be resolved, skip the registration and report it. Never register without --project to get past a missing id.

Report each duplicate set, and route it to clean. Report each orphaned row as self-healing. sync names both so the drift is visible, and it archives nothing.

Retitle rows whose title Paseo generated rather than a human:

paseo workspace rename "$WORKSPACE_ID" "$TITLE"

The title is #<n> · <issue title>, with <n> parsed from an issue-<n>-<slug> branch and the title read with gh issue view "$N" --json title. It degrades to the branch name when gh is unusable or the branch names no issue.

Only ever retitle a row whose title is null or exactly the branch name. Anything else was set by a human and is left alone, reported as a disagreement rather than overwritten.

Skip anything busy, naming the agent's short id in the report. Never retitle a workspace with a live agent in it.

Gated on one confirmation each

Both of these widen the scope past what the user asked for, so both stop and ask.

Unknown repos (sync all only, per Scope). Walk $WORKTREE_ROOT/*, resolve each candidate to its main checkout with git -C "$CANDIDATE" rev-parse --git-common-dir, and collect the repos Paseo has never seen. List them, then on one OK create the project first:

paseo project create "$REPO" --json

That call returns the new prj_… id directly. Register the repo's main checkout and then its worktrees with that id, each through workspace create --isolation local --project "$PROJECT_ID".

Paseo's own worktrees need no special case. They already carry a row, and their --git-common-dir resolves to a repo Paseo knows, so they never reach the unknown-repo bucket, and no hash-directory pattern has to be guessed at.

Tombstones. An archived row suppresses re-registration: someone archived that workspace deliberately, and re-adding it on the next run would undo the decluttering they just did. Name the tombstoned worktrees, restore them on one OK, and leave them alone otherwise.

There is no paseo workspace unarchive, per the version seam. "Restoring" a tombstone means creating a fresh row for the same path, so the archived row stays in workspaces.json and the restored workspace is a new wks_… id. Say that when you do it; do not report a resurrection.

Hand off

What changed. Name the scope you ran, the current project or the whole machine. Report registrations, restores, and retitles, each with a count. Name every skip with its reason. Put busy skips first, because those are live work.

Where it landed. Paseo's registry only, and only new or renamed rows. Say plainly that no row was archived and that no directory, branch, or git registration changed, and that the sidebar reflects the new rows within a few seconds.

Next. Crown one:

  • anything busy → name the path and the agent. Tell the user to run sync again after that agent finishes.
  • any stray project → name it, and name paseo project delete "$PROJECT_ID" as the fix. Do not run it here. That command deletes the project and every workspace under it, so it belongs behind clean's preview and confirmation, not in sync's straight-through half.
  • any duplicate or reapable → route to clean, which collapses the duplicate rows and tears the merged worktrees down through gitkit in one pass.
  • tracker drift → route to issuekit close <n>, otherwise gh issue close <n>.
  • nothing left → say the registry matches the disk and stop. This is not a loop worth repeating.

The modes

The mode bodies of clean and align live in one file each under modes/. Route with When this fires, read that one file, and follow it. Everything in this file applies to every mode and is not restated in the mode files. list and sync stay here, because list's join and verdicts are the vocabulary both sync and clean act on.

Notes

  • paseokit is machine-local and always optional. No Paseo on the box means no-op, and nothing else in the workflow may depend on it. gitkit, issuekit, and the rest never call it, because they would break on every machine without the tool. It is a pump you run, not a link in a chain.
  • orcakit is the sibling, not the predecessor. It reconciles the same worktrees into Orca, whose model is the exact inverse: Orca discovers worktrees on its own and knows nothing about them, so orcakit enriches and cleans up, while paseokit registers and reaps. Both are machine-local and optional, and neither ever calls the other.
  • Worktree facts belong to gitkit. The path convention $WORKTREE_ROOT/<repo>/<branch> and the issue-<n>-<slug> branch grammar appear here only as declared portability fallbacks for machines without gitkit. Branch naming, base-ref resolution, and the teardown rules live there, and any other copy of a gitkit fact in this file is a bug.
  • Tracker facts belong to issuekit. paseokit reads issue and pull request state to build a title and a verdict; it writes none of it.
  • Safe work runs straight through; scope-widening and destructive work asks. list never asks. sync runs its additions without a prompt, and stops only for the two operations that register something the user did not name. clean lists every planned archive and delete before doing any of it. It asks once for the archives, because the user should see what leaves the sidebar, and once per delete, because gitkit's removal rule treats each worktree as its own decision, and a removed worktree is the one thing here that git cannot give back.
  • No paseo command is safe because its name sounds safe. archive deletes directories and project delete takes every workspace under it with it. Check paseo <command> --help for what a verb reaches, and treat a one-line description as a summary rather than a contract.
  • Never write into ~/.paseo/projects/*.json. paseokit reads those files for the fields no listing exposes, and writes exclusively through paseo. A hand-edited state file needs a daemon restart to take effect, and a restart kills every running agent.
  • No shell available? Then you cannot reach the paseo CLI, git, or gh. Reason from what the user gives you and print the exact commands as a codeblock for them to run, and never report a workspace registered or archived that you could not perform.

Comments

Markdown supported