pi-session-mcp 0.13.1 → 0.14.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 (52) hide show
  1. package/.pi-session-mcp-cli/config.js +21 -3
  2. package/.pi-session-mcp-cli/doctor.js +6 -2
  3. package/.pi-session-mcp-cli/skill-library.js +1 -1
  4. package/.pi-session-mcp-cli/writer-boundary.js +2 -1
  5. package/.pi-session-mcp-cli/writer-lifecycle.js +18 -4
  6. package/CHANGELOG.md +87 -0
  7. package/README.md +1 -1
  8. package/dist/capabilities.d.ts +13 -0
  9. package/dist/capabilities.js +17 -13
  10. package/dist/config.d.ts +3 -1
  11. package/dist/config.js +21 -3
  12. package/dist/doctor.d.ts +1 -1
  13. package/dist/doctor.js +6 -2
  14. package/dist/model-catalog.d.ts +40 -0
  15. package/dist/model-catalog.js +38 -0
  16. package/dist/profile-probe.d.ts +28 -0
  17. package/dist/profile-probe.js +164 -0
  18. package/dist/sdk-pi-adapter.d.ts +1 -1
  19. package/dist/sdk-pi-adapter.js +25 -6
  20. package/dist/setup-cli.d.ts +7 -0
  21. package/dist/setup-cli.js +36 -0
  22. package/dist/setup-main.js +10 -3
  23. package/dist/setup-probe-main.d.ts +14 -0
  24. package/dist/setup-probe-main.js +76 -0
  25. package/dist/setup-probe-result.d.ts +39 -0
  26. package/dist/setup-probe-result.js +62 -0
  27. package/dist/setup-writers-common.js +1 -1
  28. package/dist/skill-library.js +1 -1
  29. package/dist/writer-boundary.d.ts +1 -1
  30. package/dist/writer-boundary.js +2 -1
  31. package/dist/writer-guard.js +1 -1
  32. package/dist/writer-lifecycle.d.ts +1 -0
  33. package/dist/writer-lifecycle.js +18 -4
  34. package/dist/writer-relay.js +12 -1
  35. package/docs/acceptance.md +313 -1
  36. package/docs/adr/0012-leased-writer-pool.md +10 -6
  37. package/docs/adr/0013-hardened-read-only-profiles.md +30 -0
  38. package/docs/adr/0014-profile-probe-and-model-catalog.md +89 -0
  39. package/docs/client-setup.md +18 -12
  40. package/docs/doctor-design.md +1 -0
  41. package/docs/execution-modes.md +25 -3
  42. package/docs/installation.md +87 -0
  43. package/docs/isolation-setup.md +8 -2
  44. package/docs/roadmap.md +20 -1
  45. package/docs/skills.md +42 -25
  46. package/docs/sprints/2026-10-24-s8-writer-migration.md +5 -3
  47. package/docs/sprints/2026-10-29-s12-audit-fixes.md +172 -0
  48. package/docs/threat-model.md +10 -1
  49. package/docs/tool-contracts.md +1 -1
  50. package/package.json +1 -1
  51. package/skills/pi-session-orchestration/SKILL.md +1 -1
  52. package/skills/pi-session-profiles/SKILL.md +124 -0
@@ -344,7 +344,7 @@ export async function inspectWork(ctx, name) {
344
344
  });
345
345
  return {
346
346
  baseline: head === state.base.commit && refs.length === 1 && refs[0].name === state.base.ref && refs[0].commit === state.base.commit && tree === baseTree,
347
- dirty: tree !== headTree, branches: refs.filter(({ name }) => name.startsWith("refs/heads/")),
347
+ dirty: tree !== headTree, head: head, branches: refs.filter(({ name }) => name.startsWith("refs/heads/")),
348
348
  digest: `sha256:${createHash("sha256").update(result.stdout).digest("hex")}`,
349
349
  };
350
350
  }
@@ -412,10 +412,16 @@ if [ "$tree" != "$(${BOUNDARY_GIT} rev-parse 'HEAD^{tree}')" ]; then
412
412
  export GIT_COMMITTER_NAME=pi-writer GIT_COMMITTER_EMAIL=pi-writer@example.invalid
413
413
  commit=$(printf '%s\\n' 'WIP collected by pi-session-mcp' | ${BOUNDARY_GIT} commit-tree "$tree" -p HEAD)
414
414
  branch=$(${BOUNDARY_GIT} symbolic-ref --short HEAD || printf '%s' "$4")
415
- if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$branch-wip"; then
415
+ # A taken name, or one that conflicts with an exported branch path, is refused rather than aborting the export.
416
+ if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$branch-wip" \\
417
+ || ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit" 2>/dev/null; then
418
+ printf '%s\\n' ref_name_refused
419
+ fi
420
+ elif ! ${BOUNDARY_GIT} symbolic-ref -q HEAD >/dev/null 2>&1 && [ -n "$(${BOUNDARY_GIT} rev-list -n 1 HEAD --not --branches)" ]; then
421
+ # Clean detached HEAD that no branch of the clone reaches: export its commit under a derived name (#193).
422
+ if ${BOUNDARY_GIT} --git-dir=/tmp/scratch show-ref --verify --quiet "refs/heads/$4-detached" \\
423
+ || ! ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$4-detached" "$(${BOUNDARY_GIT} rev-parse --verify HEAD)" 2>/dev/null; then
416
424
  printf '%s\\n' ref_name_refused
417
- else
418
- ${BOUNDARY_GIT} --git-dir=/tmp/scratch update-ref "refs/heads/$branch-wip" "$commit"
419
425
  fi
420
426
  fi
421
427
  if [ -z "$(${BOUNDARY_GIT} --git-dir=/tmp/scratch for-each-ref --format='%(refname)')" ]; then
@@ -450,6 +456,14 @@ fi`;
450
456
  return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
451
457
  }
452
458
  const work = await inspectWork(ctx, name);
459
+ // Defence in depth (#193): whatever the boundary script exported or reported as nothing to collect,
460
+ // every commit the clone's work refers to must exist as a commit object in the operator checkout
461
+ // before a digest is recorded.
462
+ for (const sha of [work.head, ...work.branches.map((branch) => branch.commit)]) {
463
+ if (sha === undefined || await git(ctx, ["cat-file", "-e", `${sha}^{commit}`], true) === undefined) {
464
+ return { status: "failed", code: "writer_collect_incomplete", refs, refused: findings.length, findings };
465
+ }
466
+ }
453
467
  await writeWriterState(ctx.home, name, { ...state, collected: { digest: work.digest, at: new Date().toISOString() } });
454
468
  return { status: "ok", code: refs.length === 0 ? "nothing_to_collect" : "writer_collected", refs, refused: findings.length, findings };
455
469
  }
@@ -2,6 +2,7 @@ import { constants } from "node:fs";
2
2
  import { open } from "node:fs/promises";
3
3
  import { Client } from "@modelcontextprotocol/client";
4
4
  import { z } from "zod";
5
+ import { projectExecutionProfile } from "./capabilities.js";
5
6
  import { ChildStdioTransport } from "./child-stdio-transport.js";
6
7
  import { PiSessionCreationError, WriterUnavailableError } from "./pi-adapter.js";
7
8
  import { RegistryError } from "./session-registry.js";
@@ -71,6 +72,16 @@ const creationCodes = new Set([
71
72
  "extension_not_installed", "extension_version_mismatch", "extension_load_failed", "extension_tool_conflict",
72
73
  "extension_tool_missing", "skill_load_failed", "sandbox_unavailable", "pi_session_creation_failed",
73
74
  ]);
75
+ const exactFields = ["permissionProfile", "provider", "model", "thinkingLevel", "shell"];
76
+ const setFields = ["skills", "extensions", "grantedExtensionTools"];
77
+ const sameSet = (left, right) => {
78
+ const a = new Set(left), b = new Set(right);
79
+ return a.size === left.length && b.size === right.length && a.size === b.size && [...a].every(value => b.has(value));
80
+ };
81
+ /** Admission compares every public capability field; a duplicate on either side is a mismatch. */
82
+ function samePublishedProfile(host, inner) {
83
+ return exactFields.every(key => host[key] === inner[key]) && setFields.every(key => sameSet(host[key], inner[key]));
84
+ }
74
85
  class InnerError extends Error {
75
86
  code;
76
87
  constructor(code) {
@@ -306,7 +317,7 @@ export class WriterRelayAdapter {
306
317
  const capabilities = await connection.call("pi_capabilities_get", {}, capabilitiesSchema, remaining(), input.signal);
307
318
  const inner = capabilities.executionProfiles.find(profile => profile.alias === input.executionProfile.alias);
308
319
  const host = this.options.executionProfiles.get(input.executionProfile.alias);
309
- if (!host || !inner || ["permissionProfile", "provider", "model", "thinkingLevel"].some(key => inner[key] !== host[key])) {
320
+ if (!host || !inner || !samePublishedProfile(projectExecutionProfile(host), inner)) {
310
321
  counts.settingsChanged++;
311
322
  continue;
312
323
  }
@@ -112,7 +112,9 @@ env -i HOME="$DISPOSABLE_HOME" PATH="$PATH" PI_SESSION_MCP_CONFIG="$ACCEPTANCE_C
112
112
  `skill_already_equivalent`, `skill_verified`, `skill_removed`,
113
113
  `skill_installed`, `skill_verified`, `skill_removed`); the skill file exists
114
114
  after steps 3 and 7 and is gone after steps 6 and 9; a foreign skill placed
115
- in the same skill directory stays unchanged throughout.
115
+ in the same skill directory stays unchanged throughout. From 0.14.0 on
116
+ (#139), that holds for both controller skills, `pi-session-orchestration`
117
+ and `pi-session-profiles`, each with its own finding in that order.
116
118
  - Steps 1 and 2 leave the client state unchanged.
117
119
  - After step 9 the `pi-session-mcp` registration is absent, the sentinel is still
118
120
  present, and the ownership record set is empty. The ownership directory has
@@ -2535,6 +2537,316 @@ Not established for `0.13.0`: the sandbox on a host that restricts unprivileged
2535
2537
  user namespaces; Git inside the sandbox on a linked worktree in live use (the
2536
2538
  bubblewrap-gated tests cover it); the macOS upgrade; Level 3.
2537
2539
 
2540
+ ### v0.13.1 post-release evidence
2541
+
2542
+ Recorded on 2026-10-29 for the published release `0.13.1`.
2543
+
2544
+ Tag binding: a fresh clone of the annotated tag `v0.13.1` (tag object
2545
+ `57c0786f1db3f732db797c8204e338ce17cf7fb2`) peels to the merge commit
2546
+ `c25c7687ecb66043a001c689c64e77be3cd25952` of #185, and
2547
+ `git describe --tags --exact-match` reports `v0.13.1`. CI on the merge commit
2548
+ (run 40832546742) and on the tag (run 40833087074) passed on the first attempt.
2549
+
2550
+ - Level 1 on the tag clone after `npm ci`, from 07:32 to 07:34 UTC with
2551
+ Node.js `26.12.1` and npm `12.2.1`: typecheck, `build:cli`, the test run
2552
+ (136 files, 3948 passed, 14 skipped, with
2553
+ `NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
2554
+ `git diff --check` exited with `0`, and the status stayed clean. Doctor
2555
+ reported `packageVersion` `0.13.1` and exited with `2` because no
2556
+ configuration was set (`config_missing`). The manifests were unchanged
2557
+ afterwards.
2558
+ - `pi-session-mcp@0.13.1` on npm: 922 files, shasum
2559
+ `e2e0e2652a2cab6bf891e61bcfa14754258eb59b`, `gitHead` `c25c768` (the merge
2560
+ commit). It was staged and approved by the maintainer; `npm pack` of the tag
2561
+ clone and the dry run of the publishing clone give the same shasum and file
2562
+ count.
2563
+ - Published npm package, Level 2 from a fresh prefix, 07:34 to 07:36 UTC:
2564
+ 160 of 160 with delivery off and 228 of 228 with `--events spool`; real
2565
+ registrations and built files unchanged, no process left, leak check passed.
2566
+ Clients: Codex CLI `0.162.1` and Claude Code `2.1.312`. The install smoke on
2567
+ the published package reported `smoke_passed` with version `0.13.1`.
2568
+ - The GitHub release `v0.13.1` was published the same day.
2569
+
2570
+ Operator upgrade of the workstation from `0.13.0` to `0.13.1` on 2026-10-29
2571
+ from 09:35 UTC, following
2572
+ [Installation](installation.md#container-writers): no writer container ran at
2573
+ the start; controller sessions without a coding session stayed open and need a
2574
+ reconnect. Both client targets were removed with `0.13.0`, then
2575
+ `npm install -g pi-session-mcp@0.13.1`. Doctor reported only
2576
+ `writer_not_ready` for the 18 writers. A dry run of each provisioning command
2577
+ with unchanged settings reported `mode_unchanged` and `writer_planned`; with
2578
+ `--apply`, five container repositories with 18 writers (2 with
2579
+ `--tags reachable`, 4, 4 with `--cache go`, 4 with base `dev`, 4 with
2580
+ `--cache uv`, three of them with operator images) reported `writer_accepted`
2581
+ from 09:36 to 09:39 UTC. Client-target `--apply` and `--verify` passed for
2582
+ `claude-code:user` and `codex:user` (`mcp_verified`, `skill_verified`, and
2583
+ `hook_verified` for the wake hook); `--writer <name> --verify` passed for all
2584
+ 18 writers. Doctor reported no error, only the known `workspace_gitfile` and
2585
+ `workspace_mode_unset` warnings of the worktree aliases.
2586
+
2587
+ Operator upgrade of the second machine (Linux) from `0.13.0` to `0.13.1` on
2588
+ 2026-10-29 from 09:47 UTC, with Node.js `24.22.0` and the same steps: no
2589
+ writer container and no server process of a client session ran at the start.
2590
+ Both client targets were removed with `0.13.0`, then
2591
+ `npm install -g pi-session-mcp@0.13.1`; Doctor reported only
2592
+ `writer_not_ready` for the 14 writers. The dry runs reported `mode_unchanged`
2593
+ and `writer_planned`; with `--apply`, four container repositories with 14
2594
+ writers (4, 2, 4 with an operator image and `--cache go`, 4 with an operator
2595
+ image) reported `writer_accepted` from 09:48 to 09:51 UTC. Client-target
2596
+ `--apply` and `--verify` passed for `claude-code:user` and `codex:user`
2597
+ (`mcp_verified`, `skill_verified`), and `--writer <name> --verify` passed for
2598
+ all 14 writers. Doctor reported no error, only `workspace_mode_unset` for one
2599
+ workspace without a mode.
2600
+
2601
+ ### v0.14.1 pre-release acceptance
2602
+
2603
+ `v0.14.1` was accepted on 2026-10-30 from 06:04 to 06:15 UTC on its release
2604
+ candidate `a239610`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
2605
+ Codex CLI `0.162.1` and Claude Code `2.1.314`. Level 2 passed in the working
2606
+ tree and for the packed tarball, each with delivery off and with delivery on.
2607
+ Level 1 passed except `npm audit`. Later commits on the release pull request
2608
+ change only this record.
2609
+
2610
+ The patch release ships Sprint 12 (#192, #193, #194) from `main` at `d9a47f5`;
2611
+ the candidate adds only the version, the changelog heading and the roadmap
2612
+ line. The MCP tool contract, result codes and configuration schema are
2613
+ unchanged. The source changes were accepted on the branch before the merge, see
2614
+ [Sprint 12 candidate acceptance](#sprint-12-candidate-acceptance-2026-10-30),
2615
+ including Level 3.
2616
+
2617
+ - Level 1 in the checkout of the candidate after `npm ci`: every command exited
2618
+ with `0`, except `npm audit`; `git status --short` printed nothing.
2619
+ - The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
2620
+ 141 files, 4044 passed and 14 skipped, and no unhandled error.
2621
+ - `npm audit` and `npm audit --omit=dev` exited with `1` and name only the
2622
+ known `undici` 8.10.2 finding through `@earendil-works/pi-coding-agent`
2623
+ 1.4.0, the latest SDK release, with eight advisories
2624
+ ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
2625
+ - `npm pack` listed 933 files (tarball shasum
2626
+ `47cf66a3856530112c11095d07cb05b7ab9892f4`, identical in three packs). A
2627
+ scan of the unpacked files for private names, hosts and the maintainer's
2628
+ home path found none.
2629
+ - Doctor reported `packageVersion` `0.14.1`. It exited with `2` because no
2630
+ configuration was set (`config_missing`).
2631
+ - Level 2 in the working tree: 160 of 160 checks passed with delivery off and
2632
+ 228 of 228 with `--events spool`.
2633
+ - Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
2634
+ 228 of 228 with `--events spool`. The install smoke from another fresh prefix
2635
+ reported `smoke_passed` with version `0.14.1`.
2636
+ - In every Level 2 run the real client registrations were unchanged, no process
2637
+ remained, the leak check passed and the disposable directories were removed.
2638
+ - Podman-gated writer tests on the candidate (Podman `6.1.4`): 8 of 8 passed
2639
+ (7 lifecycle tests, including the detached-`HEAD` export of #193, and 1
2640
+ boundary test).
2641
+ - Bubblewrap-gated sandbox tests (`test/sandbox-shell.bwrap.test.ts`, bubblewrap
2642
+ `0.13.1`): 12 of 12 passed, none skipped.
2643
+
2644
+ Not established for `0.14.1` beyond the Sprint 12 record: a Level 3 cycle on
2645
+ the release candidate itself, and the published-release Level 2 and upgrades,
2646
+ which follow after publishing.
2647
+
2648
+ ### Sprint 12 candidate acceptance (2026-10-30)
2649
+
2650
+ Recorded on 2026-10-29 and 2026-10-30 for the unreleased branch
2651
+ `fix/s12-audit-findings` ([Sprint 12](sprints/2026-10-29-s12-audit-fixes.md),
2652
+ #192, #193, #194). No release, tag or npm publish.
2653
+
2654
+ - Level 1 on the final source `9426682` in a separate detached worktree with
2655
+ Node.js `26.12.1`: `npm ci`, typecheck, `build:cli`, the test run (530 files,
2656
+ 4044 passed, 14 skipped, 0 failed, no unhandled error, with
2657
+ `NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
2658
+ `git diff --check 9d9cf8c..HEAD` exited with `0`. The same on the earlier
2659
+ candidate `896e29d`: 4043 passed, 0 failed.
2660
+ - Real rootless Podman with the pinned Node image (git `2.39.5`):
2661
+ `test/writer-lifecycle.podman.test.ts` 7 of 7, including the new clean
2662
+ detached-HEAD case, which fails against the source of `9d9cf8c`. In the image,
2663
+ `core.useReplaceRefs=false` makes a graft-hidden detached commit visible
2664
+ again.
2665
+ - Level 2 on `npm pack` of `896e29d`, installed into a fresh prefix below the
2666
+ home directory: 160 of 160 with delivery off and 228 of 228 with
2667
+ `--events spool`; real registrations and built files unchanged, no process
2668
+ left, path-leak scan 0 hits. Clients: Codex CLI `0.162.1` and Claude Code
2669
+ `2.1.312`. The two later commits change the guard's Git arguments and a
2670
+ regular-expression spelling; Level 2 was not repeated for them.
2671
+ - Live profile change (item D), on an expendable test configuration: a
2672
+ candidate with the new read-only profile `review-sonnet`
2673
+ (`anthropic/claude-sonnet-5-5`), Doctor without error, probe dry run, one
2674
+ probe turn (`probe_passed`), timestamped backup and atomic replacement,
2675
+ Doctor, then real use by a fresh server for the R1 and final reviews; rolled
2676
+ back to the backup afterwards, Doctor without error. No production
2677
+ configuration or writer was touched.
2678
+ - Live container writer (items B and C), with the packed candidate, an
2679
+ expendable configuration and a synthetic repository: profile `s12-code`
2680
+ (`deepseek/deepseek-flash`, coding, skill `pi-session-implementation`),
2681
+ one probe turn (`probe_passed`); `--repo s12-probe --mode container
2682
+ --profiles s12-code --writers 1 --apply` reported `writer_provisioned` and
2683
+ `writer_accepted`, `--writer s12-probe-w1 --verify` `mcp_verified`. A real
2684
+ relayed coding session completed its task; `--collect --apply` imported
2685
+ `refs/remotes/writer-s12-probe-w1/s12-live-c`, whose commit passed the
2686
+ oracle (4 of 4 tests), and `--finish --apply` reported `writer_reset`. With
2687
+ the host profile's skills then changed, a start failed with
2688
+ `writer_unavailable`, reason `none_admitted`, counts
2689
+ `settingsChanged: 1`, and collection afterwards found `nothing_to_collect`.
2690
+ After `--writer s12-probe-w1 --remove` and the provisioning request again,
2691
+ the writer was accepted and verified and a start succeeded. Rolled back:
2692
+ writer removed, configuration, repository and prefix deleted.
2693
+ - Level 3, ten-task cycle, on the final source `9426682` (`dist/main.js`
2694
+ SHA-256 `ab93e3a932b1d5147dcbb48bbb85d9442c2214e486101f8d427360b99672cfdb`)
2695
+ from a separate detached checkout, with a temporary credential-free
2696
+ configuration (`l3-read` read-only and `l3-code` coding, both
2697
+ `deepseek/deepseek-flash`), synthetic fixtures, events off, one probe turn
2698
+ per profile as preflight. Claude Code `2.1.312` ran as `claude -p` with a
2699
+ per-invocation `--mcp-config`, `--strict-mcp-config` and
2700
+ `--model claude-sonnet-5-5`; Codex CLI `0.162.1` as `codex exec
2701
+ --ignore-user-config` with `-c mcp_servers.*` overrides. Both clients could
2702
+ wait with `sleep` between polls.
2703
+ - Cycle 1 was not accepted: 5 of 10. The worker ignored a requested
2704
+ ordering in both follow-ups (T2), refused the abort task's essay prompt
2705
+ within about 1.5 seconds so that nothing was left to abort (T5), and the
2706
+ Codex coding snapshot could not be written from the Codex sandbox (T3). A
2707
+ path hit came from the worker's `assistantText`, not from server
2708
+ metadata. No Pi Session MCP defect was found; the cycle is kept unchanged
2709
+ with its own pre-registration.
2710
+ - Cycle 2, with a new pre-registration before its first task (order-free
2711
+ LOW oracle, a long deterministic listing as the abort prompt, snapshots
2712
+ inside the controller directory, the leak scan separating server metadata
2713
+ from `assistantText`): 10 of 10. Read-only fixture digests unchanged; both
2714
+ coding snapshots pass their oracle and the fixture tests and show only the
2715
+ allowed files; both abort tasks show running, abort, `aborted`, close, an
2716
+ empty `pi_session_list` and a fresh `idle` session. Zero leak hits, also in
2717
+ `assistantText`; no server or client process before or 10 seconds after any
2718
+ invocation. Polls per turn: Claude Code 1 to 2, Codex 2 to 4. Deviation:
2719
+ the Codex sandbox refused to spawn `git`, so the Codex snapshots' status
2720
+ lists were derived from the barrier snapshot trees against `HEAD`.
2721
+ The cycle-2 gate holds.
2722
+
2723
+ Not established: a release of these fixes, the operator rollout on either
2724
+ machine, macOS, and #120.
2725
+
2726
+ ### v0.14.0 post-release evidence
2727
+
2728
+ Recorded on 2026-10-29 for the published release `0.14.0`.
2729
+
2730
+ Tag binding: a fresh clone of the annotated tag `v0.14.0` (tag object
2731
+ `f3a8b088a85fd5825c3256f4a7950ba051cbf388`) peels to the merge commit
2732
+ `fd4fdeacf9d02c04b6fd89314f7ec819b5506ed9` of #190, and
2733
+ `git describe --tags --exact-match` reports `v0.14.0`. CI on the merge commit
2734
+ (run 40928709705) and on the tag (run 40929602738) passed on the first attempt.
2735
+ The tag was created after the npm package had been published; the package
2736
+ records `gitHead` `d904fed`, the head of #190, whose tree (`3ae966b`) is
2737
+ identical to the merge commit's.
2738
+
2739
+ - Level 1 on the tag clone after `npm ci`, from 18:18 to 18:20 UTC with
2740
+ Node.js `26.12.1` and npm `12.2.1`: typecheck, `build:cli`, the test run
2741
+ (139 files, 4005 passed, 14 skipped, with
2742
+ `NODE_OPTIONS=--unhandled-rejections=strict`), `build` and
2743
+ `git diff --check` exited with `0`, and the status stayed clean. Doctor
2744
+ reported `packageVersion` `0.14.0` and exited with `2` because no
2745
+ configuration was set (`config_missing`).
2746
+ - `pi-session-mcp@0.14.0` on npm: 932 files, shasum
2747
+ `579cc111a7aa62987b5f35e79b34b69853c8a0e3`. It was published by the
2748
+ maintainer; `npm pack` of the tag clone gives the same shasum and file count.
2749
+ - Published npm package, Level 2 from a fresh prefix, 18:19 to 18:21 UTC:
2750
+ 160 of 160 with delivery off and 228 of 228 with `--events spool`; real
2751
+ registrations and built files unchanged, no process left, leak check passed.
2752
+ Clients: Codex CLI `0.162.1` and Claude Code `2.1.312`. The install smoke on
2753
+ the published package reported `smoke_passed` with version `0.14.0`.
2754
+ - The GitHub release `v0.14.0` was published the same day.
2755
+
2756
+ Operator upgrade of the workstation from `0.13.1` to `0.14.0` on 2026-10-29
2757
+ from 18:33 UTC, following
2758
+ [Installation](installation.md#container-writers): no Pi session of this
2759
+ controller was open; other controller sessions without a coding session stayed
2760
+ open and need a reconnect. Both client targets were removed with `0.13.1`, then
2761
+ `npm install -g pi-session-mcp@0.14.0`. Doctor reported only
2762
+ `writer_not_ready` for the 18 writers. A dry run of the first provisioning
2763
+ command with unchanged settings reported `mode_unchanged` and
2764
+ `writer_planned`; with `--apply`, five container repositories with 18 writers
2765
+ (2 with `--tags reachable`, 4, 4 with `--cache go`, 4 with base `dev`, 4 with
2766
+ `--cache uv`, three of them with operator images) reported `writer_accepted`
2767
+ from 18:34 to 18:37 UTC. Client-target `--apply` and `--verify` passed for
2768
+ `claude-code:user` and `codex:user` (`mcp_verified`, `skill_verified` for both
2769
+ controller skills, and `hook_verified` for the wake hook); `--writer <name>
2770
+ --verify` passed for all 18 writers. Doctor reported no error, only the known
2771
+ `workspace_gitfile` and `workspace_mode_unset` warnings of the worktree
2772
+ aliases.
2773
+
2774
+ Operator upgrade of the second machine (Linux) from `0.13.1` to `0.14.0` on
2775
+ 2026-10-29 from 18:38 UTC, with Node.js `24.22.0` and the same steps; one
2776
+ controller session's server process stayed open and needs a reconnect. Both
2777
+ client targets were removed with `0.13.1`, then
2778
+ `npm install -g pi-session-mcp@0.14.0`; Doctor reported only
2779
+ `writer_not_ready` for the 14 writers. With `--apply`, four container
2780
+ repositories with 14 writers (4, 2, 4 with an operator image and `--cache go`,
2781
+ 4 with an operator image) reported `writer_accepted` from 18:39 to 18:42 UTC.
2782
+ Client-target `--apply` and `--verify` passed for `claude-code:user` and
2783
+ `codex:user` (`mcp_verified`, `skill_verified` for both controller skills), and
2784
+ `--writer <name> --verify` passed for all 14 writers. Doctor reported no
2785
+ error, only `workspace_mode_unset` for one workspace without a mode.
2786
+
2787
+ Not established for `0.14.0` after the upgrade: a coding session through a
2788
+ re-accepted writer, an agent-driven profile change end to end, and Level 3.
2789
+
2790
+ ### v0.14.0 pre-release acceptance
2791
+
2792
+ `v0.14.0` was accepted on 2026-10-29 from 17:56 to 18:01 UTC on its release
2793
+ candidate `6e18659`, on Linux (`x64`) with Node.js `26.12.1`, npm `12.2.1`,
2794
+ Codex CLI `0.162.1` and Claude Code `2.1.312`. Level 2 passed in the working
2795
+ tree and for the packed tarball, each with delivery off and with delivery on.
2796
+ Level 1 passed except `npm audit`. Later commits on the release pull request
2797
+ change only this record.
2798
+
2799
+ The minor release ships #139 ([ADR 0014](adr/0014-profile-probe-and-model-catalog.md))
2800
+ and #188. The MCP tool contract is unchanged. Setup gains `--list-models` and
2801
+ `--probe-profile`, installs a second controller skill (`pi-session-profiles`)
2802
+ and so reports one more skill finding per client target; Doctor gains the
2803
+ check `skill_controller_selected`.
2804
+
2805
+ The first working-tree run on the earlier candidate `08fc620` failed 36 of 160
2806
+ checks: the Level 2 harness still expected one controller skill finding, so
2807
+ every step saw one finding too many. Setup itself reported the expected
2808
+ findings. The candidate `6e18659` teaches the harness both controller skills
2809
+ with the same number of checks; every result below is from that candidate.
2810
+
2811
+ - Level 1 in a clean worktree of the candidate after `npm ci`: every command
2812
+ exited with `0`, except `npm audit`; `git status --short` printed nothing.
2813
+ - The test run, with `NODE_OPTIONS=--unhandled-rejections=strict`, reported
2814
+ 139 files, 4005 passed and 14 skipped, and no unhandled error.
2815
+ - `npm audit` and `npm audit --omit=dev` exited with `1` and name only the
2816
+ known `undici` 8.10.2 finding through `@earendil-works/pi-coding-agent`
2817
+ 1.4.0, the latest SDK release, with eight advisories
2818
+ ([#120](https://github.com/plagor44-ux/pi-session-mcp/issues/120)).
2819
+ - `npm pack` listed 932 files (tarball shasum
2820
+ `4eb9b77c9484292a8533c6be0fda44ac18656787`, identical in three packs). A
2821
+ scan of the unpacked files for private names, hosts and the maintainer's
2822
+ home path found none.
2823
+ - Doctor reported `packageVersion` `0.14.0`. It exited with `2` because no
2824
+ configuration was set (`config_missing`).
2825
+ - Level 2 in the working tree: 160 of 160 checks passed with delivery off and
2826
+ 228 of 228 with `--events spool`.
2827
+ - Packed-tarball Level 2 from a fresh prefix: 160 of 160 with delivery off and
2828
+ 228 of 228 with `--events spool`. The install smoke from another fresh prefix
2829
+ reported `smoke_passed` with version `0.14.0`.
2830
+ - In every Level 2 run the real client registrations were unchanged, no process
2831
+ remained, the leak check passed and the disposable directories were removed.
2832
+ - Podman-gated writer tests on the candidate (Podman `6.1.4`): 7 of 7 passed.
2833
+ - Bubblewrap-gated sandbox tests (`test/sandbox-shell.bwrap.test.ts`, bubblewrap
2834
+ `0.13.1`): 12 of 12 passed, none skipped.
2835
+ - Live checks before the candidate, on the merged #139 code: `--list-models`
2836
+ against a real local Pi installation reported authentication for the
2837
+ configured providers, the probe dry run planned two profiles, and one
2838
+ `--probe-profile <alias> --apply` returned `probe_passed` with one provider
2839
+ turn.
2840
+ - Review: #139 had three cross-family reviews (author family OpenAI and
2841
+ Anthropic, reviewer family GLM) and #188 one, each without blocking findings;
2842
+ their findings were fixed or declined with a reason in #187 and #189. The
2843
+ harness fix in this release was checked by its live run above.
2844
+
2845
+ Not established for `0.14.0`: an agent-driven profile change end to end with
2846
+ the `pi-session-profiles` skill, a container writer acceptance of a newly
2847
+ created profile, client-target `--verify` through the installed package, which
2848
+ follows the upgrade; Level 3.
2849
+
2538
2850
  ### v0.13.1 pre-release acceptance
2539
2851
 
2540
2852
  `v0.13.1` was accepted on 2026-10-29 from 06:40 to 06:48 UTC on its release
@@ -161,12 +161,16 @@ base.
161
161
  In both cases the host server closes that launcher and tries the next
162
162
  candidate. The inner writer guard stays the authority for baseline admission
163
163
  inside the boundary.
164
- - Before the inner start the relay compares the requested profile's entry in
165
- the writer's validated `pi_capabilities_get` (permission profile, provider,
166
- model, thinking level) with the host's configured profile and moves on with
167
- `writer_settings_changed` on any difference, so a writer never runs an
168
- outdated version of a host profile. The launcher cannot do this check: it
169
- starts before the profile is known.
164
+ - Before the inner start the relay compares every public capability field of
165
+ the requested profile's entry in the writer's validated `pi_capabilities_get`
166
+ with the host's configured profile: `permissionProfile`, `provider`, `model`,
167
+ `thinkingLevel` and `shell` exactly, and `skills`, `extensions` and
168
+ `grantedExtensionTools` as sets. A mismatch or a missing entry counts as
169
+ `settingsChanged`, starts no inner session and moves on to the next
170
+ candidate, so a writer never runs an outdated version of a host profile. The
171
+ launcher cannot do this check: it starts before the profile is known.
172
+ (Amended 2026-10-29, #194: the comparison covers every public capability
173
+ field, not only permission profile, provider, model and thinking level.)
170
174
  - If no candidate is admitted, the start fails with `writer_unavailable`, a
171
175
  content-free reason and content-free counts. `none_ready`: no writer of the
172
176
  workspace is ready, migrated and has a recorded acceptance (whether that
@@ -145,6 +145,36 @@ directly after `grantedExtensionTools`. This is configured intent only.
145
145
  MCP tools and the granted extension tools. A profile with granted extension
146
146
  tools cannot have a sandboxed shell.
147
147
 
148
+ ### Amendment 2026-10-29: context files
149
+
150
+ Issue #192. Previously every resource loader kept the SDK behaviour: Pi
151
+ context files (`AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`,
152
+ `CLAUDE.MD`) were loaded from the session directory and every ancestor
153
+ directory, and a context file reached through a symlink was followed to its
154
+ target, so a read-only session also loaded context from outside the workspace.
155
+
156
+ New rule: for **read-only profiles** every resource loader keeps only context
157
+ files whose real path lies inside the workspace's real path, on every reload
158
+ and before the first provider request. A symlinked context file that resolves
159
+ outside the workspace, a dangling one, and the context files of ancestor
160
+ directories above the workspace (including a main repository above a linked
161
+ worktree) are not loaded for read-only profiles; the linked worktree's own file
162
+ inside the workspace is. Coding profiles keep the SDK behaviour (the working
163
+ directory and its ancestors).
164
+
165
+ The filter runs on the SDK's result, after the SDK has picked the first
166
+ existing name per directory. If that file is dropped, the directory contributes
167
+ no context file: an `AGENTS.override.md` that resolves outside the workspace
168
+ also hides an `AGENTS.md` beside it. This fails closed. A kept file's content
169
+ is read again from its verified real path, so a link swapped after the SDK
170
+ read cannot bring in outside content.
171
+
172
+ Why: the read roots of this ADR. Section 1 confines the read tools of
173
+ read-only profiles to the workspace and the skill directories; context files
174
+ are model input rather than tool reads, and before this amendment they were the
175
+ one channel through which content from outside those roots still reached a
176
+ read-only session.
177
+
148
178
  ## Contract change and versioning
149
179
 
150
180
  The capabilities field `shell`, the start error `sandbox_unavailable` and the
@@ -0,0 +1,89 @@
1
+ # ADR 0014: Profile probe and offline model catalog
2
+
3
+ **Status:** Accepted (2026-10-29); released in 0.14.0. Issue #139.
4
+
5
+ ## Context
6
+
7
+ The running MCP server reads its configuration once at startup. A controlling
8
+ agent cannot reconnect its own server to prove a changed execution profile.
9
+ Asking for a manual reconnect prevents autonomous profile setup; starting a
10
+ nested controller would add another agent without testing a different boundary.
11
+ The agent also needs an authoritative offline catalog rather than guessing
12
+ provider and model ids or inspecting credential files.
13
+
14
+ ## Decision
15
+
16
+ ### 1. Offline model catalog
17
+
18
+ `pi-session-mcp-setup --list-models` reports provider ids, model ids and only
19
+ whether authentication is configured. Package-root `ModelRuntime` access sits
20
+ behind injectable `ModelCatalog`. Creation disables model network access and
21
+ refresh (`allowModelNetwork: false`, `refreshOnCreate: false`); the catalog
22
+ uses only `getProviders`, `getModels` and `checkAuth`. `checkAuth` reads the
23
+ local auth configuration offline and returns only its source and kind; the
24
+ catalog reduces that to a boolean and bounds each lookup. `hasConfiguredAuth`
25
+ is not used because it reads a snapshot that only an online availability
26
+ refresh fills, so offline it always reports no authentication. The catalog
27
+ never calls `getAuth` or `getAvailable`. This is a second narrow exception
28
+ next to the writer wrapper's credential refresh (#142), not general permission
29
+ to bypass `PiSessionAdapter`.
30
+
31
+ Providers registered only by extensions are not listed; every catalog report
32
+ states this limitation. Invalid catalog ids are omitted, never echoed.
33
+
34
+ ### 2. Opt-in profile probe
35
+
36
+ `--probe-profile review,code` runs Doctor and offline profile, provider, model
37
+ and authentication-presence checks. Only adding `--apply` authorizes one fixed
38
+ PONG turn per passing profile, sequentially, through `PiSessionAdapter`. The
39
+ production dependency is `SdkPiSessionAdapter`, resolving the profile as the
40
+ server does and protecting both the named and canonical configuration
41
+ directories from read-root expansion through skills.
42
+
43
+ Doctor blocks on global configuration/build/version errors and errors about
44
+ the requested profiles. Other errors and warnings are reported by check id
45
+ without blocking this request. Tests inject a fake catalog, adapter and Doctor;
46
+ no provider credentials are needed.
47
+
48
+ The session uses a private empty temporary directory, mode 0700, created
49
+ internally and removed after bounded session cleanup. This is a narrow
50
+ exception to the configured-workspace rule: the directory is not a workspace
51
+ alias and is never supplied as a tool or CLI argument. It avoids exposing a
52
+ real repository as the probe's working directory; it does not sandbox trusted
53
+ extensions or change coding profiles' existing authority.
54
+
55
+ Start, turn and cleanup deadlines are 60, 120 and 10 seconds respectively.
56
+ Interrupts abort and dispose the current session and skip remaining profiles.
57
+ The fixed prompt is not configurable, the reply text is never reported, usage
58
+ is `unknown`, and each finding records whether a provider turn was accepted.
59
+ No event spool is enabled.
60
+
61
+ ### 3. Guarded, lazy CLI entry
62
+
63
+ Ordinary setup requests never import the probe entry or install its stdout
64
+ guard. The catalog and probe load it dynamically after the platform gate;
65
+ the catalog needs no configuration. Its first import installs the same stdout
66
+ guard as the server before SDK evaluation. Only the guard's transport writer
67
+ emits reports; foreign stdout, including extension `console.log`, is dropped.
68
+ Adapter diagnostics are structured JSON on stderr without prompts or secrets.
69
+ Operational exceptions produce fixed refusal codes rather than raw errors.
70
+
71
+ ## Contract change and versioning
72
+
73
+ The new setup flags and report schemas are additive public CLI contract
74
+ changes under [ADR 0003](0003-post-mvp-maintenance.md#versioning). This decision
75
+ does not itself bump the version or authorize publication.
76
+
77
+ ## Consequences
78
+
79
+ - Doctor and `--verify` remain provider-free. Only
80
+ `--probe-profile … --apply` uses a provider among setup operations.
81
+ - Catalog and probe run on Linux and macOS without acquiring the setup lock
82
+ or changing registrations or configuration.
83
+ - The probe proves the profile on the host using the host Pi agent directory.
84
+ Container acceptance remains Doctor's `container_profile_refused` check and
85
+ writer provisioning, not this probe.
86
+ - A successful probe does not reload any running controller's configuration;
87
+ an MCP server restart or reconnect is still required.
88
+ - Reports expose only sanitized findings, durations and provider-turn presence,
89
+ not paths, credential values, environment values, prompts or reply text.
@@ -53,6 +53,9 @@ npm run --silent setup -- --apply --target codex:user:pi-session-mcp
53
53
  npm run --silent setup -- --verify --target codex:user:pi-session-mcp
54
54
  npm run --silent setup -- --rollback --target codex:user:pi-session-mcp
55
55
  npm run --silent setup -- --remove --target codex:user:pi-session-mcp
56
+ npm run --silent setup -- --list-models --json
57
+ npm run --silent setup -- --probe-profile review,code --json
58
+ npm run --silent setup -- --probe-profile review,code --apply --json
56
59
  ```
57
60
 
58
61
  Setup runs the previously built `dist/setup-main.js`. The dry-run operation
@@ -109,27 +112,30 @@ inspected:
109
112
  None of the failure rows changes a registration or starts the handshake, and
110
113
  each exits with `1`.
111
114
 
112
- ### Controller skill
115
+ ### Controller skills
113
116
 
114
- Besides the registration, `--apply` installs the controller skill
115
- `pi-session-orchestration` for each target, as a real copy of the effective
116
- skill: your override in the skill library if you have one, else the skill
117
- shipped in the package ([Pi skills](skills.md)). Clients discover it without
118
- further steps.
117
+ Besides the registration, `--apply` installs the controller skills
118
+ `pi-session-orchestration` (delegating work to Pi workers) and
119
+ `pi-session-profiles` (creating, changing and retiring execution profiles) for
120
+ each target, each as a real copy of the effective skill: your override in the
121
+ skill library if you have one, else the skill shipped in the package
122
+ ([Pi skills](skills.md)). Clients discover them without further steps.
119
123
 
120
- | Target | Where the skill goes |
124
+ | Target | Where the skills go |
121
125
  | --- | --- |
122
- | `codex:user:pi-session-mcp` | `~/.agents/skills/pi-session-orchestration` |
123
- | `claude-code:user:pi-session-mcp`, `claude-code:local:pi-session-mcp` | `~/.claude/skills/pi-session-orchestration` (one shared directory) |
124
- | `claude-code:project:pi-session-mcp` | `<project>/.claude/skills/pi-session-orchestration`, where `<project>` is the directory in which setup runs |
126
+ | `codex:user:pi-session-mcp` | `~/.agents/skills/<skill>` |
127
+ | `claude-code:user:pi-session-mcp`, `claude-code:local:pi-session-mcp` | `~/.claude/skills/<skill>` (one shared directory per skill) |
128
+ | `claude-code:project:pi-session-mcp` | `<project>/.claude/skills/<skill>`, where `<project>` is the directory in which setup runs |
125
129
 
126
- Every result therefore has a second finding per target, with the same client
127
- and scope and the alias `pi-session-orchestration`:
130
+ `<skill>` is `pi-session-orchestration` or `pi-session-profiles`. Every result
131
+ therefore has one more finding per skill and target, with the same client and
132
+ scope and the skill name as alias:
128
133
 
129
134
  ```text
130
135
  apply: ok
131
136
  claude-code/user/pi-session-mcp: ok (applied)
132
137
  claude-code/user/pi-session-orchestration: ok (skill_installed)
138
+ claude-code/user/pi-session-profiles: ok (skill_installed)
133
139
  ```
134
140
 
135
141
  Skill findings per operation:
@@ -28,6 +28,7 @@ and owners are:
28
28
  | `config_missing` | `PI_SESSION_MCP_CONFIG` is not set, or the file it names is absent | `configuration` |
29
29
  | `config_unreadable` | The supported configuration path cannot be read | `configuration` |
30
30
  | `config_invalid` | Production `loadConfig` rejects the configuration. One check per schema issue location, at most five, sorted | `configuration:<location>`, a dotted path of schema field names, aliases and array indexes such as `configuration:executionProfiles.review.default`; an unknown key that is not a schema field name, and any other key that is not a valid alias, is shown as `<key>`, because it could be a path or a secret. `configuration` when the error has no location |
31
+ | `skill_controller_selected` | Production `loadConfig` rejects the configuration because a `skillCatalog` value names a controller skill (`builtin:pi-session-orchestration` or `builtin:pi-session-profiles`). Reported instead of `config_invalid` for that location and blocks like it | `configuration:skillCatalog.<alias>` |
31
32
  | `workspace_missing` | A resolved configured workspace does not exist | validated workspace alias |
32
33
  | `workspace_not_directory` | A resolved workspace is not a directory | validated workspace alias |
33
34
  | `workspace_unreadable` | A resolved workspace cannot be read | validated workspace alias |