Skip to content

Release v1.6.0 - #27

Open
porcelaincode wants to merge 21 commits into
mainfrom
dev
Open

porcelaincode wants to merge 21 commits into
mainfrom
dev

Conversation

@porcelaincode

@porcelaincode porcelaincode commented Aug 4, 2026

Copy link
Copy Markdown
Member

Each commit is scoped to one change. Full gate passes on the tip: typecheck + lint clean, 958 tests, 0 failures (bun test, the command CI runs).

What's in it

Commit Change
96b8116 style: fix lint findings (useConst, formatting) surfaced by the sync commit
6303f42 Cross-device session sync, sharing hardening, and mid-turn steering
472ae32 feat(cowork): add the cowork surface, tool allowlists, and capability control protocol
492cd45 feat(engine): allow reconnecting MCP and re-scanning skills between turns
b0e62b9 feat(threads): emit todo_update and persist task lists and surface on the thread
c0bdd84 feat(subagents): stream Task progress, cap concurrency, and forbid nesting
bc12886 feat(mcp): OAuth for remote servers, per-server status, disabled flag
4bcdd8f feat(permissions): bare mcp__<server> rules cover a whole server
89b2bac feat(skills): bundled office skills + when: glob auto-activation
89d18e5 feat(plugins): installable plugin system for commands, agents, skills, MCP
6d5e8d6 chore(models): drop deprecated sarvam-30b, regenerate the catalog

Landed since that table was written:

Commit Change
94ebc3b fix(test): reset the memoized auth cache in autoTurn's classifier stub (CI fix, part 2 — see below)
37e4c62 fix(test): stop autoTurn's classifier mock leaking into the classify suites (CI fix, part 1 — see below)
584ca33 feat(siblings): let pier sessions on one machine discover and message each other
8c4adf3 feat(memory): reasoning bank — distilled strategy lessons across sessions
79cf9c7 fix(test): wait for the bg shell to reach completed instead of racing it
85c2bed fix(test): reset the memoized thread-sync latch in the discovery test
2ea24fc fix(test): stop outputFormat's models mock leaking into every later test
70027cc fix(auto): share repeat detection, fix the Auto-mode block escape hatch
f862f0b style: biome-format the repeat-detection changes

Notes for review

Cross-device sync (6303f42) is the big new piece since the last description update. Thread sync becomes a real two-way mirror instead of local-wins:

  • Guarded pushes with an expected_count cursor so two devices can't interleave history (bridge 409s → reconcile by item-id prefix).
  • reconcileThread merges local vs server history: remote-extension adopts server, local-extension keeps local, divergence keeps the longer side and preserves the loser as a conflict copy.
  • Outbox: failed pushes stamp pendingPush, retried at boot.
  • Per-thread /sync off opt-out; /share, /unshare, /sessions un-gated.

Session sharing gets a reconnect supervisor + takeover: HostShare.rebind re-announces and re-offers outstanding approvals on a transport drop, UUID request ids, /unshare vs transport-drop split. Repl gets a reconnect loop with backoff; serve --share auto-mirrors via a TeeIO; work_diff streams to remote Changes panels.

New controls/commands: serve-protocol rewind (truncate history + durable bridge copy), pier adopt <id> (cloud→laptop handoff), pier devices, set_model / end_session host controls.

Mid-turn steering: queued messages are handed to the running turn at the next tool-loop boundary (steering_injected retires the chip by text), instead of waiting for the whole turn to resolve.

sarvam-30b is gone upstream. It was Sarvam's only cheap tier, so a few assumptions had to move: the BYOK shortlist and validation probe, cheapCompletion (Sarvam BYOK now pays full freight for throwaway completions — callers already swallow failures to ''), and the fallback map, which now points sarvam-105b at pier-hybrid so a Sarvam-side outage lands on a different upstream rather than a dead sibling. compact.test no longer depends on sarvam-30b's auto_compact_token_limit (no catalog model sets one now) and exercises the context_window * 0.9 fallback instead.

The empty tool allowlist is load-bearing. tools: [] means no tools (the desktop's folderless chat mode); omitting the field means the full set for the mode. serve uses ??, never ||, so [] survives. test/chatModeAllowlist.test.ts pins both that and the fact that every chat-allowlisted tool still resolves, so a registry rename can't silently empty chat mode's toolset.

MCP prefix rules match on the rule, not by splitting the tool name. Server names may contain underscores, so mcp__gmail must not shadow mcp__gmail_backup__send — covered by test.

reloadCapabilities() is only safe between turns. Dynamic tools are process-global under the one-session-per-process invariant; the serve handlers reject with busy while a turn is running.

All new control subtypes are additive and feature-detected via InitializeResponse.capabilities. Older servers answer unsupported subtype, which clients read as absence. CLIENT_SUBTYPES is kept in lockstep with the schema and asserted by a parity test.

CI fix (37e4c62, 94ebc3b)

CI was red on 7 tests — 6 in autoClassify.test.ts, 1 in localClassify.test.ts — while the same commit passed locally.

Root cause. bun test runs all 129 files in ONE process and mock.module is process-global with no unwind. autoTurn.test.ts (file #11 in CI order) mocked ../src/safety/autoClassify.js to swap the classifier verdict per test, which replaced the real module for every file after it. classifyAction became async () => verdict, returning whatever the last autoTurn test left behind — block / "network command". Every downstream test that calls the real classifyAction got that canned block and never reached its own fetch stub, which is why the transcript assertions saw a null lastBody. Tests calling classifyActionLocal directly kept passing, which localised the leak to the mocked module.

Why CI only. The leak depends on whether a victim file resolves its autoClassify import before or after file #11 installs the mock. A static top-level import can win that race; a dynamic await import() inside a test body never does. Confirmed directly rather than by inference: with the mock in place, classifyAction.toString() in a later file is "async classifyAction() { return verdict; }"; with the fix it is the real implementation.

Fix. The mock was never needed. classifyAction reaches the network only through globalThis.fetch, and the session model there is sarvam-105b (a pier model, not BYOK), so stubbing fetch drives the real function down the bridge path — the technique autoClassify.test.ts already uses. An afterAll restores fetch and the two env vars, so the file leaks nothing itself. Same class of bug, and same remedy, as 2ea24fc.

Part 2 (94ebc3b) — the follow-on failure. Removing the mock fixed the 7 downstream tests but broke the two verdict tests in autoTurn.test.ts itself, again only on CI. loadAuth() memoizes into a module-level cache; this file writes a pier_auth.json into a temp PIER_HOME at module scope, but ten files run before it in CI order and any of them that resolves auth first caches null from its own empty home. requireToken() then throws on that stale null, classifyAction fail-safes to prompt, and both tests saw the user being prompted instead of the classifier deciding. resetAuthCache() after setting PIER_HOME fixes it — the same thing autoClassify.test.ts already does — with a second reset in afterAll so the temp home's token does not linger.

Verification. Both fixes were checked in a linux/amd64 oven/bun:1.3.13 container — CI's platform and Bun version — not just on macOS, since every failure in this pair was ordering- and platform-dependent and passed locally throughout: 956 pass, 2 skip, 0 fail, tsc clean. Both CI checks on the PR are green.

Reasoning bank (8c4adf3)

An opt-in memory of generalized strategy lessons ("when the suite hangs, check the stack-size env first"), distinct from CLAUDE.md/AGENTS.md project memory, which holds codebase facts and stays always-on. Off by default behind the reasoningBank config key while the retrieval loop is validated.

  • Lessons are one markdown file each under $PIER_HOME/memory, capped at 50 and evicted by net usefulness (hits - misses) — files stay the source of truth so a user can read and rm a bad lesson by hand.
  • The system prompt carries an index (id/title/description), never bodies; the model is the retriever (no embeddings) and pulls a body on demand via MemoryRecall, which doubles as the relevance signal.
  • Outcome labelling is local-only — tool errors, a user correction, /undo, test exit — with no LLM judge, and biased to unknown, since a mislabelled lesson is worse than none.
  • Distillation is one constrained model call at teardown (max 3 lessons/session) over a compacted transcript; the no-literal-strings rule is enforced in code, because a global bank injects repo-A lessons into repo B.
  • Consolidation supersedes near-duplicates and prunes lessons the model keeps declining, so the bank does not depend on the distiller being right every time.
  • The main session, exec, and subagents all read the bank but never write to it — only the main session sees the outcome signals that label a lesson. /memory list|show|forget|clear manages it; bare /memory keeps its AGENTS.md meaning.

Sibling sessions (584ca33)

ListAgents / SendMessage let pier sessions in different terminals on the same machine discover and message each other — e.g. ask which files a peer owns before working in the same repo.

  • Discovery is a PID registry ($PIER_HOME/sessions/<pid>.json), valid because one-session-per-process is already an invariant; stale entries are reaped via process.kill(pid, 0) behind a strict /^\d+\.json$/ filename guard.
  • Each session listens on $PIER_HOME/socks/<pid>.sock; a send is one NDJSON line plus a one-line ack — fire-and-forget, no correlation id, and a "reply" is just an independent send back.
  • An inbound message enters the conversation at the next safe boundary (steered into a running turn, or a fresh turn when idle). The REPL renders it as an attributed peer cell, skips @-mention resolution so a peer's paths cannot inject local files, and keeps it out of prompt history.
  • Identity comes from the thread title, with --name overriding; TodoWrite/UpdatePlan publish the in-progress item as "working on: …". A <sibling-sessions> roster is rebuilt into <env> on every model call.
  • ListAgents is read-only; SendMessage is allowlisted for auto-approval — a same-machine send stays inside the machine's trust boundary and keeps message text out of the bridge classifier. Unix sockets only: every entry point no-ops on win32.

Commit splitting

session.ts, turn.ts and store.ts were split at the hunk level so the fallback-map change, skill-hint injection, subagent emit plumbing, and todo_update event each landed separately.

Cowork, the tool allowlist and the capability subtypes stayed in one commit (472ae32) — their changes interleave inside the same hunks (SessionController.create, InitializeRequestSchema, the initialize handler), so separating them would mean intermediate commits referencing opts.tools before it is declared, i.e. commits that don't compile.

Version

Title says v1.6.0, but nothing in the tree encodes it — package.json is the 0.0.0-dev sentinel and versions come from release tags. No version bump is included here; tag after merge.

porcelaincode and others added 21 commits August 5, 2026 01:32
sarvam-30b was deprecated upstream, leaving sarvam-105b as Sarvam's only
served model. Regenerate models_catalog.json without it and fix up every
place that assumed a second, cheaper Sarvam sibling existed:

- BYOK provider shortlist + validation probe now use sarvam-105b.
- cheapCompletion has no cheap Sarvam tier anymore; callers already
  swallow failures to '' and fall back to deterministic paths.
- the fallback map points sarvam-105b at pier-hybrid, so a Sarvam-side
  outage lands on a different upstream instead of a dead sibling.
- compact.test drops its dependence on sarvam-30b's
  auto_compact_token_limit (no catalog model sets one now) and exercises
  the context_window * 0.9 fallback instead.

Co-authored-by: Pier Code <no-reply@piercode.com>
… skills and MCP

Plugins live in $PIER_HOME/plugins (plus cowork_plugins for the cowork
surface) and contribute content dirs that the existing loaders now scan:

- `pier plugin install|list|remove|enable|disable` (src/commands/pluginCli.ts).
- custom commands from a plugin are namespaced `<plugin>:<name>` so they
  can never collide with user or project commands.
- agents and skills scan plugin dirs between user and project, keeping
  project > plugin > user precedence on a name collision.
- AgentDef carries the source `path` for disk-loaded agents, so the
  capability inventory can attribute an agent to its origin.

Co-authored-by: Pier Code <no-reply@piercode.com>
Ship docx/xlsx/pptx/pdf authoring skills with the binary and seed them
into $PIER_HOME/skills on session init, so they are discoverable like any
user skill. Seeding is idempotent and never clobbers a user-modified copy.

assets/skills/** is the source of truth; src/skills/bundled.ts is
generated from it by scripts/gen-bundled-skills.ts.

Skills may also declare `when:` activation globs in frontmatter. When a
user message references a matching filename, the turn injects a hint that
the skill exists — hint only, backed by a cheap filesystem scan.

Co-authored-by: Pier Code <no-reply@piercode.com>
…that server

A permission rule naming just `mcp__gmail` now matches `mcp__gmail__*`,
so a whole connector can be allowed or denied in one rule instead of one
rule per tool.

Matched as a string prefix on the RULE — never by re-splitting the tool
name on `__`, since server names may themselves contain underscores. A
rule for `mcp__gmail` must not shadow `mcp__gmail_backup__send`.

Co-authored-by: Pier Code <no-reply@piercode.com>
…upport

Remote (http) MCP servers can now authenticate:

- a passive auth provider attaches stored OAuth tokens and refreshes them
  silently; when a server demands a NEW interactive grant it throws
  NeedsAuthError rather than opening a browser mid-init.
- `pier mcp auth <name>` runs the interactive flow against a loopback
  listener and stores the tokens.
- configured `headers` now ride every HTTP request via requestInit —
  without this, header-authenticated servers 401.

The manager also tracks per-server state (connected / failed / disabled /
needs_auth) plus discovered tools, exposed via snapshot() for capability
queries and connector UIs, and honors a `disabled` flag in .mcp.json.

Co-authored-by: Pier Code <no-reply@piercode.com>
…sting

Task subagents were opaque: the parent saw one tool_begin and nothing
until the sub-turn finished. Tools now receive an optional ToolContext
emit sink (wired by the serve loop; unset in TUI/exec), and the Task tool
streams subagent_begin / subagent_tool / subagent_delta / subagent_end
events correlated by a fresh subagent_id. Text deltas are coalesced on a
250ms window so a chatty sub-model can't flood the parent's stream, and
tool events carry one-line summaries only — payloads stay in the
sub-session.

Two guards come with it:

- depth: sub-agents may not spawn sub-agents. Unbounded nesting
  multiplies model calls invisibly while the parent still sees a single
  tool_begin, so a nested Task is rejected with guidance to do the work
  directly.
- concurrency: a counting semaphore caps concurrent subagents
  (config subagentConcurrency, default 4). Over-cap calls queue FIFO
  rather than failing, so a parallel tool batch executes in waves.

Co-authored-by: Pier Code <no-reply@piercode.com>
… the thread

runTurn now yields a todo_update event whenever a todo tool succeeds
(both tools return the updated list in res.data.todos), so an embedder
can render a live task list without scraping tool output.

The thread record gains two fields, persisted together since they share
the same record and persistSession literal:

- `todos`: the last task list, restored on resume.
- `surface`: which client surface owns the thread ('chat' | 'code' |
  'cowork'), so listings can route a resume back to the right UI. The
  surface sticks once set — a resume that omits the field must not
  strip it.

Co-authored-by: Pier Code <no-reply@piercode.com>
…urns

Extract the dynamic (per-cwd) tool set — the Skill tool plus connected
MCP tools — into buildDynamicTools, and expose reloadCapabilities() to
close the existing MCP connections, rebuild the set, and re-register it.

Safe ONLY between turns: dynamic tools are process-global under the
one-session-per-process invariant, so swapping them mid-turn would change
the tool set under a running model call. Callers must reject while a turn
is running.

initSession also seeds the bundled office skills and returns the live
McpManager so callers can query per-server status.

Co-authored-by: Pier Code <no-reply@piercode.com>
… control protocol

Introduces the desktop's agentic knowledge-work surface and the protocol
additions its client needs. Kept as one change because the session
surface, the tool allowlist and the new control subtypes are threaded
through the same initialize path.

Cowork surface:
- `surface` ('chat' | 'code' | 'cowork') on initialize, persisted to the
  thread and inherited on resume unless re-stated.
- cowork appends the knowledge-work prompt overlay, defaults the sandbox
  to workspace-write + network, and clamps danger-full-access back to
  workspace-write — the OS sandbox stays the backstop.
- cowork requires an explicitly trusted folder: an untrusted cwd fails
  with 'untrusted_folder' so the client can run its trust-grant flow.

Tool allowlist:
- `tools` on initialize restricts which tools are offered and callable.
  An empty array is meaningful and is NOT the same as omitting the field:
  `[]` means no tools at all (the folderless chat mode), absent means the
  full set for the mode. serve uses `??`, never `||`, so `[]` survives.

New control subtypes (all additive, feature-detected via
InitializeResponse.capabilities; older servers answer "unsupported
subtype", which clients read as absence):
get_trust, set_trust, get_capabilities, expand_command,
reload_capabilities, set_tool_policy, mcp_authenticate.

Also: custom slash commands expand server-side, so `/name args` works as
a session's first message without a round trip; mcp_authenticate is
deliberately not awaited inline, since awaiting a browser flow would
wedge the inbound frame reader.

Co-authored-by: Pier Code <no-reply@piercode.com>
Thread sync becomes a real two-way mirror instead of local-wins:
- Guarded pushes with an expected_count cursor so two devices can't
  interleave history (bridge 409s → reconcile by item-id prefix).
- reconcileThread merges local vs server history: remote-extension
  adopts server, local-extension keeps local, divergence keeps the
  longer side and preserves the loser as a conflict copy.
- Outbox: failed pushes stamp pendingPush, retried at boot.
- Per-thread /sync off opt-out; /share, /unshare, /sessions un-gated.

Session sharing gets a reconnect supervisor + takeover:
- HostShare.rebind re-announces and re-offers outstanding approvals on
  a transport drop; UUID request ids; /unshare vs transport-drop split.
- Repl reconnect loop with backoff; serve --share auto-mirrors via a
  TeeIO; work_diff streamed to remote Changes panels.

New controls and commands:
- serve-protocol `rewind` (truncate history + durable bridge copy).
- `pier adopt <id>` (cloud→laptop handoff) and `pier devices`.
- set_model / end_session host controls.

Mid-turn steering: queued messages are handed to the running turn at
the next tool-loop boundary (steering_injected retires the chip by
text), instead of waiting for the whole turn to resolve.

Co-Authored-By: Pier Code <no-reply@piercode.com>
useConst on a never-reassigned let, and biome-format two functions
flagged after the sync/steering commit.
Companion to the bridge fix. In a live session Auto mode blocked every network
command ("This exact command already ran…") when none had ever executed, and
the agent burned ~12 turns rewording the same git push before the user
interrupted.

The blocking itself came from a bridge-side short-circuit, but two problems
live here:

1. The escape hatch never fired. MAX_REPEATED_BLOCKS=3 was added for exactly
   this case ("the git push duplicate loop"), but it keyed on exact equality of
   the action command. A blocked agent rewords its retry — which the block
   message actively invited — so lastBlockedAction reset to 1 every iteration
   and the turn spun to max_iterations without ever asking the user. It now
   counts consecutive Auto-mode blocks regardless of the command text and
   resets on progress (a classifier allow, or any tool that actually ran), so
   rephrasing is irrelevant. The hatch's outcome is a user prompt, never an
   auto-approve, so firing more often is strictly safer. The block message now
   states the action did not run and tells the agent not to retry a reworded
   variant.

2. BYOK had no repeat handling at all, despite localClassify.ts documenting
   itself as a faithful port kept in sync with the Go classifier. The two paths
   had silently diverged on a load-bearing security behavior.

detectRepeat now lives in safety/autoClassify.ts as the single source of truth
for both paths, with the Go side mirroring it. A prior call counts as executed
only when it has a paired, non-error result: blocked attempts are marked
is_error (rendered "[error] " by flattenMessage) and never count, and a match
with no result yet is not "already ran" either — that is the parallel-batch
window, where results for a whole batch are appended only after it completes,
which is exactly when Auto mode classifies. Matching reads the parsed command
field and ignores the agent-authored description.

For BYOK, buildClassifierMessages now renders the same advisory NOTE as the
bridge, byte-for-byte, and only for a genuinely executed repeat.

Tests: a shared fixture (test/fixtures/repeat-detection.json) that both this
implementation and the Go mirror must satisfy; a regression test proving
reworded retries still reach the user prompt; and classifierParity.test.ts
pinning system-prompt byte-identity and NOTE shape across the two languages, so
the drift that produced the BYOK gap cannot recur silently. Each new test was
verified to fail against the reintroduced old behavior.

Co-Authored-By: Pier Code <no-reply@piercode.com>
Formatting only, no behavior change. The Auto-mode repeat-detection
commit landed without running the formatter, so CI's lint job failed on
both the push and the PR check.

Three call sites collapse onto one line (detectRepeat's signature, a
buildClassifierMessages call, repeatNote's return) and two useTemplate
findings fold concatenation into a single template literal.

Both folded strings are byte-identical to before — repeatNote's is
pinned by the Go parity fixture, and turn.ts's is the Auto-mode block
message.

Co-Authored-By: Pier Code <no-reply@piercode.com>
`bun test` runs all 126 files in ONE process and `mock.module` is
process-global with no unwind, so the `../src/bridge/models.js` stub in
outputFormat.test.ts (file #6 in CI order) replaced the real module for
every file after it. Later tests resolved a 1-model catalog
(`sarvam-105b`, no `pier-hybrid`) instead of the 15-model bundled one.

That silently broke 4 tests on a clean machine — 3 in mentorCommand
("Unknown model: pier-hybrid") and hydrateRemoteThread in discovery. The
runner proved it: `fetchModels.toString()` there was the stub body, with
`readCache() = null` and localhost:9000 unreachable, so the bundled
fallback (15 models, has pier-hybrid) could never be reached.

It reproduced only on CI because a populated ~/.pier-bun disk cache masks
it locally, and it only surfaced now because the Test step had been
skipped behind a failing Lint step in every prior run.

The stub was never needed: it only existed to keep runExec offline, and
an empty PIER_HOME plus a dead PIER_BASE_URL already forces the bundled
catalog, which resolves `sarvam-105b` fine. Drops the mock, so nothing
leaks. Also folds the duplicated `_mkdtemp`/`_join`/`_tmp` aliases into
the now top-level node imports.

Co-Authored-By: Pier Code <no-reply@piercode.com>
Second, independent instance of the same class of bug. `syncEnabled` in
threads/bridge.ts memoizes for the whole process and `disableThreadSync()`
latches it to false permanently. forkThread.test.ts (#52) and
threads.test.ts (#94) both call it at MODULE scope, so by the time
discovery.test.ts (#116) runs, sync is off for reasons that have nothing
to do with discovery.

loadRemoteThread then short-circuits on `!threadSyncOn()` and
hydrateRemoteThread returns null without ever issuing a request — which
is why the runner failed it in 3ms, far too fast for the local
Bun.serve round-trip it was supposed to make.

Adds resetThreadSyncCache() (tests only, mirrors resetModelsCache) and
calls it in the test so the assertion no longer depends on which files
ran before it.

Co-Authored-By: Pier Code <no-reply@piercode.com>
The test polled until the output contained `done`, then immediately
asserted the status was `completed`. Those are two different events: the
write being flushed does not mean the child has exited and been reaped,
so on a slower runner the next read still returned
`<status>running</status>` and the assertion failed.

Flaky rather than broken — the same commit passed the preceding CI run
and failed the next. Polls for the status transition (same 6s budget as
the loop above) so it waits for the thing it actually asserts.

Co-Authored-By: Pier Code <no-reply@piercode.com>
Fix the three CI-only test failures uncovered once the lint gate stopped
skipping the Test step: a process-global mock.module leak, a memoized
thread-sync latch, and a bg-shell status race.

Co-Authored-By: Pier Code <no-reply@piercode.com>
…ions

Adds an opt-in memory of *generalized* lessons ("when the suite hangs, check
the stack-size env first"), distinct from CLAUDE.md/AGENTS.md project memory,
which stores codebase facts and stays always-on.

src/memory/:
  types    lesson shape; description states when a lesson does NOT apply
  store    one markdown file per lesson under $PIER_HOME/memory, capped at 50
           and evicted by net usefulness (hits - misses); files are the source
           of truth so a user can read and rm a bad lesson by hand
  signals  per-turn outcome labelling from LOCAL state only — tool errors, a
           user correction, /undo, test exit — no LLM judge; biased to
           `unknown` because a mislabelled lesson is worse than none
  inject   injects an INDEX (id/title/description), never bodies; the model is
           the retriever, so no embeddings. The activating imperative lives in
           a per-turn reminder, not this block — measured twice, an imperative
           16 KB back in the system prompt is skipped by both models
  distill  one constrained callModel at teardown, max 3 lessons/session, on a
           compacted transcript; the no-literal-strings rule is enforced in
           code, since a global bank injects repo-A lessons into repo B
  consolidate  supersede near-duplicates and prune lessons the model keeps
           declining — fixes ReasoningBank's append-only rot
  flush    session-end wiring at the controller turn boundary + teardown, not
           SessionEnd (misses exec/serve/crashes) or the debounced persist path

Wiring: MemoryRecall tool pulls a body on demand and records the hit; the
index is injected for the main session, exec, and subagents — all of which
READ but never WRITE, since only the main session sees the outcome signals
that label a lesson. `/memory list|show|forget|clear` manages the bank; bare
`/memory` keeps its AGENTS.md meaning. Off by default behind `reasoningBank`
while the retrieval loop is validated.

Tests: test/reasoningBank.test.ts, test/reasoningBankWrite.test.ts.

Co-Authored-By: Pier Code <no-reply@piercode.com>
… each other

Adds ListAgents / SendMessage so sessions in different terminals can coordinate
— e.g. ask which files a peer owns before working in the same repo.

engine/siblings.ts: discovery is a PID registry ($PIER_HOME/sessions/<pid>.json)
rather than a socket-dir scan, valid because one-session-per-process is already
an invariant. Each session listens on $PIER_HOME/socks/<pid>.sock; a send is one
NDJSON line plus a one-line ack — fire-and-forget, no correlation id, and a
"reply" is just an independent send back to the sender's address. Stale entries
are reaped in listSiblings() via process.kill(pid, 0), with a strict
/^\d+\.json$/ filename guard. Unix sockets only: every entry point no-ops on
win32 and the tools are not registered there.

Delivery: an inbound message enters the conversation at the next safe boundary
— steered into a running turn, or a fresh turn when idle. The REPL renders it
as an attributed peer cell (never as this session's own user), skips @-mention
resolution so a peer's paths can't inject local files, and keeps it out of
prompt history; rehydrate replays it the same way.

Identity: init registers the session with cwd, launch patches in threadId/model,
the auto-derived thread title publishes as the display name, and `--name` sets
a user name that wins. TodoWrite/UpdatePlan publish the in-progress item as
"working on: …". A <sibling-sessions> roster is rebuilt into <env> on every
model call, so it tracks sessions starting and exiting mid-conversation.

Safety: ListAgents is read-only; SendMessage is allowlisted for auto-approval in
every mode — a same-machine send stays inside the machine's trust boundary, the
receiver renders it clearly attributed, and this keeps message text out of the
bridge classifier.

Tests: test/siblings.test.ts, plus a tools.test.ts registry assertion.

Co-Authored-By: Pier Code <no-reply@piercode.com>
…suites

`bun test` runs all 129 files in ONE process and `mock.module` is
process-global with no unwind, so the `../src/safety/autoClassify.js` stub
in autoTurn.test.ts (file #11 in CI order) replaced the real module for
every file after it. `classifyAction` became `async () => verdict` —
returning whatever verdict the last autoTurn test happened to leave behind
(`block` / "network command").

That failed 7 tests downstream: 6 in autoClassify.test.ts (#125) and the
routing test in localClassify.test.ts (#49). Each one calls the real
`classifyAction`, so each got the canned `block` and never reached its own
fetch stub — hence `lastBody` staying null in the transcript assertions.
The tests asserting `classifyActionLocal` directly kept passing, which is
what localised the leak to the mocked module.

It reproduced only on CI because the leak depends on whether a victim file
resolves its `autoClassify` import before or after this file installs the
mock — static top-level imports can win the race, dynamic
`await import()` inside a test body never does. Proven directly rather than
by inference: with the mock in place, `classifyAction.toString()` in a later
file is `"async classifyAction() { return verdict; }"`; with this change it
is the real implementation.

The mock was never needed. `classifyAction` reaches the network only through
`globalThis.fetch`, and the session model here is `sarvam-105b` (a `pier`
model, not BYOK), so stubbing fetch drives the REAL function down the bridge
path — the same technique autoClassify.test.ts already uses. An `afterAll`
restores `fetch` and the two env vars, so this file now leaks nothing itself
(verified with a probe).

Co-Authored-By: Pier Code <no-reply@piercode.com>
Follow-up to 37e4c62, which swapped autoTurn's leaking `mock.module` for a
`fetch` stub driving the real `classifyAction`. That fixed the 7 downstream
failures but broke the two verdict tests in this file on CI.

`loadAuth()` memoizes into a module-level cache. This file writes a
`pier_auth.json` into a temp PIER_HOME at module scope, but ten files run
before it in CI order, and any of them that resolves auth first caches
`null` from its own empty home. `requireToken()` then throws on that stale
null, `classifyAction` fail-safes to `prompt`, and both tests saw the user
being prompted instead of the classifier deciding.

It passed on macOS purely because the local file order left the cache
unpopulated. Reproduced deterministically by loading auth in a file
scheduled before this one, which reproduced both failures exactly;
`resetAuthCache()` after setting PIER_HOME fixes them, mirroring what
autoClassify.test.ts already does. `afterAll` resets again so the temp
home's token does not linger for later files.

Verified in a linux/amd64 oven/bun:1.3.13 container (CI's platform and Bun
version) rather than only on macOS: 956 pass, 2 skip, 0 fail, and tsc
clean.

Co-Authored-By: Pier Code <no-reply@piercode.com>
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.

1 participant