pi-session-mcp 0.8.0 → 0.8.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.
@@ -346,18 +346,61 @@ Pi Session MCP tools; the script never does.
346
346
  from the harness of `cc810c9` on, a SHA-256 over every built module in
347
347
  `dist/`; `run` refuses a changed build.
348
348
  - Configuration: `prepare --base-config <operator config> --from-profile
349
- <alias>` copies only provider, model and thinking level of one working profile
350
- without extensions or MCP servers into a temporary configuration with two
351
- profiles: `accept-read` (default, `read-only`, skill `review`) and
349
+ <alias>` copies only provider, model and thinking level of the one explicitly
350
+ selected profile into a run configuration with two profiles: `accept-read` (default, `read-only`, skill `review`) and
352
351
  `accept-code` (`coding`, skills `implementation` and `handshake`). From the
353
352
  harness of `3f98104` on, all three are `builtin:` references: the two worker
354
353
  skills ship with the candidate, and the `handshake` skill sits in the
355
354
  override directory `skills/` next to the run configuration, so the run also
356
355
  exercises library resolution. The `handshake` skill is generated per run and
357
- holds a random token that no controller prompt contains.
356
+ holds a random token that no controller prompt contains. A profile with
357
+ `mcpServers` is always rejected. A profile with `extensions` is rejected
358
+ unless `--with-extensions` is given; with it, `prepare` also copies exactly
359
+ that profile's extension allowlist (1 to 8 distinct exact
360
+ `npm:<name>@<version>` pins) into both run profiles and installs nothing.
361
+ `--with-extensions` on a profile without extensions is rejected. Nothing else
362
+ is copied: the run has its own workspaces and skills.
363
+ - Working directory: `--work-dir` (default `~/.cache/pi-session-mcp-acceptance`)
364
+ must not be inside a temporary directory (the system temporary directory,
365
+ `/tmp`, `/var/tmp` or `/dev/shm`), because Codex refuses helper binaries
366
+ there.
358
367
  - Fixtures: four small synthetic git repositories per client (`slug`, `stats`,
359
368
  `wait`, `range`), each a separate workspace alias (`cc-*` for Claude Code,
360
369
  `cx-*` for Codex). Both clients use the same configuration and build.
370
+ - Fixture baselines: when it creates a fixture, `prepare` stores its initial
371
+ commit and a manifest of its files (path, type, mode, SHA-256; without
372
+ `.git/`) under the run's private directory, never inside the worker's
373
+ workspace. `run` refuses a fixture that differs from its baseline before the
374
+ launch.
375
+ - Bound resources: `prepare` records a digest of the candidate build, the
376
+ installed Claude Code controller skill copy, the candidate skill that the
377
+ Codex controller's link points to, the worker skills (`implementation`,
378
+ `review`), the `handshake` skill, the override directory, the run
379
+ configuration, the Claude MCP configuration, the fixture baselines, the
380
+ harness itself, and the `pi-session-orchestration` copies in the operator's
381
+ home (`~/.claude/skills` and `~/.agents/skills`, recorded as missing when
382
+ absent). `run` rechecks all of them before the launch and again after the
383
+ run. A change before the launch refuses the run; a change after it fails the
384
+ result with `resourcesChanged`.
385
+ - Home controller skills: Claude Code prefers a personal skill over a project
386
+ skill of the same name. With `--setting-sources project,local`, as the
387
+ harness starts it, Claude Code `2.1.296` listed only the project copy when a
388
+ different personal copy existed (observed on 2026-10-12 with a marker skill);
389
+ without that flag the personal copy shadowed the project copy. Re-check this
390
+ when the client version changes. Codex's precedence between its home skills
391
+ and the run's repository skill is not established, so a Codex run is refused
392
+ while `~/.agents/skills/pi-session-orchestration` differs from the
393
+ candidate's skill.
394
+ - Launch record and failed analysis: `run` records the launch once before the
395
+ controller starts, so an attempt that ends without a result is never repeated
396
+ silently. If the analysis after the controller fails, the result is recorded
397
+ as `analysisFailed` and never passes, and the run's own processes are still
398
+ terminated. A `SIGINT` or `SIGTERM` that the harness handles during the
399
+ analysis records the result at once as `harnessAborted` and `analysisFailed`,
400
+ keeps the supervision's flags (`timedOut`, `spawnFailed`, `unterminated`),
401
+ terminates the run's recorded processes and exits with 1. A harness that ends
402
+ without handling a signal (for example `SIGKILL`, `SIGHUP` or out of memory)
403
+ writes no result; `evidence` lists its launch as one without a result.
361
404
  - Controller skill: from the harness of `3f98104` on, `prepare` installs the
362
405
  Claude Code controller skill the way an operator does, with the candidate's
363
406
  `pi-session-mcp-setup --apply --target claude-code:project:pi-session-mcp` in
@@ -377,6 +420,18 @@ Pi Session MCP tools; the script never does.
377
420
  approvals from [client setup](client-setup.md#run-codex-without-prompts).
378
421
  Neither run changes a persistent registration, `HOME` or `CODEX_HOME`, and
379
422
  both use the existing authentication.
423
+ - Time limit: `run --timeout-min` takes a number above 0 and at most 180
424
+ (default 25). After the limit the harness sends `SIGTERM` to the client's
425
+ process group and waits up to 10 seconds. It then sends `SIGKILL` to that
426
+ group and to the recorded processes of the run whose PID and start time still
427
+ match, never by name. A process that is only found by its working directory
428
+ inside the run is reported, not killed. The run is complete when the client
429
+ exits and a bounded drain of its output has finished; a client that the
430
+ escalation cannot reap still ends the wait and is marked `unterminated`. The
431
+ result classifies `timedOut`, `spawnFailed`, `harnessAborted` (`SIGINT` or
432
+ `SIGTERM` to the harness) and `analysisFailed`; the CLI exits 1 for each of
433
+ them. The original result of a client
434
+ and scenario is written once; a second `run` for the same pair is refused.
380
435
 
381
436
  ### Scenarios
382
437
 
@@ -392,17 +447,70 @@ Each client runs two invocations, one per scenario:
392
447
  - The script watches `/proc` during the run and records every process of the
393
448
  controller tree, every server of this run (entry point and configuration
394
449
  path) and every process whose working directory lies inside the run, by PID
395
- and start time. After the controller exits and the server's 15-second
396
- shutdown bound has passed, none of them may remain.
450
+ and start time. A later sighting of a process never lowers its role or drops
451
+ its fixture: an exiting server, or a zombie its client has not reaped yet,
452
+ has no command line, environment or working directory left to read, and a
453
+ descendant that outlives the client keeps the ownership that makes it the
454
+ run's own to terminate. After the controller exits and the server's
455
+ 15-second shutdown bound has passed, none of them may remain.
397
456
  - From the controller transcript it extracts only tool names, aliases, states,
398
457
  timestamps and counts. It scans every Pi Session MCP result, with
399
458
  `assistantText` removed, for local paths, the token and credential markers,
400
459
  and counts mentions in `assistantText` separately.
401
- - Fixture oracles run on the workspace after the controller exits: `node
402
- --test` and `git status` per fixture. Every changed file must have been last
403
- written during a worker turn of its task's session, and `HANDSHAKE.txt`
404
- during the follow-up turn, by the server's turn timestamps. This catches a
405
- write outside the worker turns, not one during them.
460
+ - Scope: after the controller exits, each fixture is compared with its
461
+ baseline in three independent ways: the working tree against the manifest
462
+ (including untracked, ignored, deleted, renamed and mode-changed files), the
463
+ index against the base commit, and `HEAD` against the base commit. A change
464
+ that a worker committed therefore counts. The union of changed paths must lie
465
+ inside the files allowed for that task. A missing or unreadable baseline,
466
+ `.git` or base commit fails the check; it is never read as clean. Git runs
467
+ pinned to the fixture with hooks and `fsmonitor` switched off.
468
+ - Business checks: besides the fixture's own `node --test`, the harness runs
469
+ its own cases per fixture (`slug`, `stats`, `range`) against the worker's
470
+ module in a separate `node` process, because a worker could have edited the
471
+ tests it is judged by. For `stats` one case passes a frozen input array, so
472
+ an implementation that sorts its input in place fails. The fixture's tests
473
+ and the business checks must both pass.
474
+ - Every changed file must have been last written during a worker turn of its
475
+ task's session, and `HANDSHAKE.txt` during the follow-up turn, by the
476
+ server's turn timestamps. This catches a write outside the worker turns, not
477
+ one during them.
478
+ - Acceptance verdict: each result carries `acceptance` with `passed` and a
479
+ list of `failures`, computed mechanically from the checks. `passed` is true
480
+ only when the list is empty. A failure code is listed when any of these
481
+ does not hold:
482
+ - the controller exit code is 0 (`controllerExit`), neither `timedOut`,
483
+ `spawnFailed` nor `harnessAborted` is set, and the analysis finished
484
+ (`analysisFailed`);
485
+ - no bound resource changed after the run (`resourcesChanged`), and for a
486
+ reanalysis no resource or fixture differs from the state recorded at the
487
+ end of the run (`stateChangedSinceRun`);
488
+ - at least one server of the run was observed (`noServerObserved`) and no
489
+ process of the run remains (`residualProcesses`);
490
+ - the controller called `pi_capabilities_get` first (`capabilitiesNotFirst`),
491
+ saw the candidate version and configuration fingerprint
492
+ (`candidateNotConfirmed`) and saw exactly the skills `implementation` and
493
+ `handshake` on `accept-code` (`workerSkills`);
494
+ - the structural leak scan found nothing (`structuralLeaks`), the token
495
+ appears nowhere it must not (`tokenExposed`), and no worker prompt or
496
+ controller action touched a private marker (`privateAccess`);
497
+ - at least two sessions started and all were closed (`sessionsNotClosed`);
498
+ - `parallel`: both first turns completed (`firstTurnsNotCompleted`), they
499
+ overlapped (`noOverlap`) and the follow-up completed
500
+ (`followUpNotCompleted`);
501
+ - `abort`: the `wait` turn ended `aborted` after only successful aborts
502
+ (`abortFailed`), in the required order and before its session closed
503
+ (`abortOrder`), and `range` completed with the abort inside its running
504
+ interval (`neighborNotRunningDuringAbort`);
505
+ - per fixture: only allowed changes (`<task>:changes`), every change written
506
+ during a worker turn (`<task>:attribution`), fixture tests and business
507
+ checks passing (`<task>:tests`), for `slug` the added test and the
508
+ handshake (`slug:followUp`), for `wait` no late file (`wait:lateFile`),
509
+ and an oracle for every task of the scenario (`oracleMissing`).
510
+ - Liveness observation: for each turn of a session, `turnLiveness` records the
511
+ number of `pi_turn_get` polls that saw the turn `running` and whether
512
+ `updatedAt` exceeded `startedAt` while it was running. This is an
513
+ observation, not a pass criterion.
406
514
  - Controller writes are limited by the client restrictions: the Codex
407
515
  controller runs in a read-only sandbox, and the Claude Code controller has no
408
516
  write or edit tool, although its allowed `git -C` could still change a
@@ -413,14 +521,43 @@ Each client runs two invocations, one per scenario:
413
521
  private directory or its files; this is a marker check, not a sandbox.
414
522
  - The harness records its own SHA-256 at `prepare` and refuses to run or
415
523
  re-evaluate a run with another version.
524
+ - `reanalyze` recomputes the transcript analysis and the checks of a finished
525
+ run and never overwrites the original result: it writes a numbered
526
+ reanalysis file. At the end of each run the harness records the state of
527
+ the fixtures (working tree, `HEAD`, index) and of the bound resources. If a
528
+ fixture or a bound resource differs from that state, the reanalysis fails
529
+ with `stateChangedSinceRun`.
416
530
  - `evidence` writes one sanitized JSON record and refuses to write a local path
417
- or the token. Transcripts and the configuration stay in the run's private
418
- directory and are never published.
419
-
420
- A controller that reports success does not count; only the checks above do.
531
+ or the token. It lists the original results and the reanalyses separately,
532
+ lists every launch with whether it left a result, and includes the extension
533
+ names and versions and the resource digests.
534
+ Transcripts and the configuration stay in the run's private directory and
535
+ are never published.
536
+
537
+ A controller that reports success does not count; only the checks above and
538
+ the `acceptance` verdict derived from them do.
421
539
  If a client or its authentication is not available, record that client's part
422
540
  as blocked.
423
541
 
542
+ ### Method limits of earlier records
543
+
544
+ Records made with the harness before it was hardened in #53 have these method
545
+ limits, which that change removed:
546
+
547
+ - The scope check used `git status`, which does not show a change that a worker
548
+ had already committed.
549
+ - The business checks ran the fixture tests inside the worker's workspace,
550
+ which a worker could have modified.
551
+ - Only the build, the harness and the bytes of the configuration file (through
552
+ its fingerprint) were bound to the run. The installed controller skill, the
553
+ contents of the selected worker skills and the fixture baselines were not.
554
+ - After the time limit the harness sent `SIGTERM` and then waited for the
555
+ client to close without a bound. A hang would have shown as a hang, not as a
556
+ pass.
557
+
558
+ These limits do not show that any recorded result was wrong. The records stay
559
+ as recorded; a new run gets a new record.
560
+
424
561
  ## Evidence rules
425
562
 
426
563
  - Record only sanitized evidence: statuses, booleans, counts, durations,
@@ -837,6 +974,10 @@ These runs do not cover macOS, Codex skill discovery from the user's home
837
974
  controller read it from there), the extension profile path live, or more than
838
975
  two concurrent sessions.
839
976
 
977
+ The multi-session runs above were made with the earlier harness. Its
978
+ [method limits](#method-limits-of-earlier-records) apply to them. They do not
979
+ show that any of these results was wrong.
980
+
840
981
  Before its release, `v0.8.0` passed Level 1 and the working-tree Level 2 on
841
982
  its release candidate `6d197a0` on Linux (`x64`), with Node.js `26.11.1`, Codex
842
983
  CLI `0.162.1` and Claude Code `2.1.296`, on 2026-10-12:
@@ -854,3 +995,212 @@ CLI `0.162.1` and Claude Code `2.1.296`, on 2026-10-12:
854
995
  - Apart from the version, the candidate's sources equal the merge of #48,
855
996
  whose final candidate `0ebbaa0` passed the multi-session skill acceptance
856
997
  recorded above.
998
+
999
+ After its release, `v0.8.0` passed Level 1 on a fresh clone of its tag and
1000
+ Level 2 in three variants. The release was published on 2026-10-12. The release
1001
+ notes do not date the individual checks, and they name no operating system for
1002
+ the local runs.
1003
+ - Source and tag: the release source and the annotated tag target is
1004
+ `de46896a6cc3aebc61234917c6d8c472003da36b`, the merge of #49 (head
1005
+ `805162ed3c3bcd73113dccabf62656414e3dd9be`). The annotated tag object is
1006
+ `a198292fab7f4ef9bdbd18a0ba40324e45e71e21`. The feature pull request #48
1007
+ (head `a2dc018a22ffa8dd8ff6af5d358f4f57cf7db845`) was merged as
1008
+ `f9e4b6711e9ca0419b2325bec8d6c350ebead5f5`.
1009
+ - CI: #48 and #49 passed on Linux, macOS 15 and macOS 26. The
1010
+ [merged-main run](https://github.com/plagor44-ux/pi-session-mcp/actions/runs/38226706157)
1011
+ and the
1012
+ [tag run](https://github.com/plagor44-ux/pi-session-mcp/actions/runs/38226900880)
1013
+ are on record.
1014
+ - Level 1 on a fresh clone of the tag, with Node.js `26.11.1` and npm
1015
+ `12.2.0`: every command exited with `0`.
1016
+ - Typecheck, server build and CLI build.
1017
+ - The test run reported 43 files, 884 passed and 5 skipped (macOS-only).
1018
+ - `npm audit` and `npm audit --omit=dev` reported 0 vulnerabilities.
1019
+ - Offline Doctor reported `packageVersion` `0.8.0`. With the example
1020
+ configuration its only finding was the placeholder workspace.
1021
+ - npm package `pi-session-mcp@0.8.0`, published from a fresh clone of the tag
1022
+ through staged publishing and approved by the maintainer: 125 files, 189.3 kB
1023
+ and the shasum `37f76ab9c3807213b7e6178156992c76f0ff8378`. The tarball
1024
+ downloaded from the registry had the same SHA-1 and SHA-512 integrity.
1025
+ - Level 2 ran through `scripts/setup-acceptance.mjs` in three variants: the
1026
+ working tree of the release candidate, a fresh clone of the tag, and the npm
1027
+ package installed into a temporary prefix. Each passed 160 of 160 checks.
1028
+ - The clients were Codex CLI `0.162.1` (`user`) and Claude Code `2.1.296`
1029
+ (`user`, `project`, `local`), in disposable homes.
1030
+ - Every step also checked the controller skill: installed after apply,
1031
+ verified, removed after rollback and remove, with a foreign skill in the
1032
+ same directory untouched.
1033
+ - The real client registrations were unchanged, no process remained, and the
1034
+ build stayed byte-identical.
1035
+ - Multi-session skill acceptance: it ran on the branch candidate `0ebbaa0`
1036
+ (recorded above), not on the tag. According to the release notes, its
1037
+ sources equal the release apart from the version and the documentation.
1038
+ Claude Code `2.1.296` and Codex CLI `0.162.1` were the controllers, with Pi
1039
+ SDK `1.1.0` workers. Both the `parallel` scenario (overlapping turns, a
1040
+ same-session follow-up and a skill-only token written by the worker) and the
1041
+ targeted `abort` scenario with a neighbor that kept running passed every
1042
+ check for both clients. The Claude Code controller skill was installed with
1043
+ `setup --apply`, and all catalog skills were `builtin:` references.
1044
+
1045
+ This record does not cover:
1046
+ - A full ten-task Level 3 cycle. The multi-session check is targeted.
1047
+ - A Codex controller that reads the skill from `~/.agents/skills` live (setup
1048
+ installs it there and Level 2 checks the file), the extension profile path
1049
+ live with skills (the SDK contract tests cover it), more than two concurrent
1050
+ sessions, and macOS beyond CI.
1051
+ - The contents of skill files: they are not snapshotted, so skill files must
1052
+ not change while sessions use them. The model sees skill paths in its system
1053
+ prompt and may repeat them in its answers.
1054
+ - Isolation: skill selection limits the regular skill catalog only. An
1055
+ allowlisted extension stays trusted code, and a `coding` profile is no
1056
+ operating-system sandbox.
1057
+ - Package provenance: the npm package has no provenance attestation, because it
1058
+ was published from the maintainer's machine.
1059
+
1060
+ The post-0.8.0 stabilization (#47 and #50 to #54) passed on 2026-10-12, on
1061
+ Linux (`x64`) with Node.js `26.11.1`, Claude Code `2.1.296`, Codex CLI
1062
+ `0.162.1`, Pi SDK `1.1.0` and `pi-cursor-sdk@0.5.3`:
1063
+ - Candidate: a clean detached checkout of `1e904be`, built with `npm ci`,
1064
+ `npm run build` and `npm run build:cli`. `dist/main.js` has the SHA-256
1065
+ `47875d38886528ace6b242df2c6ad79d624c51e330e63c2f401576eadc449ed7`, and the
1066
+ dist tree the SHA-256
1067
+ `7b16757f9a1a0318247914f56debe79119a3845e1a1debcf1ad371db3a958fce`. The
1068
+ built `dist/` and `.pi-session-mcp-cli/` are byte-identical for every
1069
+ candidate below (`0f7c376`, `06471b5`, `cb6e949` and `1e904be`): the commits
1070
+ after `0f7c376` change only the acceptance harness, its tests and
1071
+ documentation. The commits after `1e904be` change only tests and documentation.
1072
+ - Level 1 on that checkout: every command exited with `0`. The test run
1073
+ reported 50 files, 1035 passed and 6 skipped; `npm audit` and
1074
+ `npm audit --omit=dev` reported 0 vulnerabilities; `npm pack --dry-run`
1075
+ listed 129 files with the three skills and no acceptance harness;
1076
+ `git diff --check` and `git status --short` printed nothing.
1077
+ - Level 2 through `scripts/setup-acceptance.mjs` with the setup of that
1078
+ checkout: 160 of 160 checks passed. All nine steps passed on all four
1079
+ targets. The real client registrations were unchanged, no process remained,
1080
+ no report leaked a path, and the build was unchanged. `06471b5` and
1081
+ `cb6e949` had passed 160 of 160 as well.
1082
+ - Multi-session skill acceptance, run 4, with the harness of that checkout
1083
+ (SHA-256 `e694c9be2779aae164ced0b771db035dac64f136160cc98d2c9e9cf63b629afe`):
1084
+ - `prepare --with-extensions` derived the configuration from the operator's
1085
+ Cursor profile: provider `cursor`, model `composer-2.5`, thinking level
1086
+ `off` and the exact allowlist `npm:pi-cursor-sdk@0.5.3` on both run
1087
+ profiles. Nothing else was copied. Fingerprint
1088
+ `sha256:05a3fb00f9a1bb57b891d4cfe273aa19e1a716c0d17d2d24097e4225dbea4142`.
1089
+ - `prepare` installed the Claude Code controller skill with the candidate's
1090
+ `setup --apply`: `applied` and `skill_installed`, a regular copy identical
1091
+ to the shipped skill.
1092
+ - Every invocation confirmed the candidate and the fingerprint through
1093
+ `pi_capabilities_get` as its first call, saw the skills `implementation`
1094
+ and `handshake` on `accept-code`, and had exactly one server of the run
1095
+ observed. No process remained, no bound resource changed, and no Pi
1096
+ Session MCP result contained a local path, the token or a credential
1097
+ marker.
1098
+ - Both Claude Code invocations, the required part, and Codex `parallel`
1099
+ passed every check. Codex `abort` failed one check (`range:changes`); every
1100
+ other check of it passed.
1101
+
1102
+ | Client | Scenario | Result |
1103
+ | --- | --- | --- |
1104
+ | Claude Code | `parallel` | Passed. First turns of two Cursor sessions overlapped by 20.4 s; follow-up in the same session completed; both fixtures pass their tests and the business checks with only the allowed files changed, each last written during a worker turn; `HANDSHAKE.txt` holds the token and was written during the follow-up turn, although no prompt contained the token |
1105
+ | Claude Code | `abort` | Passed. First turns overlapped by 21.5 s; one abort, result `ok`; `aborted` seen through `pi_turn_get` after the abort and before close; the abort fell inside the running interval of the neighbor, which completed and passes its tests; the worker's `sleep` was observed running, and at the end of the run it was gone and had written no file; both sessions closed |
1106
+ | Codex | `parallel` | Passed, as for Claude Code, with an overlap of 21.1 s |
1107
+ | Codex | `abort` | Failed with `range:changes`. The abort, its order, the running neighbor, cleanup and the neighbor's tests and business checks passed (overlap 19.2 s). The `range` worker also wrote `HANDSHAKE.txt`: the controller's prompt said "Change only src/range.js" and did not mention a handshake, so the Cursor worker applied the `handshake` skill of its profile on its own. The scope check caught it, as intended. This is worker behavior, not a Pi Session MCP defect |
1108
+
1109
+ The controllers made 5 to 7 `pi_turn_get` calls per invocation, and each
1110
+ invocation took 90 to 127 seconds.
1111
+
1112
+ - Turn liveness (#47): in every Cursor turn that a controller polled while it
1113
+ was `running`, `updatedAt` had moved past `startedAt`: 7 of 7 turns in run
1114
+ 4, 31 of 31 in runs 1 to 4. Turns that no poll saw running are not counted.
1115
+ This shows public stream events reaching `updatedAt` before the turn ended.
1116
+ It does not show progress, and work without a public event stays
1117
+ invisible.
1118
+ - Controller skill use, checked in the private transcripts of runs 1, 3 and
1119
+ 4:
1120
+ - Each Claude Code invocation invoked its skill once and loaded the copy
1121
+ that setup had installed for the candidate. A phrase that only the
1122
+ candidate's skill contains appeared, and none from the operator's personal
1123
+ copy in `~/.claude/skills`, which is the 0.8.0 skill. So the personal copy
1124
+ did not shadow the project copy.
1125
+ - Each Codex invocation read the candidate's skill through the run-scoped
1126
+ link with one shell command.
1127
+ - The handshake token, which only the override-directory skill held, shows
1128
+ that the worker skills reached the Cursor sessions.
1129
+
1130
+ Earlier attempts on the same build, kept on record:
1131
+ - Before a reboot, on `0f7c376`:
1132
+ - Level 1 passed, with 1028 tests passed and 6 skipped, and the working-tree
1133
+ Level 2 passed 160 of 160.
1134
+ - Claude Code `parallel` passed every check, with an overlap of 52.2 s.
1135
+ - Claude Code `abort` failed with `sessionsNotClosed`, `abortFailed`,
1136
+ `abortOrder`, `neighborNotRunningDuringAbort` and `range:tests`: neither
1137
+ `pi_session_start` started a session. At that time the operator's Pi agent
1138
+ directory was not writable, because `/home` had run out of Btrfs metadata
1139
+ space. The run directory was on a memory-backed file system and was lost in
1140
+ the reboot, so only the maintainer's summaries remain: there is no evidence
1141
+ file, and the exact start error is not on record.
1142
+ - Run 1 on `0f7c376`, with the harness SHA-256
1143
+ `9135f8d9a3d904172475e2fd0d5fce762613a8683a642b53642167572e02ab91`: all four
1144
+ invocations passed every check. The overlaps were 29.0 s and 22.2 s for
1145
+ Claude Code (`parallel`, `abort`) and 22.8 s and 26.5 s for Codex.
1146
+ - Run 2 on `06471b5`, with the harness SHA-256
1147
+ `d63b03065b28f3714ee2e9f76d722053c835f080b3a6edc96e1aab0031a0eadc`:
1148
+ - Codex `parallel` passed.
1149
+ - The other three invocations failed only with `noServerObserved`, and every
1150
+ other check passed, including the aborts, the running neighbors and the
1151
+ absence of residual processes.
1152
+ - The cause was in the harness. It replaced a process's earlier sighting on
1153
+ every sweep, so a sweep that caught the server while its client was ending
1154
+ it (exiting or an unreaped zombie, with nothing left to read) recorded it as
1155
+ a plain descendant of the client. `cb6e949` keeps the server role.
1156
+ - Run 3 on `cb6e949`, with the harness SHA-256
1157
+ `9f62535c3cb28506595ba72e5bafad772df71f473b4711799724186a946713b9`: Level 2
1158
+ passed 160 of 160, and all four invocations passed every check. The overlaps
1159
+ were 22.8 s and 22.2 s for Claude Code and 24.2 s and 18.6 s for Codex. One
1160
+ `assistantText` in Codex `parallel` named the absolute path of the worker's
1161
+ own `HANDSHAKE.txt`; `assistantText` is the worker's free text, counted
1162
+ separately, and not a structural leak. The two harness commits after it keep
1163
+ a process's ownership across sightings and match the client by PID and
1164
+ start time.
1165
+
1166
+ This record does not cover:
1167
+ - macOS beyond CI, or a ten-task Level 3 cycle.
1168
+ - A Codex controller that reads the skill from a copy setup installed in
1169
+ `~/.agents/skills`, because the run must not change the operator's home.
1170
+ Codex read the candidate's skill through a run-scoped link, and Level 2
1171
+ checks the installed file.
1172
+ - A third concurrent session, a `read-only` worker, or more than two
1173
+ concurrent sessions.
1174
+ - A real crash in the middle of a setup skill update, a real contention of
1175
+ two customization processes outside the tests, and an operating-system
1176
+ signal to the harness during a live analysis. The provider-free tests cover
1177
+ these.
1178
+
1179
+ After its release, `v0.8.0` was installed for the maintainer on Linux: the npm
1180
+ package `pi-session-mcp@0.8.0` in the user's npm prefix, registered for Claude
1181
+ Code. Checked on 2026-10-12:
1182
+ - Offline Doctor reported `packageVersion` `0.8.0` and no finding.
1183
+ - `--verify` for `claude-code:user:pi-session-mcp` reported `mcp_verified`, and
1184
+ `skill_verified` for the controller skill.
1185
+ - Codex has no registration of the server on that machine, so `codex:user` was
1186
+ not part of this installation.
1187
+
1188
+ Before its release, `v0.8.1` passed Level 1 and the working-tree Level 2 on its
1189
+ release candidate `baa6406` on Linux (`x64`), with Node.js `26.11.1`, Codex CLI
1190
+ `0.162.1` and Claude Code `2.1.296`, on 2026-10-12:
1191
+ - Level 1 on a clean detached checkout: every command exited with `0`.
1192
+ - The test run reported 50 files, 1035 passed and 6 skipped.
1193
+ - `npm audit` and `npm audit --omit=dev` reported 0 vulnerabilities.
1194
+ - `npm pack --dry-run` listed 129 files, including the three skills, and no
1195
+ acceptance harness.
1196
+ - Doctor reported `packageVersion` `0.8.1`. With the example configuration
1197
+ its only finding was the placeholder workspace.
1198
+ - Level 2 through `scripts/setup-acceptance.mjs` on the candidate's working
1199
+ tree: 160 of 160 checks passed. All nine steps passed on all four targets,
1200
+ including the controller skill finding of every step. The real client
1201
+ registrations were unchanged, no process remained, and the built files were
1202
+ unchanged.
1203
+ - Apart from the version and documentation, the candidate's sources equal
1204
+ `0c7bad3`. Its built modules have the same dist tree SHA-256
1205
+ (`7b16757f9a1a0318247914f56debe79119a3845e1a1debcf1ad371db3a958fce`) as the
1206
+ final candidate `1e904be` of the multi-session acceptance recorded above.
@@ -215,3 +215,14 @@ the controller skill that `setup --apply` installs, so the next release after
215
215
  0.7.1 is 0.8.0. The change is recorded under
216
216
  `## Unreleased` in the changelog. Implementing it does not itself release or
217
217
  bump the version.
218
+
219
+ ## Note of 2026-10-12: fingerprints, locking and recovery after 0.8.0
220
+
221
+ The decisions above stand. Three details changed after 0.8.0 (issues #50, #51
222
+ and #52): skill tree fingerprints are version 2 (`v2:` values; a 0.8.0 value
223
+ counts only when a trusted reference proves it), `--apply --customize-skill`
224
+ runs under a kernel lock on the override directory and writes the bases file as
225
+ schema version 2, and an interrupted update of an installed copy is settled by
226
+ the next `--apply`, `--remove` or `--rollback`. See [Pi
227
+ skills](../skills.md#upgrade-from-080) and the [setup lifecycle
228
+ design](../client-setup-design.md#skill-installation).
@@ -71,7 +71,7 @@ The registry owns public UUIDs, lifecycle state, the turn read model, background
71
71
 
72
72
  ## Lifecycle and public data
73
73
 
74
- `pi_session_prompt` creates a provisional turn identity, invokes Pi preflight, and only after accepted preflight publishes `{session, turn}`; rejected preflight leaves no orphan turn. Completion returns to `idle`; failure is generic. Each active session retains at most 20 turns. A turn is `running`, `completed`, `failed`, or `aborted`; its public view contains IDs, timestamps, and terminal `completedAt`. Completed text is normalized and capped at 64 KiB UTF-8 with Unicode-safe truncation. It can contain model-visible workspace data and remains potentially sensitive.
74
+ `pi_session_prompt` creates a provisional turn identity, invokes Pi preflight, and only after accepted preflight publishes `{session, turn}`; rejected preflight leaves no orphan turn. Completion returns to `idle`; failure is generic. Each active session retains at most 20 turns. A turn is `running`, `completed`, `failed`, or `aborted`; its public view contains IDs, timestamps, and terminal `completedAt`. While a turn runs, its `updatedAt` is the time of the last observed public SDK activity (assistant message and tool execution events, content-free) and is a liveness lower bound only; see [Tool contracts](tool-contracts.md). Completed text is normalized and capped at 64 KiB UTF-8 with Unicode-safe truncation. It can contain model-visible workspace data and remains potentially sensitive.
75
75
 
76
76
  Session metadata contains only whitelisted values: public session ID, workspace alias, execution-profile alias, derived permission `profile`, post-verified provider/model/effective thinking level, lifecycle state, and timestamps. Capability metadata uses a separate whitelist: server name/version, loaded configuration fingerprint/reload policy, workspace aliases, and configured profile fields, including the selected skill aliases. Neither view contains workspace paths, full model objects, SDK session IDs, auth sources, credential paths, headers, base URLs, prompts, transcripts, or raw provider errors.
77
77
 
@@ -79,7 +79,12 @@ findings make the overall result `failed`, like any other finding.
79
79
  bounded reader: regular files only, at most 32 files, 1 MiB and 8 levels, no
80
80
  links or special files inside the tree, entries whose name starts with `.`
81
81
  skipped. A missing or unusable source is `skill_source_invalid`. The fingerprint
82
- is a SHA-256 over the sorted relative paths and contents.
82
+ is version 2 (`src/skill-library.ts`): `v2:` and a SHA-256 over a domain and
83
+ version tag, the file count, and per file in path byte order the length-prefixed
84
+ UTF-8 path and the length-prefixed content. Version 1 (0.8.0) hashed
85
+ `path NUL content NUL` per file and was ambiguous, because content may contain
86
+ NUL bytes. A version 1 value never proves identity alone (see *Legacy records*
87
+ below).
83
88
 
84
89
  **Slots.** A slot is the skill directory a client reads:
85
90
 
@@ -97,20 +102,63 @@ alias `skill:pi-session-orchestration`. A project slot uses
97
102
  of a SHA-256 over the project's real path: an opaque identity, not a path, so a
98
103
  `--remove` in another project never touches this project's copy. The `pending`
99
104
  phase is written before the first filesystem change and promoted to `owned`
100
- after a post-install inspection. During an update, the pending record also
101
- keeps the fingerprint of the copy it replaces (`replaces`), so an interrupted
102
- update leaves a copy that setup still recognizes as its own: the next
103
- `--apply` finishes the update and `--remove` deletes it. A later run that finds
104
- a `pending` record whose slot already equals the source promotes it.
105
+ after a post-install inspection. Skill records hold `v2:` values in
106
+ `fingerprint` and `replaces`; registration records keep their 64-hex snapshot
107
+ hash. During an update, the pending record also keeps the fingerprint of the
108
+ copy it replaces (`replaces`), so an interrupted update leaves a copy that
109
+ setup still recognizes as its own: the next `--apply` finishes the update and
110
+ `--remove` deletes it. A later run that finds a `pending` record whose slot
111
+ already equals the source promotes it.
105
112
 
106
113
  **Atomic copy.** Setup creates missing parent directories (mode `0755`), stages
107
- the tree in a temporary directory in the slot's parent (files `0644`, each file
114
+ the tree in a staging directory in the slot's parent (files `0644`, each file
108
115
  and directory fsynced), and re-checks that the slot is unchanged
109
116
  (fingerprint, device and inode). It then renames the exact old tree to a backup
110
- name, renames the staged directory into place, inspects the result, and deletes
111
- the backup only if it is still the exact old tree it moved. An unhealthy
112
- ownership fence retains staged and backup content for conservative recovery.
113
- The slot is never edited in place.
117
+ name, renames the staged directory into place, inspects the result, verifies
118
+ that the backup is still the exact old tree it moved, renames it to a trash
119
+ name and deletes the trash entry by entry. The slot is never edited in place.
120
+
121
+ **Intermediates and recovery.** The three intermediate directories are named
122
+ after the pending record's transaction id, in the slot's parent:
123
+ `.pi-session-mcp-<skill>-<transactionId>.staging`, `.backup` and `.trash`.
124
+ Recovery derives them from the persisted record and never searches. The order
125
+ is: pending record, staging, backup rename, install, verify backup, rename to
126
+ trash, delete the trash entry by entry, owned record. `--apply`, `--remove` and
127
+ `--rollback` settle a pending transaction first, inside the ownership
128
+ transaction, and only when the slot is empty, a link, or one of setup's two
129
+ copies:
130
+
131
+ - A trash directory is deleted after a structural recheck: a real directory,
132
+ regular files and directories only, no hidden entries, within the tree
133
+ limits. It may be partial, because it was verified before the rename.
134
+ - The backup must equal the replaced copy; the staging directory must equal
135
+ the target or be an interrupted write of it. A partially written staging
136
+ directory counts only when every file is a prefix-consistent part of the
137
+ current library tree and that tree is the record's target.
138
+ - `--apply` with an empty slot finishes forward with a complete staging
139
+ directory, else moves the verified backup back. `--remove` and `--rollback`
140
+ delete the verified intermediates and leave the slot to the removal.
141
+ - Anything modified or foreign stays and is reported as
142
+ `skill_recovery_retained` (divergent). A failed update leaves its staging
143
+ directory until the next run settles it, and an unhealthy ownership fence
144
+ retains intermediates the same way.
145
+ - Random-name directories of 0.8.0 updates are never searched for or deleted;
146
+ an operator may remove them by hand.
147
+ - `--dry-run` and `--verify` do not report leftover intermediates.
148
+
149
+ **Legacy records.** A skill record whose `fingerprint` or `replaces` is a bare
150
+ 64-hex value was written by 0.8.0. It names the slot only if a trusted
151
+ reference has that version 1 value and is byte-identical, by its version 2
152
+ fingerprint, to the installed copy. The references are the shipped skills of
153
+ 0.8.0 (a fixed table in `src/skill-library.ts`) and the current library tree.
154
+ When proven, `--apply` migrates the record to version 2 and then updates the
155
+ copy as usual; `--remove` removes the proven copy. Otherwise `--dry-run`,
156
+ `--apply`, `--remove` and `--rollback` report `skill_legacy_unverified`
157
+ (divergent) and change neither the copy nor the record. The operator inspects
158
+ the copy and, if it is unchanged setup content that is no longer needed,
159
+ deletes it; an absent slot clears the record on the next operation. The
160
+ intermediates of an interrupted 0.8.0 update cannot be derived and are not
161
+ recovered.
114
162
 
115
163
  **Divergence rules.** Setup never replaces or deletes, and reports
116
164
  `skill_divergent` for:
@@ -118,6 +166,8 @@ The slot is never edited in place.
118
166
  - an installed copy whose fingerprint differs from setup's own copy (the
119
167
  owned fingerprint, or the one a pending update replaces), even if it happens
120
168
  to equal the new source;
169
+ - an installed copy that a 0.8.0 record names but no trusted reference proves
170
+ (`skill_legacy_unverified`);
121
171
  - a copy that differs from the source without a record (a foreign skill of the
122
172
  same name);
123
173
  - any entry whose name starts with `.` inside the installed copy: setup never
@@ -171,11 +221,41 @@ status and a stable code. No skill content, fingerprint or path leaves setup.
171
221
  `src/skill-customize.ts`) does not touch clients and takes no ownership lock. It
172
222
  copies a shipped skill into the override directory `skills/` next to the
173
223
  configuration file and records the shipped fingerprint under the skill name in
174
- `<override dir>/.pi-session-mcp-bases.json` (`{"schemaVersion":1,"bases":{...}}`,
175
- strictly validated, at most 64 KiB, replaced atomically). The copy is staged in
224
+ `<override dir>/.pi-session-mcp-bases.json`
225
+ (`{"schemaVersion":2,"bases":{"<name>":"<fingerprint>"}}`, strictly validated,
226
+ at most 64 KiB). A value is a `v2:` fingerprint or a legacy 64-hex value that
227
+ could not be migrated. A version 1 file is still read. The copy is staged in
176
228
  a temporary directory and renamed into place; an existing override is never
177
229
  overwritten. A changed shipped skill is only reported (`skill_override_outdated`);
178
- `--apply` then moves the recorded base and nothing else. It cannot be combined
230
+ `--apply` then moves the recorded base and nothing else.
231
+
232
+ `--apply` is a cross-process transaction per override directory
233
+ (`src/override-lock.ts`). It takes an exclusive kernel lock on the stable lock
234
+ file `<override dir>/.pi-session-mcp-bases.lock` (mode `0600`, never deleted;
235
+ on Linux the trusted util-linux `flock` at `/usr/bin/flock` or `/bin/flock`, on
236
+ macOS `O_EXLOCK`; the same supported local filesystems as the ownership
237
+ state). Under the lock it re-reads and validates the bases file, copies the
238
+ skill if no override exists, merges the base and writes the file atomically: an
239
+ fsynced temporary file, a rename and a directory fsync. The lock is released on
240
+ every path. The wait is bounded at 30 seconds (`skill_customize_lock_busy`);
241
+ a lock that cannot be taken is `skill_customize_lock_unavailable`. A killed
242
+ holder releases the lock through the kernel and may leave a stale
243
+ `.pi-session-mcp-bases.json.<hex>.tmp` or a temporary copy directory
244
+ `.<skill>.<hex>.tmp`, which are harmless. The dry-run takes no
245
+ lock and writes nothing: no directory, lock file or temporary file. Every write
246
+ produces version 2 and migrates each legacy value that names a released 0.8.0
247
+ shipped skill; other legacy values are kept as they are.
248
+
249
+ Decisions for an existing legacy base: if it names the current shipped skill,
250
+ the dry-run reports `skill_base_migration_planned` and `--apply` reports
251
+ `skill_base_migrated`; if it names no released shipped skill, the dry-run
252
+ reports `skill_base_unverified` and `--apply` acknowledges it as
253
+ `skill_base_updated`; if it names another released skill, the result is
254
+ `skill_override_outdated`. An existing override that equals the shipped skill
255
+ but has no base completes a customization whose bases commit failed
256
+ (`skill_customize_planned`, `skill_customized`). Of two concurrent applies for
257
+ the same skill, one reports `skill_customized` and the other
258
+ `skill_override_current`. Doctor stays offline, read-only and lock-free. It cannot be combined
179
259
  with `--target`, `--verify`, `--rollback` or `--remove`. The finding target is
180
260
  `{ client: "skills", scope: "override", alias: <name> }`; the alias is the
181
261
  skill name only if it is valid, else `invalid`.
@@ -205,17 +285,23 @@ Stable finding codes are grouped by responsibility:
205
285
  - rollback/removal: `rolled_back`, `removed`, `ownership_diverged`,
206
286
  `cleanup_failed`;
207
287
  - controller skill, dry-run: `skill_absent`, `skill_equivalent`, `skill_linked`,
208
- `skill_update_planned`, `skill_divergent`, `skill_source_invalid`;
209
- - controller skill, apply: `skill_installed`, `skill_already_equivalent`, `skill_linked`,
210
- `skill_updated`, `skill_divergent`, `skill_source_invalid`,
288
+ `skill_update_planned`, `skill_recovery_planned`, `skill_divergent`,
289
+ `skill_legacy_unverified`, `skill_source_invalid`;
290
+ - controller skill, apply: `skill_installed`, `skill_already_equivalent`, `skill_recovered`, `skill_linked`,
291
+ `skill_updated`, `skill_divergent`, `skill_legacy_unverified`,
292
+ `skill_recovery_retained`, `skill_source_invalid`,
211
293
  `skill_install_failed`;
212
294
  - controller skill, verify: `skill_verified`, `skill_missing`,
213
295
  `skill_not_equivalent`;
214
296
  - controller skill, rollback/removal: `skill_removed`, `skill_kept_shared`,
215
- `skill_not_owned`, `skill_absent`, `skill_divergent`;
297
+ `skill_not_owned`, `skill_absent`, `skill_divergent`,
298
+ `skill_legacy_unverified`, `skill_recovery_retained`;
216
299
  - skill customization: `skill_customize_planned`, `skill_customized`,
217
- `skill_override_current`, `skill_override_outdated`, `skill_base_updated`,
218
- `skill_not_shipped`, `skill_bases_corrupt`, `skill_override_invalid`,
300
+ `skill_override_current`, `skill_override_outdated`,
301
+ `skill_base_migration_planned`, `skill_base_migrated`,
302
+ `skill_base_unverified`, `skill_base_updated`, `skill_not_shipped`,
303
+ `skill_bases_corrupt`, `skill_override_invalid`,
304
+ `skill_customize_lock_busy`, `skill_customize_lock_unavailable`,
219
305
  `skill_customize_failed`.
220
306
 
221
307
  The public statuses are `planned`, `ok`, `unchanged`, `absent`, `divergent`,