pier-setup 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +34 -31
  2. package/install.mjs +76 -106
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -19,18 +19,21 @@ pier ships as **two halves**, installed separately:
19
19
  | Half | Package | Role |
20
20
  |---|---|---|
21
21
  | **pi extension** | `packages/pier-ext` (npm: [`pi-pier`](https://www.npmjs.com/package/pi-pier), [pi.dev gallery](https://pi.dev/packages/pi-pier)) | Injects `todo_write` / `subagent` / `terminal` / `ask_user_question` tools into a pi session, and reports pane state over the herdr socket API |
22
- | **herdr plugin** | `packages/pier-workbench` (`pier.workbench`) | Workspace bootstrap, blocked human-gate notifications, focus heat layout (focused pane grows in place) |
22
+ | **herdr plugin** | `packages/pier-workbench` (`pier.workbench`) | Blocked human-gate notifications, focus heat layout (focused pane grows in place), ops dashboard + sidebar agent view |
23
23
 
24
24
  ### Highlights
25
25
 
26
- - **Todo loop**: `todo_write` with full-replace semantics, session JSONL as source of truth, correct branch rollback; live pane-title projection `▶i ○p ■b ✓c (N/M) · current task`; TUI widget is an activity-anchored window (the in_progress entry and its surrounding context stay visible as work moves down the list); `/todos` command (including unblock)
26
+ - **Todo loop**: `todo_write` with full-replace semantics, session JSONL as source of truth, correct branch rollback; live pane-title projection `▶i ○p ■b ✓c (N/M) · current task`; `/todos` command (including unblock)
27
27
  - **Todo anti-freeze**: an all-completed list unchanged for ≥6 turns flips the read hook from recitation to a rewrite warning; after ≥1h it archives — first offering one rewrite window with old entries as reference, then (if ignored) a final notice that clears the list (rm persisted to JSONL; the empty guard takes over — multi-step work must re-track); `/todos` still shows history, session JSONL untouched
28
28
  - **Interactive subagents**: every subagent = an isolated pi session in its own pane (separate context window); **a human can enter that pane and talk to it directly** (fix bugs, take over, answer ask_user_question)
29
+ - **Delegation identity** (D111): spawn hands the worker a `PI_HERDR_DELEGATION` context (parent pane/session, taskId, task, role), replayed env > worker marker > branch fold; the worker's system prompt gains a `pier-delegation` section (identity + one stable delivery contract), its transcript opens with an "⟵ delegated by master …" card, and its pane title carries an `↳` prefix; `pi-parent`/`pi-task` herdr tokens ride their own report batch, the dashboard groups workers under their master, and blocked alerts name the edge (`worker ← master`)
30
+ - **Task slugs + worker progress reports** (D112): spawn allocates a task slug (numbered on collision); tool outputs print `slug@paneId` and `agentId` accepts the bare slug, bare pane id, or the combined form (the slug part survives a revive that changed the pane); a worker-only `report_progress` tool pipes checkpoints into the master's context as a display-less custom message — no chat flood, no forced turn
31
+ - **Unified TUI status strip** (D113/D114): the resident widget is one line per agent — self as `▶ current item ✓done/total`, each running subagent as `↳ slug ▶ current item ✓d/n` (blocked shows the task + reason; settled rows never occupy the strip; a fully completed self list hides the widget instead of showing a bare `✓N/N`); workers push their full todo list over the pipe on every `todo_write` (event-driven, no polling), and `/pier-tree` opens the complete delegation tree (your todos + every worker's full list + settled nodes) in the shared Pier **popup** — the same singleton panel as `/dashboard`, opened on its `Tree` tab (inside the popup, `1`/`2`/`Tab` switch views; an explicit `/pier-tree overlay` keeps the independent full-window pane; non-Herdr falls back to the in-pane notify view); `/dashboard` opens the same panel on its `Dashboard` tab
29
32
  - **Subagent output preview**: `subagent(action: "output", agentId)` returns what a running background subagent has printed since the previous call — an in-memory per-pane cursor computes the delta (`agent.read`, falling back to `pane.read` on older herdr), and the result carries status, revision, truncation and a `restart` flag for scrolled/cleared buffers, so the master can notice a stuck worker instead of waiting for settlement
30
33
  - **Soft write-locks**: per-pane lock beacons on `write`/`edit` paths; cross-pane conflicts warn (default) or block (`PI_HERDR_WRITE_LOCK=1`). The warning lists every holder; `/locks` is the human view of the same table (`herdr agent list` exposes the raw tokens to agents)
31
34
  - **Role profiles**: built-in `master` / `worker-default`; custom roles as `.pi-herdr/roles/<name>.json`; toolset converges per role (deny rules are not bypassable)
32
35
  - **Human gate**: subagent `ask_user_question` → sidebar blocked marker + notification; manual takeover (ESC-interrupt then typing) is heuristically detected so the master pauses/returns management automatically
33
- - **Ask picker**: a `multi: true` question renders as an interactive toggle list (space toggles a row, `a` toggles all, enter confirms, esc declines, live selection count) with the free-text row pinned last; `allowOther: false` gives pure choice. The typed-index prompt remains the fallback for RPC mode and older pi
36
+ - **Ask picker**: every choice question renders through one pier-owned picker in the TUI — single-select (arrows + enter/space pick the highlighted row) and `multi: true` toggle list (checkboxes: space toggles, `a` toggles all, live selection count) share the same framed chrome, `recommended` seeds the cursor (and pre-checks in multi), and the free-text row stays last; `allowOther: false` gives pure choice. The host select dialog and typed-index prompts remain the fallbacks for RPC mode and older pi
34
37
  - **Blocking dialogs mark the pane**: pi's `ui_prompt_start` / `ui_prompt_end` events cover the inherently blocking `ctx.ui` dialogs (`select` / `confirm` / `input` / `editor`), so a dialog from *any* extension marks the pane blocked and emits the `herdr:blocked` edge; nested gates coalesce, and the ask tool no longer needs its 5s refresh heartbeat. `custom` is deliberately excluded — pi routes resident overlays through it too (pier's own slim-frame overlay never calls `done()`), which used to leave a working pane marked blocked for the whole session
35
38
  - **Role gate stops early**: a denied worker tool call returns `terminate`, so a batch whose results are all terminating ends without another model round trip
36
39
  - **Transcript cards**: pier's own session entries (todo edits, subagent/terminal registries, role manifests, soft approvals) and its reminder messages render as compact cards instead of raw JSON — entry/message renderers, no pi-tui dependency
@@ -45,8 +48,8 @@ pier ships as **two halves**, installed separately:
45
48
  ### Requirements
46
49
 
47
50
  - Node ≥ 22
48
- - pi ≥ 0.84.4 (`@earendil-works/pi-coding-agent`)
49
- - herdr ≥ 0.9.0 (macOS / Linux / Windows; Windows is preview beta)
51
+ - pi ≥ 1.3.0 (`@earendil-works/pi-coding-agent`) — `before_agent_start.messageType` and resumed-session active tools
52
+ - herdr ≥ 0.9.5 (macOS / Linux / Windows; Windows is preview beta) — native `pane.focused`, self-reported resume, and shutdown-safe session saves
50
53
 
51
54
  ### One-shot install (recommended)
52
55
 
@@ -56,7 +59,7 @@ No clone needed — run the npm package directly:
56
59
  npx pier-setup@latest # user mode: pi install npm:pi-pier + herdr plugin install
57
60
  npx pier-setup@latest version # local vs npm latest (installer / pi-pier / herdr plugin)
58
61
  npx pier-setup@latest update # refresh both halves in place (does not uninstall first)
59
- npx pier-setup@latest uninstall # --purge also drops boot-config.json
62
+ npx pier-setup@latest uninstall
60
63
 
61
64
  npm i -g pier-setup # or install globally, then: pier-setup / version / update
62
65
  ```
@@ -70,15 +73,13 @@ Dev mode still requires cloning the repo:
70
73
  git clone https://github.com/July24/pier && cd pier
71
74
  node install.mjs install --dev # local-path pi install + herdr plugin link; code changes are live
72
75
  node install.mjs version --dev
73
- node install.mjs update --dev # rewrite boot-config only; pull the repo yourself
76
+ node install.mjs update --dev # re-check env + versions; pull the repo yourself
74
77
  ```
75
78
 
76
- The script verifies the environment (node / pi / herdr versions), probes pi's node
77
- and cli.js absolute paths, generates boot-config.json (user mode stores it in the
78
- herdr plugin config dir, so reinstalls don't lose it), and registers both halves.
79
+ The script verifies the environment (node / pi / herdr versions) and registers both halves.
79
80
 
80
81
  `update` re-runs `pi update npm:pi-pier` (falls back to `pi install`) and
81
- `herdr plugin install … --yes`, then rewrites boot-config. Override sources with
82
+ `herdr plugin install … --yes`. Override sources with
82
83
  `--pi-spec=` / `--herdr-spec=` (npm publishing or forks). `pier-setup --help`
83
84
  lists every command.
84
85
 
@@ -131,15 +132,7 @@ pi overwrites tools/commands by name; event listeners stack. Two todo or subagen
131
132
  **Designed coexistence:** herdr’s official `herdr:pi` reporter. Keep it. pier emits `herdr:blocked` so that plugin remains lifecycle authority.
132
133
 
133
134
  - **other agents inside herdr (claude code / codex etc.)**: tabs without a pi pane
134
- are excluded from heat reflow; blocked notifications are pi-only; master-tab
135
- auto-bootstrap can be disabled with `autoBootstrap: false` in boot-config.
136
-
137
- ### Bootstrap config (workbench half)
138
-
139
- Master-tab bootstrap needs local node / pi paths: `pier-setup` generates
140
- them automatically; manually, copy `packages/pier-workbench/scripts/boot-config.example.json`
141
- (placeholders for both macOS and Windows). User mode reads it from
142
- `herdr plugin config-dir pier.workbench`; dev mode from `packages/pier-workbench/scripts/boot-config.json`.
135
+ are excluded from heat reflow; blocked notifications are pi-only.
143
136
 
144
137
  ### Usage
145
138
 
@@ -153,7 +146,7 @@ them automatically; manually, copy `packages/pier-workbench/scripts/boot-config.
153
146
  ```
154
147
  packages/
155
148
  pier-ext/ # pi extension (npm: pi-pier): todo/subagent tools, herdr client, vocab authority, skill
156
- pier-workbench/ # herdr plugin (pier.workbench): workspace bootstrap + blocked notify + heat layout
149
+ pier-workbench/ # herdr plugin (pier.workbench): blocked notify + heat layout + ops dashboard
157
150
  docs/ # install guide, configuration overview, role profile docs, sidebar role config
158
151
  ```
159
152
 
@@ -161,7 +154,7 @@ docs/ # install guide, configuration overview, role profile docs, s
161
154
 
162
155
  ```sh
163
156
  npm install --ignore-scripts
164
- npm test # node --test, 809 unit tests (planner / todo replay / anti-freeze staleness / session tail / GC / lifecycle / renderers / ask picker / subagent output / jev decision layer: diagnostic gate, notice ranking, excerpt windows)
157
+ npm test # node --test, 733 unit tests (planner / todo replay / anti-freeze staleness / session tail / GC / lifecycle / renderers / ask picker / subagent output / jev decision layer: diagnostic gate, notice ranking, excerpt windows)
165
158
  ```
166
159
 
167
160
  ## Configuration
@@ -179,9 +172,8 @@ Runtime policies and operational limits are centralized in `runtime-policy.ts` (
179
172
  | `PIER_READY_TIMEOUT_MS` | `90000` | ms | Subagent pane pipe readiness wait (exponential backoff; a pane that exited fails immediately with its last output attached) |
180
173
  | `PIER_SESSION_TTL_SECONDS` | `600` | s | Session retention TTL after subagent exit before GC cleanup |
181
174
  | `PIER_GIT_TIMEOUT_MS` | `10000` | ms | Execution timeout for git operations (worktree creation, diff summary, cleanup) |
182
- | `PIER_FOCUS_POLL_MS` | `1500` / `0` | ms | Focus sampling for heat layout (`0` disables). Default 1500ms on Herdr <0.9.1; 0 (event-first) on 0.9.1+ |
183
175
  | `PIER_TERM_READ_MAX` | `8000` | chars | Maximum terminal buffer characters read per operation |
184
- | `PIER_TERM_IDLE_MS` | `1800000` | ms | Idle time before pier nudges about an open terminal |
176
+ | `PIER_TERM_IDLE_MS` | `300000` | ms | Idle time before the second terminal-cleanup nudge (one is sent at turn end for a terminal used that turn) |
185
177
  | `PIER_TODO_GRACE_MS` | `30000` | ms | Settle grace before the unfinished-todo reminder |
186
178
  | `PIER_TRACE` | – | flag | Write diagnostics (tool renderers, swallowed errors) to stderr |
187
179
  | `PIER_JEV_ENABLE` | `0` | flag | Jev decision layer (TypeSafe System One classification) master switch; every call site fails open — see `docs/rfc-jev-integration.md` |
@@ -199,15 +191,26 @@ processes stay as they are (`PI_HERDR_SUBAGENT`, `PI_HERDR_ROLE_MANIFEST`, `PI_H
199
191
  `/pier-config doctor` lists every option with its effective value and where it came from, plus the
200
192
  errors pier deliberately swallowed this session.
201
193
 
202
- **Focus heat:** Herdr 0.9.0 resolved mouse focus in the client and did not deliver `pane.focused`
203
- to plugins, so each pane sampled `layout.export → focused_pane_id` and replayed the workbench
204
- event. Herdr 0.9.1+ delivers `pane.focused` natively, so the poller defaults off. Override with
205
- `PIER_FOCUS_POLL_MS`; `PIER_WORKBENCH_ROOT` points at a relocated plugin checkout.
194
+ **Focus heat:** Herdr 0.9.5 delivers `pane.focused` natively, so pier starts no per-pane sampler;
195
+ the workbench receives the event directly. `PIER_WORKBENCH_ROOT` points at a relocated plugin checkout.
196
+
197
+ **Upstream tracking:** pi 1.3 / herdr 0.9.5 impact, version-floor cleanup, and the next extension
198
+ directions (remote machines, native worktrees, plugin actions):
199
+ **[docs/rfc-upstream-absorption-2026-10.md](docs/rfc-upstream-absorption-2026-10.md)**.
206
200
 
207
201
  ### Efficiency mechanisms (D100–D103, opt-in)
208
202
 
209
203
  Three token-saving mechanisms ship **disabled by default** and are safe to try one at a time:
210
204
 
205
+ **Origin — credit where due**: the mechanism ideas and names come from NVIDIA's
206
+ [SoL-Pi](https://github.com/NVlabs/SoL-Pi) ([paper](https://arxiv.org/abs/2609.20519) ·
207
+ [blog](https://nvlabs.github.io/SoL-Pi/)) — four harness-level efficiency mechanisms surfaced by
208
+ scaled auto-research loops. pier adopts three of them (ObservationPack / EPR / OCC; the fourth,
209
+ Action Fusion, is not adopted) and re-implements them on its own stack, extending the originals
210
+ with a jev decision layer (small System One classifications at the diagnostic gate, excerpt-window
211
+ pick, and settle attribution — [rfc-jev-integration.md](docs/rfc-jev-integration.md)), role gating,
212
+ and the 4-plane efficiency config with per-mechanism JSONL telemetry (D100–D103).
213
+
211
214
  - **ObservationPack** — large tool outputs are projected as placeholders after the first `fullSends` requests, with paged retrieval via `obs_recall` (session JSONL stays untouched).
212
215
  - **EPR** — long diagnostic (`bash` test/build) logs are reduced in-process to a byte-verified evidence receipt; the raw log is archived first and stays readable.
213
216
  - **OCC** — `todo_write` boundaries drive pi's native compaction under a KV-cache cost model, carrying unfinished tasks across the compaction.
@@ -218,10 +221,10 @@ PI_HERDR_OBS_PACK_ENABLE=1 PI_HERDR_OBS_PACK_LOG=1 pi # start here
218
221
 
219
222
  Full quick start, what to watch in `efficiency-logs/*.jsonl`, rollback steps and a feedback template: **[docs/efficiency-trial.md](docs/efficiency-trial.md)**.
220
223
 
221
- Everything above (plus roles, pi settings, boot-config and the `PIER_*`/`PI_HERDR_*` env knobs) is also inspectable in-session:
224
+ Everything above (plus roles, pi settings and the `PIER_*`/`PI_HERDR_*` env knobs) is also inspectable in-session:
222
225
 
223
226
  ```text
224
- /pier-config # 5-plane index; hands a guided change to the agent
227
+ /pier-config # 4-plane index; hands a guided change to the agent
225
228
  /pier-config show all # effective value + source (env > workspace > user > default) per key
226
229
  /pier-config check # validation across planes · /pier-config doc → written report
227
230
  ```
@@ -241,6 +244,6 @@ MIT
241
244
 
242
245
  ---
243
246
 
244
- > 💡 **For contributors**: `.gitignore` is tracked in the repository. Note that `packages/pier-workbench/scripts/boot-config.json` is machine-local (see the `.example.json` template), and `docs/research/` contains local research notes ignored by `.gitignore`.
247
+ > 💡 **For contributors**: `.gitignore` is tracked in the repository. `docs/research/` contains local research notes ignored by `.gitignore`.
245
248
  >
246
249
  > **Naming convention**: the brand is **pier** (repo/packages/plugin); runtime protocol identifiers keep the **`pi-herdr`** prefix (`.pi-herdr/roles/` dir, `pi-herdr.subs` session custom entries, `~/.pi/agent/herdr-pi/roles/` user dir) — they persist with user sessions/config files, renaming would break existing data, so they are the compatibility layer.
package/install.mjs CHANGED
@@ -3,25 +3,23 @@
3
3
  * pier one-shot installer / uninstaller / updater / version inspector (cross-platform: macOS / Linux / Windows).
4
4
  *
5
5
  * Usage:
6
- * node install.mjs install [--dev] [--hmr-dev] [--force] # Install (default command, 'install' can be omitted)
7
- * node install.mjs update [--dev] [--hmr-dev] # Refresh both halves in place to latest release (no prior uninstall)
8
- * node install.mjs version [--json] # Local vs npm latest
9
- * node install.mjs uninstall [--dev] [--purge] # Uninstall; --purge deletes boot-config.json as well
6
+ * node install.mjs install [--dev] # Install (default command, 'install' can be omitted)
7
+ * node install.mjs update [--dev] # Refresh both halves in place to latest release (no prior uninstall)
8
+ * node install.mjs version [--json] # Local vs npm latest
9
+ * node install.mjs uninstall [--dev] # Uninstall both halves
10
10
  * node install.mjs --prepare # npm prepare: hooksPath + repo root bin link
11
11
  * node install.mjs --help
12
12
  *
13
13
  * Modes:
14
14
  * User mode (default): pi install npm:pi-pier
15
15
  * + herdr plugin install July24/pier/packages/pier-workbench --yes
16
- * boot-config.json written to herdr plugin config-dir; extPath points to pi-installed
17
- * pi-pier (~/.pi/agent/npm/...), not the repo path inside pier-setup package.
18
16
  * --dev Development mode: pi install <repo>/packages/pier-ext + herdr plugin link <repo>/packages/pier-workbench
19
- * (linked directory is live; code edits take effect immediately; update only rewrites boot-config).
17
+ * (linked directory is live; code edits take effect immediately).
20
18
  * Distribution specs can be overridden via --pi-spec= / --herdr-spec= (for npm publish or fork scenarios).
21
19
  * Failure semantics: Each step provides manual equivalent command; step failure does not abort reporting (exitCode=1).
22
20
  */
23
21
  import { execFileSync } from 'node:child_process';
24
- import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
22
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
25
23
  import { homedir } from 'node:os';
26
24
  import { dirname, join, resolve } from 'node:path';
27
25
  import { fileURLToPath } from 'node:url';
@@ -29,7 +27,6 @@ import { fileURLToPath } from 'node:url';
29
27
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)));
30
28
  const EXT_DIR = join(ROOT, 'packages', 'pier-ext');
31
29
  const WB_DIR = join(ROOT, 'packages', 'pier-workbench');
32
- const DEV_BOOT_CONFIG = join(WB_DIR, 'scripts', 'boot-config.json');
33
30
  const EXT_PATH = join(EXT_DIR, 'src', 'index.ts');
34
31
  const IS_WIN = process.platform === 'win32';
35
32
  const COMMANDS = ['install', 'uninstall', 'update', 'version', 'help'];
@@ -103,6 +100,11 @@ function tryRun(cmd, args) {
103
100
  try { run(cmd, args); return true; } catch (e) { log(` (${e.message.split('\n')[0]})`); return false; }
104
101
  }
105
102
 
103
+ /** Same as tryRun without the diagnostic line, for feature probes whose failure is expected. */
104
+ function tryRunQuiet(cmd, args) {
105
+ try { run(cmd, args); return true; } catch { return false; }
106
+ }
107
+
106
108
  function parseVersion(s) {
107
109
  const m = String(s).match(/(\d+)\.(\d+)\.(\d+)/);
108
110
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
@@ -141,10 +143,6 @@ function npmLatest(name) {
141
143
  }
142
144
  }
143
145
 
144
- /** Herdr plugin config directory (boot-config destination in user mode; semantic path exists once plugin is registered). */
145
- function herdrConfigDir() {
146
- try { return run('herdr', ['plugin', 'config-dir', 'pier.workbench']); } catch { return null; }
147
- }
148
146
 
149
147
  /* ── Environment check (pre-install) ────────────────────────────────── */
150
148
  function checkEnv() {
@@ -152,13 +150,13 @@ function checkEnv() {
152
150
  if (major < 22) die(`node ≥ 22 required, found ${process.versions.node}`);
153
151
  log(`✓ node ${process.versions.node}`);
154
152
  const piVersion = (() => { try { return parseVersion(run('pi', ['--version'])); } catch { return null; } })();
155
- if (!piVersion || !geVersion(piVersion, [0, 84, 4])) {
156
- die(`pi ≥ 0.84.4 required${piVersion ? `, found ${piVersion.join('.')}` : ' (pi not found or not runnable)'}. Install: npm i -g @earendil-works/pi-coding-agent`);
153
+ if (!piVersion || !geVersion(piVersion, [1, 3, 0])) {
154
+ die(`pi ≥ 1.3.0 required${piVersion ? `, found ${piVersion.join('.')}` : ' (pi not found or not runnable)'}. Install: npm i -g @earendil-works/pi-coding-agent`);
157
155
  }
158
156
  log(`✓ pi ${piVersion.join('.')}`);
159
157
  const herdrVersion = (() => { try { return parseVersion(run('herdr', ['--version'])); } catch { return null; } })();
160
- if (!herdrVersion || !geVersion(herdrVersion, [0, 9, 0])) {
161
- die(`herdr ≥ 0.9.0 required${herdrVersion ? `, found ${herdrVersion.join('.')}` : ' (herdr not found)'}. Install: https://herdr.dev/`);
158
+ if (!herdrVersion || !geVersion(herdrVersion, [0, 9, 5])) {
159
+ die(`herdr ≥ 0.9.5 required${herdrVersion ? `, found ${herdrVersion.join('.')}` : ' (herdr not found)'}. Install: https://herdr.dev/`);
162
160
  }
163
161
  log(`✓ herdr ${herdrVersion.join('.')}`);
164
162
  return { piVersion, herdrVersion };
@@ -167,26 +165,6 @@ function checkEnv() {
167
165
  /** pi extension source: user mode uses git/npm spec, dev mode uses local directory. */
168
166
  const piSource = () => (dev ? EXT_DIR : PI_SPEC);
169
167
 
170
- /* ── boot-config detection and writing ────────────────────────────── */
171
- function probePiRuntime() {
172
- const piBin = which('pi');
173
- // bin is a node shim: realpath resolves to .../pi-coding-agent/dist/cli.js (npm layout, isomorphic win/posix)
174
- if (piBin) {
175
- try {
176
- const real = realpathSync(piBin);
177
- if (real.endsWith('cli.js') && existsSync(real)) {
178
- return { piCli: real, piNode: process.execPath };
179
- }
180
- } catch { /* fall back to fallback */ }
181
- }
182
- // npm root -g fallback: when pi is not in PATH (e.g. nvm shim), construct from global npm layout
183
- try {
184
- const gRoot = run('npm', ['root', '-g']);
185
- const cand = join(gRoot, '@earendil-works', 'pi-coding-agent', 'dist', 'cli.js');
186
- if (existsSync(cand)) return { piCli: cand, piNode: process.execPath };
187
- } catch { /* npm unavailable */ }
188
- return null;
189
- }
190
168
 
191
169
  function piAgentDir() {
192
170
  return process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent');
@@ -260,47 +238,6 @@ function resolveUserExtPath() {
260
238
  return extPathFromPiList();
261
239
  }
262
240
 
263
- function writeBootConfig(hmrDev, force) {
264
- const probed = probePiRuntime();
265
- if (!probed) die('cannot locate pi cli.js (tried: pi shim realpath, npm root -g). Fill boot-config.json manually.');
266
- let extPath;
267
- if (dev) {
268
- if (!existsSync(EXT_PATH)) die(`pier-ext entry missing: ${EXT_PATH} (repo layout broken?)`);
269
- extPath = EXT_PATH;
270
- } else {
271
- extPath = resolveUserExtPath();
272
- if (!extPath) die(`cannot locate installed pier-ext (npm:pi-pier / git clone). Did \`pi install ${piSource()}\` succeed?`);
273
- }
274
-
275
- const config = {
276
- mainTabLabel: 'main',
277
- piNode: probed.piNode,
278
- piCli: probed.piCli,
279
- extPath,
280
- workbenchPluginId: 'pier.workbench',
281
- hmrDev,
282
- };
283
-
284
- const target = dev ? DEV_BOOT_CONFIG : join(herdrConfigDir() ?? die('cannot resolve herdr plugin config-dir (is pier.workbench installed?)'), 'boot-config.json');
285
- if (existsSync(target) && !force) {
286
- let existing = null;
287
- try { existing = JSON.parse(readFileSync(target, 'utf8')); } catch { /* treated as corrupted */ }
288
- if (existing) {
289
- log(`• ${target} exists, diff (existing → new):`);
290
- for (const k of Object.keys(config)) {
291
- if (existing[k] !== config[k]) log(` ${k}: ${JSON.stringify(existing[k])} → ${JSON.stringify(config[k])}`);
292
- }
293
- log(' kept existing (use --force to overwrite)');
294
- return;
295
- }
296
- }
297
- mkdirSync(dirname(target), { recursive: true });
298
- writeFileSync(target, JSON.stringify(config, null, 2) + '\n');
299
- log(`✓ boot-config.json ${force ? 'overwritten (--force)' : 'written'} → ${target}`);
300
- log(` piNode = ${config.piNode}`);
301
- log(` piCli = ${config.piCli}`);
302
- log(` extPath = ${config.extPath}`);
303
- }
304
241
 
305
242
  function pkgDirFromExtPath(extPath) {
306
243
  let d = dirname(extPath);
@@ -313,16 +250,57 @@ function pkgDirFromExtPath(extPath) {
313
250
  return null;
314
251
  }
315
252
 
316
- function herdrPluginInfo() {
317
- let out;
318
- try { out = run('herdr', ['plugin', 'list']); } catch { return { installed: false, spec: null, sha: null }; }
319
- const line = out.split(/\r?\n/).find((l) => l.includes('pier.workbench'));
320
- if (!line) return { installed: false, spec: null, sha: null };
253
+ const HERDR_PLUGIN_ID = 'pier.workbench';
254
+ const HERDR_PLUGIN_NAME = 'pier-workbench';
255
+ const NOT_INSTALLED = Object.freeze({ installed: false, spec: null, sha: null, version: null, source: null, enabled: false });
256
+
257
+ /** `plugin list --json` (herdr 0.9.5+): returns undefined for a malformed payload, null when the
258
+ * plugin is absent, so the caller can fall back to the text listing on older binaries. */
259
+ function parseHerdrPluginJson(out) {
260
+ let parsed;
261
+ try { parsed = JSON.parse(out); } catch { return undefined; }
262
+ const plugins = parsed?.result?.plugins ?? parsed?.plugins;
263
+ if (!Array.isArray(plugins)) return undefined;
264
+ const p = plugins.find((x) => x && (x.plugin_id === HERDR_PLUGIN_ID || x.name === HERDR_PLUGIN_NAME));
265
+ if (!p) return null;
266
+ const source = p.source && typeof p.source === 'object' ? p.source : {};
267
+ const kind = typeof source.kind === 'string' ? source.kind : null;
268
+ const root = typeof p.plugin_root === 'string' ? p.plugin_root : null;
269
+ const commit = typeof source.commit === 'string' ? source.commit : null;
270
+ const spec = kind === 'local'
271
+ ? `local:${root ?? p.manifest_path ?? ''}`
272
+ : (typeof p.version === 'string' ? `v${p.version}` : (kind ?? 'installed'));
273
+ return {
274
+ installed: true,
275
+ spec,
276
+ sha: commit ? commit.slice(0, 7) : null,
277
+ version: typeof p.version === 'string' ? p.version : null,
278
+ source: kind,
279
+ enabled: p.enabled === true,
280
+ };
281
+ }
282
+
283
+ function parseHerdrPluginText(out) {
284
+ const line = out.split(/\r?\n/).find((l) => l.includes(HERDR_PLUGIN_ID));
285
+ if (!line) return NOT_INSTALLED;
321
286
  const bracket = line.match(/\[([^\]]+)\]/);
322
287
  const spec = bracket ? bracket[1] : line.trim();
323
288
  const at = spec.lastIndexOf('@');
324
289
  const fullSha = at >= 0 && !spec.startsWith('local:') ? spec.slice(at + 1) : null;
325
- return { installed: true, spec, sha: fullSha ? fullSha.slice(0, 7) : null };
290
+ return { installed: true, spec, sha: fullSha ? fullSha.slice(0, 7) : null, version: null, source: spec.startsWith('local:') ? 'local' : null, enabled: false };
291
+ }
292
+
293
+ function herdrPluginInfo() {
294
+ try {
295
+ const out = run('herdr', ['plugin', 'list', '--json']);
296
+ const parsed = parseHerdrPluginJson(out);
297
+ if (parsed !== undefined) return parsed ?? NOT_INSTALLED;
298
+ } catch { /* older herdr or no server; fall through to the text listing */ }
299
+ try {
300
+ return parseHerdrPluginText(run('herdr', ['plugin', 'list']));
301
+ } catch {
302
+ return NOT_INSTALLED;
303
+ }
326
304
  }
327
305
 
328
306
  function collectStatus() {
@@ -344,17 +322,17 @@ function usage() {
344
322
  log(`pier-setup — install / update / inspect pier (pi-pier + pier.workbench)
345
323
 
346
324
  Usage:
347
- pier-setup [install] [--dev] [--hmr-dev] [--force]
348
- pier-setup update [--dev] [--hmr-dev]
325
+ pier-setup [install] [--dev]
326
+ pier-setup update [--dev]
349
327
  pier-setup version [--json]
350
- pier-setup uninstall [--dev] [--purge]
328
+ pier-setup uninstall [--dev]
351
329
 
352
330
  npx pier-setup@latest # recommended user install
353
331
  npx pier-setup@latest update # refresh both halves to newest release
354
332
  npx pier-setup@latest version # local vs npm latest
355
333
 
356
334
  --pi-spec= / --herdr-spec= override distribution sources
357
- --dev local link (clone); update only rewrites boot-config`);
335
+ --dev local link (clone); the linked directories are live`);
358
336
  }
359
337
 
360
338
  function version() {
@@ -390,25 +368,26 @@ function update() {
390
368
  if (latestSetup && me && cmpSemver(me, latestSetup) < 0) {
391
369
  log(`⚠ installer ${me} < latest ${latestSetup} — re-run: npx pier-setup@latest update`);
392
370
  }
393
- const hmrDev = flags.has('--hmr-dev');
394
371
  if (dev) {
395
- log('dev mode: local link is live; pull the repo yourself. Refreshing boot-config only.');
396
- writeBootConfig(hmrDev, true);
372
+ log('dev mode: local link is live; pull the repo yourself (no installer-managed files to refresh).');
397
373
  } else {
398
374
  if (tryRun('pi', ['update', piSource()])) log(`✓ pi extension updated (${piSource()})`);
399
375
  else if (tryRun('pi', ['install', piSource()])) log(`✓ pi extension installed (${piSource()})`);
400
376
  else { console.error('✗ pi update/install failed'); log(` manual: pi update ${piSource()}`); process.exitCode = 1; }
401
- if (tryRun('herdr', ['plugin', 'install', HERDR_SPEC, '--yes'])) log(`✓ herdr plugin updated (${HERDR_SPEC})`);
377
+ // 0.9.4+ updates the GitHub checkout in place without disturbing running plugin panes.
378
+ // Only try it for an installed, non-local plugin; otherwise install from the configured spec.
379
+ const existing = herdrPluginInfo();
380
+ const updated = (existing.installed && existing.source !== 'local'
381
+ && tryRunQuiet('herdr', ['plugin', 'update', HERDR_PLUGIN_ID, '--yes']))
382
+ || tryRunQuiet('herdr', ['plugin', 'install', HERDR_SPEC, '--yes']);
383
+ if (updated) log(`✓ herdr plugin updated (${HERDR_SPEC})`);
402
384
  else { console.error('✗ herdr plugin update failed'); log(` manual: herdr plugin install ${HERDR_SPEC} --yes`); process.exitCode = 1; }
403
- writeBootConfig(hmrDev, true);
404
385
  }
405
386
  log('');
406
387
  version();
407
388
  }
408
389
 
409
390
  function install() {
410
- const hmrDev = flags.has('--hmr-dev');
411
- const force = flags.has('--force');
412
391
  checkEnv();
413
392
  removeLegacyGitRegistration();
414
393
 
@@ -417,40 +396,31 @@ function install() {
417
396
  else { console.error('✗ pi install failed'); log(` manual: pi install ${EXT_DIR}`); process.exitCode = 1; }
418
397
  if (tryRun('herdr', ['plugin', 'link', WB_DIR])) log('✓ herdr plugin linked (packages/pier-workbench, local)');
419
398
  else { console.error('✗ herdr plugin link failed'); log(` manual: herdr plugin link ${WB_DIR}`); process.exitCode = 1; }
420
- writeBootConfig(hmrDev, force);
421
399
  } else {
422
400
  if (tryRun('pi', ['install', piSource()])) log(`✓ pi extension installed (${PI_SPEC})`);
423
401
  else { console.error('✗ pi install failed'); log(` manual: pi install ${PI_SPEC}`); process.exitCode = 1; }
424
402
  if (tryRun('herdr', ['plugin', 'install', HERDR_SPEC, '--yes'])) log(`✓ herdr plugin installed (${HERDR_SPEC})`);
425
403
  else { console.error('✗ herdr plugin install failed'); log(` manual: herdr plugin install ${HERDR_SPEC}`); process.exitCode = 1; }
426
- writeBootConfig(hmrDev, force);
427
404
  }
428
405
 
429
406
  log(`
430
407
  pier installed (${dev ? 'dev mode: local paths, code changes are live' : 'user mode: managed checkouts'}). Next:
431
- 1. start herdr, open/create a workspace — the main tab auto-bootstraps a pi session
408
+ 1. start herdr, open/create a workspace and run \`pi\` inside a pane
432
409
  2. inside pi, ask the model to use todo_write / subagent tools`);
433
410
  }
434
411
 
435
412
  function uninstall() {
436
- const purge = flags.has('--purge');
437
413
  if (tryRun('pi', ['remove', piSource()])) log('✓ pi extension removed');
438
414
  else { console.error('✗ pi remove failed'); log(` manual: pi remove ${piSource()}`); process.exitCode = 1; }
439
415
 
440
416
  if (dev) {
441
417
  if (tryRun('herdr', ['plugin', 'unlink', 'pier.workbench'])) log('✓ herdr plugin unlinked (files kept)');
442
418
  else { console.error('✗ herdr plugin unlink failed'); log(' manual: herdr plugin unlink pier.workbench'); process.exitCode = 1; }
443
- if (purge && existsSync(DEV_BOOT_CONFIG)) { rmSync(DEV_BOOT_CONFIG); log('✓ dev boot-config.json purged'); }
444
419
  } else {
445
420
  if (tryRun('herdr', ['plugin', 'uninstall', 'pier.workbench'])) log('✓ herdr plugin uninstalled (managed checkout removed)');
446
421
  else { console.error('✗ herdr plugin uninstall failed'); log(' manual: herdr plugin uninstall pier.workbench'); process.exitCode = 1; }
447
- if (purge) {
448
- const dir = herdrConfigDir();
449
- const f = dir ? join(dir, 'boot-config.json') : null;
450
- if (f && existsSync(f)) { rmSync(f); log('✓ boot-config.json purged'); }
451
- }
452
422
  }
453
- log(`\npier uninstalled.${purge ? '' : ' (boot-config.json kept; use --purge to remove)'}`);
423
+ log('\npier uninstalled.');
454
424
  }
455
425
 
456
426
  if (command === 'help' || flags.has('--help') || flags.has('-h')) usage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pier-setup",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "One-shot installer for pier: install / update / version / uninstall for pi-pier + pier.workbench",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,8 +31,9 @@
31
31
  "packages/*"
32
32
  ],
33
33
  "scripts": {
34
+ "build:workbench": "tsc -p packages/pier-workbench/tsconfig.build.json",
34
35
  "typecheck": "tsc --noEmit -p packages/pier-ext/tsconfig.json",
35
- "test": "npm run typecheck && node --test \"packages/*/test/*.test.ts\"",
36
+ "test": "npm run build:workbench && npm run typecheck && node --test \"packages/*/test/*.test.ts\"",
36
37
  "test:mutation": "stryker run",
37
38
  "prepare": "node ./install.mjs --prepare"
38
39
  },
@@ -42,7 +43,6 @@
42
43
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
43
44
  "@deepseek-ai/cordis-plugin-timer": "^1.1.3",
44
45
  "@stryker-mutator/core": "^10.0.0",
45
- "@stryker-mutator/typescript-checker": "^10.0.0",
46
46
  "typescript": "~5.9.3"
47
47
  },
48
48
  "pi": {