Skip to content

coding-agents: add pi as a harness, and give Prime Agent the companion skill (#3772) - #3775

Open
feniix wants to merge 7 commits into
vectorize-io:mainfrom
feniix:feat/prime-agent-skill
Open

coding-agents: add pi as a harness, and give Prime Agent the companion skill (#3772)#3775
feniix wants to merge 7 commits into
vectorize-io:mainfrom
feniix:feat/prime-agent-skill

Conversation

@feniix

@feniix feniix commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #3772.

Prime Agent discovers ~/.prime/agent/skills and the shared ~/.agents/skills exactly as pi discovers its own, but nothing installed a skill for it — so it only ever picked ours up by accident, from the shared root, when the user also ran Codex or dsh.

The change for #3772

const primeAgent = piFamilyInstaller(
  "prime-agent",
  [".prime", "agent"],
  [".prime", "agent", "skills"],   // <- was missing
);

Into its own root, not the shared one: uninstallSkill removes by a fixed directory name, so writing to ~/.agents/skills would make uninstall prime-agent delete Codex's and dsh's copy. There's a test for that.

With both hosts passing one, skillsDir stops being optional — no caller omits it now — so the parameter and its two if (skillsBase) guards become required and unconditional, and the uninstall log line stops under-reporting (it removes a skill as well as the extension entry).

The test that pinned prime-agent as deliberately skill-less is replaced by the pi cases generalised over both hosts, plus one asserting the two roots stay independent: uninstalling pi must not take Prime Agent's skill with it.

Why this PR carries more than that one-liner

piFamilyInstaller does not exist on main — it arrives with the pi harness work in the first five commits, which are not yet on their own PR. #3772's fix is one argument to a factory that this branch introduces, so the two cannot be separated without rewriting the fix against the older hand-written primeAgent installer block.

Read it as the pi harness, then #3772 on top. The last commit (a5fd6f17e) is the fix the issue asks for; the rest is its prerequisite:

commit what
dadc31aa7 refactor: extract the pi extension adapter Prime Agent shares — no behaviour change
a19b0bde4 feat: add pi as a supported harness
776f53218 feat: import past pi and Prime Agent sessions from local disk
fde3bb331 fix: don't let one stray file abort the pi history import
7ab072245 feat: give pi the companion skill, and guard every entrypoint's harness name
a5fd6f17e feat: give Prime Agent the companion skill too — #3772

Each commit builds and passes on its own (verified by checking each out in a worktree).

Background on the pi commits

pi (@earendil-works/pi-coding-agent) is the agent Prime Agent was forked from, and the two expose the identical extension API — same extensions array in settings.json, same before_agent_start / agent_end events, same registerTool, same pkg.pi manifest key, same stored-session schema. So the existing Prime Agent adapter fits pi unchanged; it just had no seam to plug into. The refactor adds one (createPiExtension(harness), mirroring createPluginEntry for the opencode family), and pi becomes a thin entrypoint alongside Prime Agent's.

Both hosts still share one bank per repo — the default coding-agent::{gitProject} is harness-neutral on purpose — so a separate entrypoint per host is about attribution (harnesses.<name> config section, metadata.harness on retained documents, control-plane logo), not about splitting memory.

Two notes for reviewers:

Verification

  • 688 tests pass; tsc, ./scripts/hooks/lint.sh and ./scripts/hooks/check-unused.sh clean.
  • Both regression tests were confirmed to fail without their fix (ENOTDIR for the history guard; expected 'prime-agent' to be 'pi' for the entrypoint guard).
  • History readers validated against real stores: 202 pi sessions attributed to one repo with zero unattributed, in 760ms across a 34k-folder tree.
  • Generated files (skill/SKILL.md, the docs page, the docs-skill mirror) regenerated per commit, so each one is self-consistent.

feniix and others added 6 commits August 24, 2026 13:57
… shares

Prime Agent is a fork of pi and its extension API is pi's: the same
`extensions` array in settings.json, the same `before_agent_start` /
`agent_end` events, the same `registerTool`. The adapter for it was written as
if that API were Prime Agent's own, so a second host on the same API had
nowhere to plug in.

Move it to harness/pi-extension.ts behind `createPiExtension(harness)`,
mirroring how harness/plugin-entry.ts already serves the opencode family, and
leave prime-agent.ts as a thin entrypoint that names its harness. The message
normalizer moves with it: the shape it parses is pi's, not the fork's, so
transcript-prime-agent.ts becomes transcript-pi.ts (PiMessage, readPiMessages).

No behaviour change — the harness is still "prime-agent", and the adapter tests
move across unchanged apart from being parameterised by harness name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pi (@earendil-works/pi-coding-agent) is the agent Prime Agent was forked from,
and the extension adapter extracted in the previous commit already fits it
unchanged. This wires it up as a harness of its own: src/pi.ts passes "pi" to
createPiExtension, the installer registers the built dist/pi.js in the
`extensions` array of ~/.pi/agent/settings.json, and the registry, tsup entry
map and Docker E2E setup gain the matching entries.

pi and Prime Agent now share one installer factory: they differ only in config
directory and which bundle reports which harness, so the two configs are proved
independent by test rather than by two near-identical code paths.

A separate entrypoint per host is what keeps each attributable — its own
`harnesses.<name>` config section and its own agent stamped on every document
it retains — while both still share one bank per repo, as every coding agent
here does by default. The control plane gains the `pi` logo entry so those
documents render a mark instead of a bare metadata chip.

Not routed through our package.json `pi` manifest key: both hosts read that same
key, so it can only ever name one bundle, and it stays pointed at Prime Agent's.
The README says so, since the package route would otherwise mis-attribute pi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…al disk

Both hosts persist every session as JSONL — a `{type:"session", id, cwd}`
header followed by `{type:"message"}` entries wrapping the same message objects
the live `agent_end` event hands over — so `install <harness>
--import-conversations` can now backfill a repo's history instead of reporting
"no local history reader", which is what it did for both.

readPiTranscript shares renderMessage with the live path rather than
re-deriving the normalization, so an imported session and a live one reach the
bank identically, injected memory stripped either way.

Only the file layout differs between the two hosts, which is all the per-host
code there is:

  pi           sessions/--<encoded cwd>--/<timestamp>_<uuid>.jsonl
  Prime Agent  sessions/<uuid>.jsonl, flat

pi's folder name is prefiltered on because that store holds one folder per
working directory — 34k on the machine this was written on — but attribution
comes from the header `cwd` in both cases, never the folder name: the encoding
is not injective (`/a/b` and `/a-b` collide), the same trap the Claude reader
documents. Sessions recording no cwd are skipped and counted.

Both hosts are done together deliberately: they share a schema, so adding a
reader to one would leave the other as exactly the sibling gap that keeps
biting us.

Verified against the real stores: 202 pi sessions attributed to one repo with
zero unattributed, in 760ms across the full tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
piHistory prefilters session folders by encoded name and hands each match to
readdirSync, so a regular FILE named like a session folder threw ENOTDIR
straight out of importLocalHistory — which documents that it never throws, and
whose caller (installer.ts importConversations) has no catch. One junk file in
~/.pi/agent/sessions killed the whole --import-conversations run rather than
costing that entry.

Guard the per-directory listing, as dshHistory already does for the same case.
The regression test fails with ENOTDIR without it.

claudeHistory has the identical exposure — it is the reader this one was modelled
on — and is deliberately left alone here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rypoint's harness name

Two gaps a review of the pi harness turned up.

pi discovers skills from `~/.pi/agent/skills` and the shared `~/.agents/skills`,
but nothing installed one for it — so a pi-only user got no companion skill,
while a user who also runs Codex or dsh picked ours up by accident from the
shared root. Install into pi's OWN directory: uninstallSkill removes by a fixed
name, so writing to the shared root would make `uninstall pi` delete Codex's and
dsh's copy. Prime Agent reads `~/.prime/agent/skills` the same way and could
take the skill too; it deliberately does not yet, and a test pins that so the
difference does not read as an oversight.

The harness name an entrypoint reports is a bare string literal — it picks the
`harnesses.<name>` config section, feeds `{harness}` bank templating, and is
stamped on every document that host retains — and nothing checked it. The
registry, the installer and the control plane's logo map are three separate
hand-maintained lists, so `createPiExtension("prime-agent")` in src/pi.ts would
have shipped green. Assert it over the whole family instead, enumerated from
PLUGIN_ENTRYPOINTS rather than a fourth list, covering both idioms in use (the
shared-factory argument, and the module constant Cline and dsh declare).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes vectorize-io#3772.

Prime Agent discovers `~/.prime/agent/skills` and the shared `~/.agents/skills`
exactly as pi discovers its own, but nothing installed a skill for it — so it
only ever picked ours up by accident, from the shared root, when the user also
ran Codex or dsh.

It already shares pi's installer factory, so this is the skills directory it was
missing. Into its OWN root, not the shared one: uninstallSkill removes by a fixed
directory name, so writing to `~/.agents/skills` would make `uninstall
prime-agent` delete Codex's and dsh's copy.

With both hosts passing one, `skillsDir` stops being optional — no caller omits
it now, so the parameter and the two `if (skillsBase)` guards become required and
unconditional. The uninstall log line stops under-reporting: it removes a skill
as well as the extension entry.

The test that pinned prime-agent as deliberately skill-less is replaced by the pi
cases generalised over both hosts, plus one asserting the two roots stay
independent — uninstalling pi must not take Prime Agent's skill with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Strix Security Review

Warning

This pull request has 17 commits after the last Strix review (a5fd6f1). Strix has not reviewed these changes.
Automatic review on push is off for this repository. To review the latest changes, tag @strix-security in a comment, or turn on re-review on push.

No security issues found.

Updated for a5fd6f1.


Reviewed by Strix
Re-run review · Configure security review settings

# Conflicts:
#	hindsight-docs/docs-integrations/coding-agents.md
#	hindsight-integrations/coding-agents/README.md
#	hindsight-integrations/coding-agents/src/installer.ts
#	skills/hindsight-docs/references/sdks/integrations/coding-agents.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

coding-agents: Prime Agent never gets the companion skill, though it discovers the same skill roots pi does

1 participant