pi-session-mcp 0.12.0 → 0.12.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.
@@ -1,3 +1,4 @@
1
+ import { isShortBranchName, MAX_WRITERS } from "../execution-modes.js";
1
2
  import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "../extension-spec.js";
2
3
  import { isAcceptableExtensionToolName, MAX_GRANTED_EXTENSION_TOOLS } from "../session-tools.js";
3
4
  import { MAX_PROFILE_SKILLS } from "../skill-catalog.js";
@@ -106,8 +107,11 @@ function validCapabilities(value) {
106
107
  if (!workspace || typeof workspace !== "object")
107
108
  return false;
108
109
  const record = workspace;
109
- return exactKeys(record, ["alias", "mode"]) && typeof record.alias === "string" && ALIAS_PATTERN.test(record.alias)
110
- && typeof record.mode === "string" && ["unset", "host", "container", "read-only"].includes(record.mode);
110
+ return exactKeys(record, record.mode === "container" ? ["alias", "mode", "writers", "defaultBase"] : ["alias", "mode"])
111
+ && typeof record.alias === "string" && ALIAS_PATTERN.test(record.alias)
112
+ && typeof record.mode === "string" && ["unset", "host", "container", "read-only"].includes(record.mode)
113
+ && (record.mode !== "container" || (typeof record.writers === "number" && Number.isInteger(record.writers) && record.writers >= 1 && record.writers <= MAX_WRITERS
114
+ && (record.defaultBase === null || (typeof record.defaultBase === "string" && isShortBranchName(record.defaultBase)))));
111
115
  }))
112
116
  return false;
113
117
  if (!profiles.every((profile) => {
@@ -5,6 +5,8 @@ import { ConfigError } from "./config.js";
5
5
  import { ImmutableMap } from "./execution-profile.js";
6
6
  import { isSafeRefName } from "./writer-boundary.js";
7
7
  export const EXECUTION_MODES_FILE = "execution-modes.json";
8
+ /** Writers per container workspace; capabilities report it and setup verify checks it. */
9
+ export const MAX_WRITERS = 16;
8
10
  /** A base is a plain local branch name: safe, not `HEAD`, no ref prefix, no commit ID, at most 200 characters. */
9
11
  export function isShortBranchName(value) {
10
12
  return value.length <= 200
@@ -21,7 +23,7 @@ const entrySchema = z.discriminatedUnion("mode", [
21
23
  z.object({ mode: z.literal("read-only") }).strict(),
22
24
  z.object({
23
25
  mode: z.literal("container"),
24
- writers: z.number().int().min(1).max(16).default(1),
26
+ writers: z.number().int().min(1).max(MAX_WRITERS).default(1),
25
27
  lifecycle: z.enum(["persistent", "ephemeral"]).default("persistent"),
26
28
  profiles: z.array(z.string()).min(1),
27
29
  image: z.union([z.literal("node"), z.string().regex(/^[0-9a-f]{64}$/)]).default("node"),
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.12.1 — 2026-10-28
6
+
7
+ Patch release for
8
+ [#160](https://github.com/plagor44-ux/pi-session-mcp/issues/160). The MCP
9
+ contract is unchanged. `npm audit` still reports `undici` 8.10.2, pinned by Pi
10
+ SDK 1.4.0, the latest SDK release
11
+ ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
12
+
13
+ - Fixed: `setup --verify` of a client target failed with `mcp_failed` on every
14
+ installation with a `container` workspace. The verifier did not accept the
15
+ `writers` and `defaultBase` fields that 0.12.0 added to such workspaces in
16
+ `pi_capabilities_get`. It now accepts exactly these fields on container
17
+ workspaces, with `writers` from 1 to 16 and `defaultBase` `null` or a short
18
+ branch name, and still rejects them on every other mode. A new test verifies
19
+ the real server with container workspaces, which Level 2 does not configure
20
+ ([#160](https://github.com/plagor44-ux/pi-session-mcp/issues/160)).
21
+ - Changed: the upgrade runbook names the reset after migration. Migration keeps
22
+ each clone, so writers migrated from an older release lack the commit
23
+ identity until they are reset or finished.
24
+ - Tests: the Doctor mode-order test no longer reads the host's writer lease
25
+ directory, which made it fail after a 0.12.0 migration.
26
+
5
27
  ## 0.12.0 — 2026-10-28
6
28
 
7
29
  Minor release under ADR 0003 for
package/README.md CHANGED
@@ -30,7 +30,7 @@ It is not the right tool when:
30
30
 
31
31
  ## Status
32
32
 
33
- The package version is **0.12.0**.
33
+ The package version is **0.12.1**.
34
34
 
35
35
  [Per-repository execution modes](docs/execution-modes.md) make host writes explicit and block coding on read-only aliases. On container workspaces, the one host `pi-session-mcp` registration leases a rootless-Podman writer per coding session on Linux; no per-writer registrations or tool prefixes are needed ([ADR 0012](docs/adr/0012-leased-writer-pool.md)). Setup provisions, accepts, collects and resets the pool. Starts normally use the workspace default base; fix rounds can pass a short branch `base`. Start/get report `writer` and `base` for collection, and capabilities report configured `writers` and `defaultBase`. Reset, finish and recreate need no reconnect; finish returns the writer to the default base. See the [container upgrade runbook](docs/installation.md#container-writers).
36
36
 
@@ -1,3 +1,4 @@
1
+ import { isShortBranchName, MAX_WRITERS } from "../execution-modes.js";
1
2
  import { MAX_PROFILE_EXTENSIONS, parseExtensionSpec } from "../extension-spec.js";
2
3
  import { isAcceptableExtensionToolName, MAX_GRANTED_EXTENSION_TOOLS } from "../session-tools.js";
3
4
  import { MAX_PROFILE_SKILLS } from "../skill-catalog.js";
@@ -106,8 +107,11 @@ function validCapabilities(value) {
106
107
  if (!workspace || typeof workspace !== "object")
107
108
  return false;
108
109
  const record = workspace;
109
- return exactKeys(record, ["alias", "mode"]) && typeof record.alias === "string" && ALIAS_PATTERN.test(record.alias)
110
- && typeof record.mode === "string" && ["unset", "host", "container", "read-only"].includes(record.mode);
110
+ return exactKeys(record, record.mode === "container" ? ["alias", "mode", "writers", "defaultBase"] : ["alias", "mode"])
111
+ && typeof record.alias === "string" && ALIAS_PATTERN.test(record.alias)
112
+ && typeof record.mode === "string" && ["unset", "host", "container", "read-only"].includes(record.mode)
113
+ && (record.mode !== "container" || (typeof record.writers === "number" && Number.isInteger(record.writers) && record.writers >= 1 && record.writers <= MAX_WRITERS
114
+ && (record.defaultBase === null || (typeof record.defaultBase === "string" && isShortBranchName(record.defaultBase)))));
111
115
  }))
112
116
  return false;
113
117
  if (!profiles.every((profile) => {
@@ -15,6 +15,8 @@ export interface WorkspaceModeEntry {
15
15
  readonly container?: ContainerSettings;
16
16
  }
17
17
  export declare const EXECUTION_MODES_FILE = "execution-modes.json";
18
+ /** Writers per container workspace; capabilities report it and setup verify checks it. */
19
+ export declare const MAX_WRITERS = 16;
18
20
  /** A base is a plain local branch name: safe, not `HEAD`, no ref prefix, no commit ID, at most 200 characters. */
19
21
  export declare function isShortBranchName(value: string): boolean;
20
22
  export declare function executionModesPath(configPath: string): string;
@@ -5,6 +5,8 @@ import { ConfigError } from "./config.js";
5
5
  import { ImmutableMap } from "./execution-profile.js";
6
6
  import { isSafeRefName } from "./writer-boundary.js";
7
7
  export const EXECUTION_MODES_FILE = "execution-modes.json";
8
+ /** Writers per container workspace; capabilities report it and setup verify checks it. */
9
+ export const MAX_WRITERS = 16;
8
10
  /** A base is a plain local branch name: safe, not `HEAD`, no ref prefix, no commit ID, at most 200 characters. */
9
11
  export function isShortBranchName(value) {
10
12
  return value.length <= 200
@@ -21,7 +23,7 @@ const entrySchema = z.discriminatedUnion("mode", [
21
23
  z.object({ mode: z.literal("read-only") }).strict(),
22
24
  z.object({
23
25
  mode: z.literal("container"),
24
- writers: z.number().int().min(1).max(16).default(1),
26
+ writers: z.number().int().min(1).max(MAX_WRITERS).default(1),
25
27
  lifecycle: z.enum(["persistent", "ephemeral"]).default("persistent"),
26
28
  profiles: z.array(z.string()).min(1),
27
29
  image: z.union([z.literal("node"), z.string().regex(/^[0-9a-f]{64}$/)]).default("node"),
@@ -6,6 +6,7 @@ import { loadConfig } from "./config.js";
6
6
  import { containerRuntimeUsable } from "./container-runtime.js";
7
7
  import { ALIAS_PATTERN } from "./execution-profile.js";
8
8
  import { runDoctor } from "./doctor.js";
9
+ import { MAX_WRITERS } from "./execution-modes.js";
9
10
  import { changeExecutionMode } from "./setup-modes.js";
10
11
  import { acquireOverrideLock, OverrideLockError } from "./override-lock.js";
11
12
  import { createCommandMcpStdioLauncher, createProcessRunner } from "./setup-process.js";
@@ -158,7 +159,7 @@ async function remove(ctx, name, discard, deps, extras = []) {
158
159
  const writerIndex = (name) => Number(name.slice(name.lastIndexOf("-w") + 2));
159
160
  async function plan(ctx, entry) {
160
161
  const keep = [], missing = [], surplus = [];
161
- for (let index = 1; index <= 16; index++) {
162
+ for (let index = 1; index <= MAX_WRITERS; index++) {
162
163
  const name = writerName(ctx.alias, index), state = await readWriterState(ctx.home, name);
163
164
  if (entry.mode === "container" && index <= entry.container.writers) {
164
165
  keep.push(name);
@@ -322,7 +323,7 @@ export async function changeWriterMode(configPath, request, deps) {
322
323
  return changeExecutionMode(configPath, request, modeDependencies);
323
324
  }
324
325
  writerWork = config.workspaceModes.get(request.alias)?.mode === "container";
325
- for (let index = 1; index <= 16 && !writerWork; index++) {
326
+ for (let index = 1; index <= MAX_WRITERS && !writerWork; index++) {
326
327
  try {
327
328
  await lstat(join(writersRoot(deps.home ?? homedir()), writerName(request.alias, index)));
328
329
  writerWork = true;
@@ -359,7 +360,7 @@ export async function changeWriterMode(configPath, request, deps) {
359
360
  const config = await loadConfig(configPath);
360
361
  if (!config.workspaceModes.get(request.alias)?.container?.base) {
361
362
  const bases = new Set();
362
- for (let index = 1; index <= 16; index++) {
363
+ for (let index = 1; index <= MAX_WRITERS; index++) {
363
364
  const state = await readWriterState(deps.home ?? homedir(), writerName(request.alias, index));
364
365
  if (state)
365
366
  bases.add(state.base.ref);
@@ -2457,6 +2457,131 @@ Not established for `0.11.6`:
2457
2457
  wrapper's periodic credential copy (#136);
2458
2458
  - the VS Code wake; macOS delivery.
2459
2459
 
2460
+ ### v0.12.1 pre-release acceptance
2461
+
2462
+ `v0.12.1` was accepted on 2026-10-28 from 13:12 to 13:17 UTC on its release
2463
+ candidate `c6b472d`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
2464
+ Codex CLI `0.162.1` and Claude Code `2.1.312`. Level 2 passed in the working
2465
+ tree and for the packed tarball, each with delivery off and with delivery on.
2466
+ Level 1 passed except `npm audit`. Later commits on the release pull request
2467
+ change only this record.
2468
+
2469
+ The patch ships #160 on top of 0.12.0. It does not change the MCP contract or
2470
+ the setup report schema.
2471
+
2472
+ - Level 1 in a clean worktree of the candidate after `npm ci`: every command
2473
+ exited with `0`, except `npm audit`; `git status --short` printed nothing.
2474
+ - The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
2475
+ 120 files, 3708 passed and 14 skipped, and no unhandled error. It ran on a
2476
+ host with a real writer lease directory, which the Doctor mode-order test
2477
+ no longer reads.
2478
+ - `npm audit` exited with `1` and names only the known `undici` finding
2479
+ through `@earendil-works/pi-coding-agent` 1.4.0
2480
+ ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
2481
+ - `npm pack` listed 907 files (tarball shasum
2482
+ `f206e0eb0818342fb9e6a36c2c147fb690c515ce`); a scan of the packed files for
2483
+ private names, hosts and paths found none.
2484
+ - Doctor reported `packageVersion` `0.12.1`. It exited with `2` because no
2485
+ configuration was set (`config_missing`).
2486
+ - Level 2 in the working tree: 160 of 160 checks passed with delivery off and
2487
+ 228 of 228 with `--events spool`.
2488
+ - Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
2489
+ 228 of 228 with `--events spool`. The install smoke reported `smoke_passed`
2490
+ with version `0.12.1`.
2491
+ - In every Level 2 run the real client registrations and the built files were
2492
+ unchanged, no process remained, and the leak check passed.
2493
+ - Podman-gated writer tests on the candidate: 4 of 4 passed.
2494
+ - The candidate's setup verifier against its own server with the operator's
2495
+ real configuration, which has five container workspaces, reported
2496
+ `verified`, with initialize, `tools/list` (eight tools) and
2497
+ `pi_capabilities_get`. The same check with the installed 0.12.0 failed at
2498
+ `pi_capabilities_get`.
2499
+ - Review: `review-glm` (another model family than the author) on the fix,
2500
+ verdict `NON_BLOCKING_ONLY`; the shared writer bound and the real-server test
2501
+ with container workspaces were added.
2502
+
2503
+ Not established for `0.12.1`: client-target `--verify` through the installed
2504
+ package, which follows the upgrade; Level 3.
2505
+
2506
+ ### v0.12.0 post-release evidence
2507
+
2508
+ Recorded on 2026-10-28 for the published release `0.12.0`.
2509
+
2510
+ Tag binding: a fresh clone of the annotated tag `v0.12.0` (tag object
2511
+ `bb77dfd8a4e49849d5e9c7fd305fb00baf443044`) peels to the merge commit
2512
+ `dfbef8c5efce38eacc8704dbaf93c534cdad0ef8` of #159, and
2513
+ `git describe --tags --exact-match` reports `v0.12.0`. CI on the merge commit
2514
+ passed on Linux, macOS 15, macOS 26 and `verify` after one rerun: the first
2515
+ macOS 26 run hit a timeout in `test/extension-ablation-deps.test.ts`
2516
+ (5044 ms against 5000 ms), a git-heavy evaluation test that the release did not
2517
+ touch. CI on the tag passed on the first attempt.
2518
+
2519
+ - Level 1 on the tag clone after `npm ci`: typecheck, `build:cli`, the test
2520
+ run (120 files, 3693 passed, 14 skipped, with
2521
+ `NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
2522
+ `git diff --check` exited with `0`, and the status stayed clean. Doctor
2523
+ reported `packageVersion` `0.12.0`.
2524
+ - `pi-session-mcp@0.12.0` on npm: 907 files, shasum
2525
+ `4699b05ef849e88c6d56f5ddae6b06984799cea6`, `gitHead` `1fb8a7e` (the head of
2526
+ #159, whose tree equals the merge commit). A local `npm pack` of the merge
2527
+ commit gives the same shasum. A scan of the published files for private
2528
+ names, hosts and paths found none.
2529
+ - Published npm package, Level 2 from a fresh prefix: 160 of 160 with delivery
2530
+ off and 228 of 228 with `--events spool`; the install smoke reported
2531
+ `smoke_passed` with version `0.12.0`. Clients: Codex CLI `0.162.1` and Claude
2532
+ Code `2.1.312`.
2533
+ - The GitHub release `v0.12.0` was published the same day.
2534
+
2535
+ Operator upgrade of the workstation from `0.11.8` to `0.12.0` on 2026-10-28,
2536
+ following [Installation](installation.md#container-writers):
2537
+
2538
+ - Coding sessions were closed and the two pi-session-mcp writer servers were
2539
+ disabled in the controller; one idle controller of another repository still
2540
+ had four writer containers running.
2541
+ - Both host client targets (`claude-code:user`, `codex:user`) were removed with
2542
+ `0.11.8`, then `npm install -g pi-session-mcp@0.12.0`. Doctor reported
2543
+ `packageVersion` `0.12.0` and, as expected, every writer `writer_not_ready`
2544
+ and `writer_not_migrated`.
2545
+ - Five container repositories with 18 writers were migrated by repeating their
2546
+ provisioning commands with unchanged settings, each first as a dry run
2547
+ (`planned`), then with `--apply`: pi-session-mcp (2 writers), node-app (4),
2548
+ go-app (4, operator image, `--cache go`), pynode-app (4, operator image,
2549
+ base `dev`) and py-app (4, operator image). Every writer reported
2550
+ `writer_accepted` and is `launch: "relay"`, `ready`, verdict `pass`. Each
2551
+ repository recorded its shared default base (`main`, and `dev` for
2552
+ pynode-app). All 36 owned writer registrations and their hook entries were
2553
+ removed; the fence removed the four running legacy containers; no writer
2554
+ container remained.
2555
+ - `--apply` of both client targets passed, but `--verify` failed with
2556
+ `mcp_failed` on both:
2557
+ [#160](https://github.com/plagor44-ux/pi-session-mcp/issues/160). A direct
2558
+ handshake with the installed server returned the eight tools and the
2559
+ capabilities, including `writers` and `defaultBase` for each container
2560
+ workspace; the 0.12.0 verifier rejected them. The fix, run with the real
2561
+ configuration against the candidate's own server, reported `verified`.
2562
+ - `--writer <name> --verify` passed for all 18 writers over the relay path
2563
+ (`mcp_verified`).
2564
+ - Live relayed coding session through the one host registration, after
2565
+ reconnecting `pi-session-mcp`: `pi_capabilities_get` listed the container
2566
+ workspaces with `writers` and `defaultBase`; `pi_session_start` with profile
2567
+ `code` on the pi-session-mcp workspace leased `pi-session-mcp-w1` at
2568
+ `refs/heads/main` and returned `writer` and `base`. The worker implemented
2569
+ the #160 fix in two turns. Both turns woke the controller through the wake
2570
+ hook from the host spool, without polling. Close, collect and finish
2571
+ succeeded without a reconnect. A read-only `review-glm` session on the host
2572
+ checkout reviewed the commit (`NON_BLOCKING_ONLY`).
2573
+ - The first turn could not commit: the migrated clones had no commit identity
2574
+ (Doctor `writer_git_identity_missing` on all 18 writers), because migration
2575
+ keeps the clone and only provisioning and reset copy the identity. After
2576
+ `--finish` of the used writer and `--reset --apply` of the other 17, Doctor
2577
+ reported no writer finding, and all 18 writers stayed `relay`, `ready` and
2578
+ accepted. The upgrade runbook now names this step.
2579
+
2580
+ Not established for `0.12.0`: the upgrade of the second machine; the live
2581
+ checks of #151 (`--cache uv`) and #154 (`--tags reachable`), which need a
2582
+ settings change of an existing writer; a busy-pool (`writer_unavailable`) and a
2583
+ `base` fix round in live use; Level 3.
2584
+
2460
2585
  ### v0.12.0 pre-release acceptance
2461
2586
 
2462
2587
  `v0.12.0` was accepted on 2026-10-28 from 11:40 to 11:45 UTC on its release
@@ -756,6 +756,15 @@ repository step between the install and the client targets:
756
756
  3. `--apply` and `--verify` the client targets, then `--writer <name> --verify`
757
757
  for each writer. Reconnect the host `pi-session-mcp` server after migration
758
758
  and client setup. Later reset, finish and recreate need no reconnect.
759
+ On 0.12.0, client-target verify fails with `mcp_failed` once a container
760
+ workspace exists
761
+ ([#160](https://github.com/plagor44-ux/pi-session-mcp/issues/160)); 0.12.1
762
+ fixes it.
763
+ 4. Migration keeps each clone, so a writer migrated from a release before
764
+ 0.12.0 has no commit identity yet, and Doctor reports
765
+ `writer_git_identity_missing`. Collect any work, then
766
+ `--writer <name> --reset --apply` (or `--finish --apply`) each writer;
767
+ reset copies the operator checkout's identity into the clone.
759
768
 
760
769
  The repository step does not count another repository's writers that are not
761
770
  ready yet, so the repositories can be accepted one after the other. Releases
package/docs/roadmap.md CHANGED
@@ -15,7 +15,8 @@ mean they are implemented or that their gates passed.
15
15
  - 2026-10-28: [ADR 0012](adr/0012-leased-writer-pool.md) was accepted and
16
16
  implemented in #157: one host registration with a leased container writer
17
17
  pool, base-aware allocation and host-spool events. Prepared as minor release
18
- 0.12.0 together with #151 and #154; no new live acceptance is claimed.
18
+ 0.12.0 together with #151 and #154, released the same day and installed with
19
+ a live relayed coding session; 0.12.1 fixes client-target verify (#160).
19
20
 
20
21
  The explicit 2026-10-17 user request adds a bounded development-only
21
22
  [token-footprint measurement](sprints/2026-10-17-token-footprint.md), following
@@ -40,7 +40,7 @@ records name the package version and the build they ran on, see
40
40
  ```json
41
41
  {
42
42
  "ok": true,
43
- "server": { "name": "pi-session-mcp", "version": "0.12.0" },
43
+ "server": { "name": "pi-session-mcp", "version": "0.12.1" },
44
44
  "configuration": {
45
45
  "fingerprint": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
46
46
  "reloadPolicy": "restart-required"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-session-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Local stdio MCP server for controlled Pi coding-agent sessions",
5
5
  "license": "MIT",
6
6
  "keywords": [