@junghanacs/entwurf 0.36.1 → 0.37.0

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,102 @@ All notable changes to this project will be documented here. Format follows [Kee
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.37.0 - 2026-10-15
8
+
9
+ ### pi-durable
10
+
11
+ - **The durable app forwards pi's transport and stops compacting beside a running turn (#143).**
12
+ The owned Harness settings now carry `SettingsManager.getTransport()` (pi's default `auto`)
13
+ into every request; the reference app had left the field out, so the SDK chose per connection.
14
+ `auto` permits a cached WebSocket continuation only when the provider's own body and prefix
15
+ conditions hold; it does not make every request a delta. Background compaction is off
16
+ (`backgroundTokens: 0`): a turn past the blocking threshold waits for the SDK's summary instead
17
+ of one being written beside it. Threshold, manual and overflow compaction stay the SDK's, on
18
+ pi's own compaction settings. The SDK engine, models, auth and storage are unchanged, and this
19
+ claims neither lower vendor overload nor better memory.
20
+ - **`entwurf pi-durable --resume` reopens a chosen session (#143).** It lists the current
21
+ directory's durable sessions from their files and the record store only (creation time, garden
22
+ id, last write, open elsewhere), most recently written first, and opens the chosen one as it
23
+ was saved, under its own garden id. Cancelling opens nothing. No session, a non-terminal, a
24
+ session open elsewhere, or a new session's argument beside it refuse by name, with no fallback.
25
+ `--continue` still opens the newest-created session.
26
+ - **The durable app lists a project's skills again.** The owned prompt handed pi's `skills`
27
+ setting to the skill loader as written, so a relative entry was read against the session
28
+ directory: a project's `.pi/settings.json` entry `../.claude/skills` named a `.claude/skills`
29
+ beside the project, in its parent, and the project's own skills were silently not listed.
30
+ Relative path entries are now read as pi reads them, the project's against its `.pi` and the
31
+ global ones against the agent directory. Both scopes are kept, the project's first, so a project
32
+ skill wins over a global one of the same name. Absolute and `~` entries are unchanged.
33
+ Patterns, package skills, `.agents/skills` and skill diagnostics are not supported by this
34
+ change. No project trust check is added: the durable app reads a project's settings without a
35
+ trust decision, as it did before.
36
+
37
+ ### Herdr plugin
38
+
39
+ - **The measured Herdr moves to 0.9.5, the hotfix of 0.9.4.** `scripts/fixtures/herdr-supply.json`
40
+ pins v0.9.5 (`2ecf19a9`), protocol 22, with both Linux asset digests taken from the immutable
41
+ release and named by its attestation. On aarch64 the pin rests on a private CLI baseline:
42
+ version, protocol 22, 18 `integration status` rows unchanged from 0.9.4, and a residue-free
43
+ `server stop`. The full `check-herdr-sandbox` then passed at this pin on x86_64 in the CI run of
44
+ the SHA that introduced it (run 38667564413 at `d8cd8f3`, 12 assertions on private Herdr 0.9.5),
45
+ and the x86_64 asset is recorded as executed with that receipt; it has not run on aarch64. The
46
+ supply receipt mutant now removes the x86_64 receipt instead of flipping an unexecuted flag,
47
+ because no asset at this pin is unexecuted. Plugin admission (Herdr >= 0.9.0), the plugin
48
+ version and the runtime lock are unchanged.
49
+ - **A host that uses the Claude integration needs `herdr integration install claude` again
50
+ after upgrading to Herdr 0.9.5.** 0.9.5 raises its claude integration from v10 to v11 and leaves pi at v9. Over integrations seeded by
51
+ 0.9.4 it reads claude as `outdated (v10 < v11)`, which the plugin's activation profile files as
52
+ `herdr-integration-outdated`, so the build and pane block [1] refuse by name until the
53
+ reinstall. In the measured 0.9.4-seeded private sandbox, the reinstall changed only the hook's
54
+ version line; the pi extension and the Claude settings stayed byte-identical. The plugin README
55
+ and the launch-rail document say so.
56
+
57
+ ### Release boundary and verification
58
+
59
+ - **Scope.** A core minor over `v0.36.1`: #143 at `3a65c8d`, the Herdr 0.9.5 pin at `d8cd8f3`
60
+ and its CI record at `dbb8a32`, and the skill fix at `4968ffd`. Not in it: #142 (the ACP
61
+ thinking option), and the default thinking level of a new durable session named by a model
62
+ without `:<level>`, which starts at `off` where pi starts at its configured default; that is
63
+ recorded on #142 and not changed here. The Herdr manifest stays 0.36.0; plugin 0.37.0 is a
64
+ separate cut after the core is published.
65
+ - **Local evidence before this preparation is focused, not a whole pass.** Each affected
66
+ qualification group (PARTIAL) measured its own change with a green control, and none is
67
+ carried to another. #143 killed 81/81 in the `check-pi-durable-beside` group. The supply
68
+ receipt mutant re-aimed at `dbb8a32` killed with its whole `check-herdr-supply` group, 11/11;
69
+ that commit skipped CI, so the re-aimed mutant has no CI run yet. The skill fix killed 82/82 in
70
+ the `check-pi-durable-beside` group (receipt `d934e43c4dad5dc28b49a3a85c5a133d126df9558f2bc260087c1b34cf7422f9`),
71
+ with 900 of the 982 committed claims not run. The qualification body has not run on any of them.
72
+ - **Unpublished candidate carrier.** As in 0.36.1, `plugins/herdr/runtime-lock.json` names the
73
+ closed `herdr-checkout` source, because 0.37.0 has no published integrity yet. The npm 0.36.1
74
+ bytes and every existing tag are unchanged. Re-pinning registry bytes is the separate Herdr
75
+ 0.37.0 cut, after the operator publishes core 0.37.0.
76
+ - **No local static floor; it is the exact CI's.** `check:hermetic` carries
77
+ `check-herdr-plugin-build` and `check-herdr-sandbox`, which must not run on the operator host,
78
+ and `release-gate`'s first MUST step is `pnpm run check:full`. By the operator's decision for
79
+ this cut only, `check:full` was not run locally and is owed whole to the prepared SHA's CI.
80
+ The decision is not inherited by a later cut.
81
+ - **LIVE, taken step by step, not as `release-gate --cut`.** The gate's 23 non-static MUST steps
82
+ and its BEHAVIOR step ran in the gate's own order, command, environment and working directory,
83
+ stopping at the first red, on the frozen candidate (`4968ffd` with this preparation's edits),
84
+ with the candidate fingerprint the same before and after each run. The first run passed steps
85
+ 1-18 and failed `smoke-mux-lifecycle-live`: its pi-native sibling (`openai-codex/gpt-6-luna`)
86
+ was alive and answered with a shell call and "ACK" but never called the callback tool within
87
+ 300 s. No input of that step changed in this cut, and the cause was not measured. By the
88
+ operator's choice it was run again once, unchanged, with steps 1-18 reused: steps 19-23 and the
89
+ BEHAVIOR step then passed. **MUST 23/23 PASS (18 + 5 on the second run), SKIP 0; BEHAVIOR 1/1
90
+ PASS.** `smoke-mux-lifecycle-live` passed 81 checks, `smoke-codex-fresh-live` 66 assertions
91
+ against an app-server started for each run in its own tmux session (named pane pid, a child
92
+ holding the kernel LISTEN inode, `gpt-5.6-luna` and `openai-codex/gpt-5.6-luna`, stopped by one
93
+ TERM after the run), and `smoke-pi-durable-fresh-live` 36 assertions. The gate sessions held no
94
+ ambient identity carrier. This is local LIVE evidence, not a release-gate verdict and not the
95
+ exact CI.
96
+ - **Exact CI owes the whole floor.** The prepared SHA's exact CI (the four required jobs, the
97
+ whole qualification body over the 982 committed claims, the re-aimed supply mutant included,
98
+ and `check-herdr-sandbox` on pinned Herdr 0.9.5) is the next checkpoint and is not presumed
99
+ PASS.
100
+ - **Publication boundary.** No make artifact, core tag, GitHub Release or npm publication is
101
+ claimed; they wait for the exact CI, the make decision and the operator.
102
+
7
103
  ## 0.36.1 - 2026-10-14
8
104
 
9
105
  ### ACP
package/README.md CHANGED
@@ -91,8 +91,13 @@ incomplete, drifted or mixed supply refuses by name.
91
91
  ```bash
92
92
  entwurf pi-durable --provider <provider> --model <model-id> --width task-wide
93
93
  entwurf pi-durable --continue
94
+ entwurf pi-durable --resume
94
95
  ```
95
96
 
97
+ `--continue` opens this directory's newest durable session. `--resume` lists its
98
+ sessions, most recently written first, by creation time, garden id and last write, and
99
+ reopens the one you choose as it was saved; a cancelled choice opens nothing.
100
+
96
101
  The contact adds garden identity and two-way mailbox communication. Entwurf owns the
97
102
  thin app/TUI composition (shipped as an emitted JS carrier); execution, models/auth, queues,
98
103
  SQLite, transcripts and recovery remain the upstream SDK's:
package/VERIFY.md CHANGED
@@ -227,7 +227,14 @@ holds that node and the badge counts it, `pending` while recovered), and the car
227
227
  `durableLayout` drawn by the published pi-tui renderer on a fake terminal across rows 1–8 × cols
228
228
  1–12, count changes and resizes, against the pinned upstream screen for the zero-task case. Its
229
229
  editor is a fixed stand-in: the real `CustomEditor`, overlays, emoji width and a consumer's own task
230
- are the visible-terminal and consumer subjects. `check-pi-runtime-version` asks the
230
+ are the visible-terminal and consumer subjects. The 1.2 bootstrap child also observes `--resume`
231
+ reopening the chosen session and cancelling to nothing (#143). Two more sandboxed children reach the
232
+ reference app's settings and prompt mapping: one drives the real bootstrap against a loopback
233
+ WebSocket Responses endpoint, whose received frames decide the forwarded transport and its
234
+ continuation and the reference compaction policy (no background summary; blocking and overflow
235
+ kept, #143); the other renders the carrier prompt's skills section with no model, against a fixture
236
+ of both settings scopes, an absolute entry, a same-name pair and a wrong-base decoy.
237
+ `check-pi-runtime-version` asks the
231
238
  same verifier whether this checkout resolves exactly the pinned SDK set (the native set; the PATH
232
239
  host is setup's subject), and `check-pi-import-surface` asserts the carrier fence. The separate
233
240
  `check-pi-durable-contact`, `check-pi-durable-send` and `check-pi-durable-receive` are
@@ -358,6 +358,7 @@ tree (for example an override that binds pi-durable's `pi-ai` to an older publis
358
358
  ```bash
359
359
  entwurf pi-durable --provider <provider> --model <exact catalog id> --width task-wide
360
360
  entwurf pi-durable --continue # this directory's newest durable session
361
+ entwurf pi-durable --resume # choose one of this directory's durable sessions
361
362
  ```
362
363
 
363
364
  The bootstrap imports only the compiled closure shipped beside it
@@ -107,9 +107,24 @@ import * as fs from "node:fs";
107
107
  import * as path from "node:path";
108
108
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
109
109
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
110
- import { defaultMetaMailboxDir, defaultMetaReceiversDir, defaultMetaSendersDir, defaultMetaSessionsDir, readAddressableMetaIdentity, readMetaReceiverMarker, readMetaSenderMarker, requireGardenId, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
110
+ import { defaultMetaMailboxDir, defaultMetaReceiversDir, defaultMetaSendersDir, defaultMetaSessionsDir, listAllMetaIdentitiesDir, readAddressableMetaIdentity, readMetaReceiverMarker, readMetaSenderMarker, requireGardenId, upsertMetaSession, writeMetaReceiverMarker, writeMetaSenderMarker, } from "./lib/meta-session.js";
111
111
  /** The backend id this contact mints under. */
112
112
  export const PI_DURABLE_BACKEND = "pi-durable";
113
+ /**
114
+ * The garden ids the record store holds for these durable sessions (#143 `--resume`), read only: a session no record
115
+ * names is absent, and an unreadable record is counted, never guessed.
116
+ */
117
+ export function piDurableGardenIds(nativeSessionIds, sessionsDir = defaultMetaSessionsDir()) {
118
+ const wanted = new Set(nativeSessionIds);
119
+ const { identities, errors } = listAllMetaIdentitiesDir(sessionsDir);
120
+ const gardens = new Map();
121
+ for (const identity of identities) {
122
+ if (identity.backend === PI_DURABLE_BACKEND && wanted.has(identity.nativeSessionId)) {
123
+ gardens.set(identity.nativeSessionId, identity.gardenId);
124
+ }
125
+ }
126
+ return { gardens, unreadable: errors.length };
127
+ }
113
128
  /** The durable Extension's name, owned exactly. */
114
129
  export const PI_DURABLE_EXTENSION_NAME = "entwurf";
115
130
  /** Every bridge tool this contact exposes. A tool outside this list is not callable here. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.36.1",
3
+ "version": "0.37.0",
4
4
  "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, reopen pi in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -123,7 +123,8 @@
123
123
  "husky": "^9.1.7",
124
124
  "rregex": "1.13.1",
125
125
  "typescript": "^6.0.2",
126
- "vitest": "4.1.9"
126
+ "vitest": "4.1.9",
127
+ "ws": "8.21.3"
127
128
  },
128
129
  "scripts": {
129
130
  "prepare": "npm run --silent build-bridge 1>&2",
@@ -80,19 +80,21 @@ import {
80
80
  failAfterCleanup,
81
81
  PI_DURABLE_EXTENSION_NAME,
82
82
  PI_DURABLE_TOOL_NAMES,
83
+ piDurableGardenIds,
83
84
  withGardenIdentity,
84
85
  } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
85
86
 
86
87
  const RUNTIME = "./carrier/runtime.js";
87
88
  const TUI = "./carrier/tui.js";
89
+ const SESSIONS = "./carrier/sessions.js";
88
90
 
89
91
  /** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
90
92
  export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
91
93
  new URL("../../mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js", import.meta.url),
92
94
  );
93
95
 
94
- /** Flags that take a value. Anything else but `--continue`/`-c` is an unknown argument — a bridge
95
- * entry included: the launch argv cannot point the contact at another bridge. */
96
+ /** Flags that take a value. Anything else but `--continue`/`-c` and `--resume` is an unknown argument — a
97
+ * bridge entry included: the launch argv cannot point the contact at another bridge. */
96
98
  const VALUE_FLAGS = [
97
99
  "--provider",
98
100
  "--model",
@@ -293,12 +295,13 @@ async function loadNativeModules(files) {
293
295
  * @param {{
294
296
  * bridgeEntry?: string,
295
297
  * continueSession?: boolean,
298
+ * session?: string,
296
299
  * cwd?: string,
297
300
  * model?: { provider: string, model: string },
298
301
  * firstInput?: string,
299
302
  * extensions?: readonly object[],
300
303
  * contact?: Record<string, unknown>,
301
- * open?: (options: { cwd: string, continueSession: boolean, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
304
+ * open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
302
305
  * }} options
303
306
  * @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
304
307
  * (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
@@ -307,6 +310,7 @@ async function loadNativeModules(files) {
307
310
  export async function openDurableCitizen({
308
311
  bridgeEntry = PACKAGED_BRIDGE_ENTRY,
309
312
  continueSession = false,
313
+ session,
310
314
  cwd = process.cwd(),
311
315
  model,
312
316
  firstInput,
@@ -320,6 +324,12 @@ export async function openDurableCitizen({
320
324
  "an explicit model and a first input apply only to a new session; a continued session is reopened as it was saved",
321
325
  );
322
326
  }
327
+ if (session !== undefined && (continueSession || model !== undefined || firstInput !== undefined)) {
328
+ throw refuse(
329
+ "fresh-only-argument-with-resume",
330
+ "a chosen session is reopened as it was saved: no --continue, explicit model or first input",
331
+ );
332
+ }
323
333
  if (model !== undefined && !(nonEmpty(model.provider) && nonEmpty(model.model))) {
324
334
  throw refuse("explicit-model-incomplete", "an explicit model names both its provider and its model id");
325
335
  }
@@ -333,6 +343,7 @@ export async function openDurableCitizen({
333
343
  durable = await openDurable({
334
344
  cwd,
335
345
  continueSession,
346
+ ...(session === undefined ? {} : { session }),
336
347
  extensions: [contact.extension, ...extensions],
337
348
  ...(model === undefined ? {} : { model }),
338
349
  });
@@ -389,6 +400,7 @@ export async function openDurableCitizen({
389
400
 
390
401
  function parseArgs(argv) {
391
402
  let continueSession = false;
403
+ let resume = false;
392
404
  const values = new Map();
393
405
  for (let i = 0; i < argv.length; i++) {
394
406
  const arg = argv[i];
@@ -396,6 +408,10 @@ function parseArgs(argv) {
396
408
  continueSession = true;
397
409
  continue;
398
410
  }
411
+ if (arg === "--resume") {
412
+ resume = true;
413
+ continue;
414
+ }
399
415
  if (!VALUE_FLAGS.includes(arg)) throw new Error(`Unknown argument: ${arg}`);
400
416
  if (values.has(arg)) throw refuse("argument-repeated", `${arg} is given more than once`);
401
417
  const value = argv[++i];
@@ -404,7 +420,19 @@ function parseArgs(argv) {
404
420
  }
405
421
  values.set(arg, value);
406
422
  }
423
+ if (resume && continueSession) {
424
+ throw refuse(
425
+ "resume-continue-exclusive",
426
+ "--resume chooses a session and --continue opens the newest one; give one of them",
427
+ );
428
+ }
407
429
  const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
430
+ if (resume && fresh.length > 0) {
431
+ throw refuse(
432
+ "fresh-only-argument-with-resume",
433
+ `${fresh.join(", ")} apply only to a new session; --resume reopens the chosen session as it was saved`,
434
+ );
435
+ }
408
436
  if (continueSession && fresh.length > 0) {
409
437
  throw refuse(
410
438
  "fresh-only-argument-with-continue",
@@ -426,6 +454,7 @@ function parseArgs(argv) {
426
454
  ? { nativeModules: nativeModuleDirFiles(dirArg) }
427
455
  : {};
428
456
  if (continueSession) return { continueSession, ...nativeModule };
457
+ if (resume) return { resume, ...nativeModule };
429
458
  const provider = values.get("--provider");
430
459
  if (provider === undefined) {
431
460
  throw refuse(
@@ -457,29 +486,102 @@ function parseArgs(argv) {
457
486
  };
458
487
  }
459
488
 
489
+ /** A local `YYYY-MM-DD HH:MM` for a list line. */
490
+ function localTime(ms) {
491
+ const date = new Date(ms);
492
+ const two = (n) => String(n).padStart(2, "0");
493
+ return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
494
+ }
495
+
460
496
  /**
461
- * Parse, initialize the operator's module, load the TUI, open, run, close. `loadTui`, `open` and
462
- * `contact` are test seams; `bridgeEntry` is the checkout-only gates' private bundle, as in
463
- * `openDurableCitizen` — never the launch argv.
497
+ * `--resume` (#143): this directory's durable sessions, most recently written first, each by what its files and
498
+ * the record store already say — creation time, garden id, last write, whether another process holds it. No
499
+ * session database is opened to list them, and nothing is named that the store does not hold. Resolves the
500
+ * chosen session's id, or undefined when the choice was cancelled; never a fallback to another session.
501
+ */
502
+ async function chooseSession(cwd, sessionsDir, pick) {
503
+ const { listSessions } = await import(SESSIONS);
504
+ const sessions = await listSessions(cwd);
505
+ if (sessions.length === 0) throw refuse("resume-no-session", `no durable session exists for ${cwd}`);
506
+ const { gardens, unreadable } = piDurableGardenIds(
507
+ sessions.map((session) => session.id),
508
+ sessionsDir,
509
+ );
510
+ const items = sessions.map((session) => ({
511
+ value: session.id,
512
+ label: `${localTime(session.createdAt)} ${gardens.get(session.id) ?? "no record"}`,
513
+ description: [
514
+ session.lastWriteAt === undefined ? "never written" : `last write ${localTime(session.lastWriteAt)}`,
515
+ session.id,
516
+ ...(session.openElsewhere ? ["open elsewhere"] : []),
517
+ ].join(" · "),
518
+ }));
519
+ const title =
520
+ unreadable === 0 ? "Resume durable session:" : `Resume durable session (${unreadable} unreadable record(s)):`;
521
+ const id = await pick(title, items);
522
+ if (id === undefined) return undefined;
523
+ const chosen = sessions.find((session) => session.id === id);
524
+ if (chosen === undefined) throw refuse("session-not-found", `${id} is not one of the listed sessions`);
525
+ if (chosen.openElsewhere) {
526
+ throw refuse("session-open-elsewhere", `${id} is open in another process; close it there first`);
527
+ }
528
+ return chosen.id;
529
+ }
530
+
531
+ /**
532
+ * Parse, initialize the operator's module, load the TUI, open, run, close. `loadTui`, `open`,
533
+ * `contact` and `pick` (the `--resume` list's choice) are test seams; `bridgeEntry` is the
534
+ * checkout-only gates' private bundle, as in `openDurableCitizen` — never the launch argv.
464
535
  *
465
536
  * @param {readonly string[]} argv
466
537
  * @param {{
467
- * loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void> }>,
538
+ * loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void>, pickDurableSession?: (title: string, items: object[]) => Promise<string | undefined> }>,
468
539
  * open?: (options: any) => Promise<any>,
469
540
  * contact?: Record<string, unknown>,
470
541
  * bridgeEntry?: string,
542
+ * pick?: (title: string, items: { value: string, label: string, description: string }[]) => Promise<string | undefined>,
471
543
  * }} [seams]
472
544
  */
473
- export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry } = {}) {
545
+ export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry, pick } = {}) {
474
546
  // Every argument refusal is decided here, before any module, the TUI or the app loads.
475
- const { nativeModules = [], ...args } = parseArgs(argv);
547
+ const { nativeModules = [], resume = false, ...args } = parseArgs(argv);
476
548
  // The caller's directory, fixed before any operator code runs and handed to the app explicitly.
477
549
  const cwd = process.cwd();
478
550
  const extensions = await loadNativeModules(nativeModules);
479
551
  // Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot
480
552
  // load opens no session, mints no citizen and leaves nothing to clean up.
481
- const { runDurableTui } = await loadTui();
482
- const citizen = await openDurableCitizen({ ...args, cwd, extensions, bridgeEntry, open, contact });
553
+ const tui = await loadTui();
554
+ const { runDurableTui } = tui;
555
+ let session;
556
+ if (resume) {
557
+ session = await chooseSession(
558
+ cwd,
559
+ contact?.sessionsDir,
560
+ pick ??
561
+ ((title, items) => {
562
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
563
+ throw refuse(
564
+ "resume-needs-terminal",
565
+ "--resume lists the sessions to choose from in an interactive terminal",
566
+ );
567
+ }
568
+ return tui.pickDurableSession(title, items);
569
+ }),
570
+ );
571
+ if (session === undefined) {
572
+ process.stderr.write("[pi-durable] resume-cancelled: nothing was opened\n");
573
+ return;
574
+ }
575
+ }
576
+ const citizen = await openDurableCitizen({
577
+ ...args,
578
+ ...(session === undefined ? {} : { session }),
579
+ cwd,
580
+ extensions,
581
+ bridgeEntry,
582
+ open,
583
+ contact,
584
+ });
483
585
  try {
484
586
  await runDurableTui(
485
587
  withGardenIdentity(citizen.durable.view, citizen.attachment.gardenId),
@@ -28,13 +28,16 @@ export function createHarnessSettings(settingsManager )
28
28
  const provider = settingsManager.getProviderRetrySettings();
29
29
  const idle = settingsManager.getHttpIdleTimeoutMs();
30
30
  return {
31
+ transport: settingsManager.getTransport(),
31
32
  timeoutMs: provider.timeoutMs ?? (idle === 0 ? 2147483647 : idle),
32
33
  maxRetryDelayMs: provider.maxRetryDelayMs,
33
34
  ...(provider.maxRetries === undefined ? {} : { maxRetries: provider.maxRetries }),
34
35
  };
35
36
  },
37
+ // The reference app waits for a compaction instead of summarizing beside the running turn (#143): no background
38
+ // compaction. Threshold, manual and overflow compaction stay the SDK's, on pi's own compaction settings.
36
39
  get compaction() {
37
- return settingsManager.getCompactionSettings();
40
+ return { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };
38
41
  },
39
42
  get retry() {
40
43
  return settingsManager.getRetrySettings();
@@ -1,5 +1,6 @@
1
+ import { isAbsolute, join, resolve } from "node:path";
1
2
  import { defineExtension, section } from "@earendil-works/pi-durable";
2
- import { getAgentDir } from "entwurf-pi-dist:config.js";
3
+ import { CONFIG_DIR_NAME, getAgentDir } from "entwurf-pi-dist:config.js";
3
4
  import { loadProjectContextFiles } from "entwurf-pi-dist:core/resource-loader.js";
4
5
 
5
6
  import { loadSkills, } from "entwurf-pi-dist:core/skills.js";
@@ -19,6 +20,29 @@ const CONTRIBUTIONS = {
19
20
  /** pi's section order; `buildSystemPromptSections()` omits the ones without content. */
20
21
  const KEYS = ["preamble", "tools", "rules", "docs", "project_context", "skills", "cwd"] ;
21
22
 
23
+ /** An entry pi reads as a pattern, not a path: an override or a glob. */
24
+ const PATTERN = /^[!+-]|[*?]/u;
25
+
26
+ /**
27
+ * The `skills` setting with each scope's relative entries read against that scope's own directory, as pi resolves
28
+ * them: the project's against the session directory's `.pi`, the global ones against the agent directory, the
29
+ * project's first. Read against the conversation's directory, a relative entry names another place. Absolute, `~`
30
+ * and pattern entries go on unchanged.
31
+ */
32
+ function scopedSkillPaths(settings , agentDir , projectCwd ) {
33
+ const against = (entries , base ) =>
34
+ (entries ?? []).map((entry) => {
35
+ const path = entry.trim();
36
+ return path === "" || isAbsolute(path) || path === "~" || path.startsWith("~/") || PATTERN.test(path)
37
+ ? entry
38
+ : resolve(base, path);
39
+ });
40
+ return [
41
+ ...against(settings.getProjectSettings().skills, join(projectCwd, CONFIG_DIR_NAME)),
42
+ ...against(settings.getGlobalSettings().skills, agentDir),
43
+ ];
44
+ }
45
+
22
46
  /**
23
47
  * pi's system prompt as one extension: the sections of `buildSystemPromptSections()` for the request's tools and the
24
48
  * conversation's directory. Context files and skills load once per directory, like pi at startup.
@@ -31,7 +55,12 @@ export function createPiPrompt(settings , fallbackCwd ) {
31
55
  const agentDir = getAgentDir();
32
56
  found = {
33
57
  contextFiles: loadProjectContextFiles({ cwd, agentDir }),
34
- skills: loadSkills({ cwd, agentDir, skillPaths: settings.getSkillPaths(), includeDefaults: true }).skills,
58
+ skills: loadSkills({
59
+ cwd,
60
+ agentDir,
61
+ skillPaths: scopedSkillPaths(settings, agentDir, fallbackCwd),
62
+ includeDefaults: true,
63
+ }).skills,
35
64
  };
36
65
  resources.set(cwd, found);
37
66
  }
@@ -90,6 +90,8 @@ const context = BACKGROUND_CONTEXT;
90
90
 
91
91
 
92
92
 
93
+
94
+
93
95
 
94
96
 
95
97
 
@@ -143,10 +145,17 @@ function titleOf(entry ) {
143
145
  }
144
146
 
145
147
  export async function openDurable(options = {}) {
146
- if (options.model !== undefined && options.continueSession) {
148
+ if (options.model !== undefined && (options.continueSession || options.session !== undefined)) {
147
149
  throw new Error("An explicit model applies only to a new session, not to a continued one");
148
150
  }
149
- const location = await selectSession(options.cwd ?? process.cwd(), options.continueSession ?? false);
151
+ if (options.continueSession && options.session !== undefined) {
152
+ throw new Error("A chosen session and the newest one are two different sessions; name one");
153
+ }
154
+ const location = await selectSession(
155
+ options.cwd ?? process.cwd(),
156
+ options.continueSession ?? false,
157
+ options.session,
158
+ );
150
159
  const envs = new ExecutionEnvs(location.cwd);
151
160
  let harness ;
152
161
  try {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { mkdir, readdir, realpath } from "node:fs/promises";
2
+
3
+ import { mkdir, readdir, realpath, stat } from "node:fs/promises";
3
4
  import { basename, join, resolve } from "node:path";
4
5
  import lockfile from "proper-lockfile";
5
6
  import { getAgentDir } from "entwurf-pi-dist:config.js";
@@ -14,8 +15,31 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
14
15
 
15
16
 
16
17
 
17
- /** A new session for `cwd`, or its newest one with `continueSession`. */
18
- export async function selectSession(cwdInput , continueSession ) {
18
+ /** One session of a directory as its files show it (#143): nothing in it is opened. */
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+ const SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/u;
31
+
32
+ /** A path's stat, or undefined when nothing is there; any other failure is thrown, never read as absence. */
33
+ async function statIfPresent(path ) {
34
+ try {
35
+ return await stat(path);
36
+ } catch (error) {
37
+ if ((error ).code === "ENOENT") return undefined;
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ async function sessionRoot(cwdInput ) {
19
43
  const cwd = await realpath(resolve(cwdInput));
20
44
  const root = join(
21
45
  getAgentDir(),
@@ -23,14 +47,60 @@ export async function selectSession(cwdInput , continueSession )
23
47
  "durable-sessions",
24
48
  createHash("sha256").update(cwd).digest("hex").slice(0, 24),
25
49
  );
50
+ return { cwd, root };
51
+ }
52
+
53
+ /** The sessions of `cwd`, most recently written first. Reads names, file times and locks; creates nothing. */
54
+ export async function listSessions(cwdInput ) {
55
+ const { root } = await sessionRoot(cwdInput);
56
+ let entries ;
57
+ try {
58
+ entries = await readdir(root, { withFileTypes: true });
59
+ } catch (error) {
60
+ if ((error ).code === "ENOENT") return [];
61
+ throw error;
62
+ }
63
+ const sessions = [];
64
+ for (const entry of entries) {
65
+ if (!entry.isDirectory() || !SESSION_NAME.test(entry.name)) continue;
66
+ const directory = join(root, entry.name);
67
+ let lastWriteAt ;
68
+ for (const file of ["session.sqlite", "session.sqlite-wal"]) {
69
+ const mtime = (await statIfPresent(join(directory, file)))?.mtimeMs;
70
+ if (mtime !== undefined && (lastWriteAt === undefined || mtime > lastWriteAt)) lastWriteAt = mtime;
71
+ }
72
+ sessions.push({
73
+ id: entry.name,
74
+ directory,
75
+ createdAt: Number(entry.name.slice(0, 13)),
76
+ ...(lastWriteAt === undefined ? {} : { lastWriteAt }),
77
+ openElsewhere: await lockfile.check(directory, { realpath: false }),
78
+ });
79
+ }
80
+ return sessions.sort((a, b) => (b.lastWriteAt ?? b.createdAt) - (a.lastWriteAt ?? a.createdAt));
81
+ }
82
+
83
+ /** A new session for `cwd`, its newest one with `continueSession`, or the existing one named by `sessionId`. */
84
+ export async function selectSession(
85
+ cwdInput ,
86
+ continueSession ,
87
+ sessionId ,
88
+ ) {
89
+ const { cwd, root } = await sessionRoot(cwdInput);
26
90
  await mkdir(root, { recursive: true });
27
91
 
28
92
  let directory ;
29
93
  let created = false;
30
- if (continueSession) {
94
+ if (sessionId !== undefined) {
95
+ const chosen = SESSION_NAME.test(sessionId) ? join(root, sessionId) : undefined;
96
+ if (chosen === undefined || !(await statIfPresent(chosen))?.isDirectory()) {
97
+ throw new Error(`No durable session ${sessionId} exists for ${cwd}`);
98
+ }
99
+ directory = chosen;
100
+ } else if (continueSession) {
31
101
  const entries = await readdir(root, { withFileTypes: true });
32
102
  const newest = entries
33
- .filter((entry) => entry.isDirectory() && /^\d{13}-[0-9a-f-]{36}$/u.test(entry.name))
103
+ .filter((entry) => entry.isDirectory() && SESSION_NAME.test(entry.name))
34
104
  .map((entry) => entry.name)
35
105
  .sort()
36
106
  .at(-1);
@@ -113,6 +113,29 @@ class ListSelector extends Container {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Choose one of the sessions `--resume` lists (#143) before any of them opens: the /agents list on a screen of its
118
+ * own, stopped before the session's TUI starts. Resolves the chosen value, or undefined when cancelled.
119
+ */
120
+ export function pickDurableSession(title , items ) {
121
+ initTheme();
122
+ const ui = new TuiAltScreen(new ProcessTerminal(), false, getAgentDir());
123
+ setKeybindings(KeybindingsManager.create());
124
+ return new Promise((resolve) => {
125
+ let done = false;
126
+ const finish = (value ) => {
127
+ if (done) return;
128
+ done = true;
129
+ ui.stop();
130
+ resolve(value);
131
+ };
132
+ const selector = new ListSelector(title, items, finish, () => finish(undefined));
133
+ ui.addChild(selector);
134
+ ui.setFocus(selector);
135
+ ui.start();
136
+ });
137
+ }
138
+
116
139
  /** The summary that replaced earlier context: collapsed to one line until expanded. */
117
140
  class CompactionComponent extends Box {
118
141
  #summary ;
@@ -70,32 +70,32 @@
70
70
  {
71
71
  "name": "runtime.js",
72
72
  "source": "pi/pi-durable/src/experimental/durable/runtime.ts",
73
- "blob": "36c352291620ab86184172a1b4a0df4562ab9581",
74
- "sha256": "60c2965eadbdbbe78fce1b7eeb89d97eb3104a30b7d58ebca87009d2e9118403"
73
+ "blob": "75d7bf2b4fdc36aa775f73f9d7f3de8659f19187",
74
+ "sha256": "725757156580ad2a2d170c790440763371bb8035052a0c6caae6af5159d4718f"
75
75
  },
76
76
  {
77
77
  "name": "tui.js",
78
78
  "source": "pi/pi-durable/src/experimental/durable/tui.ts",
79
- "blob": "dab99cd4778ae1db7f062e64db2d0c068f7e342d",
80
- "sha256": "0f9ebaf8fbb2a532ccd1a7198e1a25533a99720b5fedbf03f42d5c381db3abc5"
79
+ "blob": "eb5e2ec4e98919cd3ff4f81f166eb52afe6555af",
80
+ "sha256": "a1b0d9d9358c5b54ac0974de84068b7792669171917ebd7b7351374ed625c9c5"
81
81
  },
82
82
  {
83
83
  "name": "harness-setup.js",
84
84
  "source": "pi/pi-durable/src/experimental/durable/harness-setup.ts",
85
- "blob": "4d6a4be5333ebb8e0f17eac1987f18ea35a91fc7",
86
- "sha256": "712b630145dc6f5c655a85b01b1e20299c962542e63c5494d2202bb7bf414a15"
85
+ "blob": "36a622ec6d5dccda828ffaceaf74ce8ec241e39d",
86
+ "sha256": "efb06ae25258fbbab5b28f8a9bbb50956c7ebfd77a5ed23b065706f662e70ba3"
87
87
  },
88
88
  {
89
89
  "name": "prompt.js",
90
90
  "source": "pi/pi-durable/src/experimental/durable/prompt.ts",
91
- "blob": "8e2f5d5273fba58da8b314766956ed62951c3814",
92
- "sha256": "ff1ff765fa313e5cb1c3d7537c901c23adb0562ee433bd050fd35eb3d892134c"
91
+ "blob": "0e99ff64214c44986c1c2259490e401898659fd5",
92
+ "sha256": "15870ea4c9cb465395d70189be4f1480c48544d724bd9fc3b0e6650f17226c0f"
93
93
  },
94
94
  {
95
95
  "name": "sessions.js",
96
96
  "source": "pi/pi-durable/src/experimental/durable/sessions.ts",
97
- "blob": "34f9ceeeaeaf874f7478d56115da2e4f726c6a85",
98
- "sha256": "1bd1a04ece66f5ab3ca22860c64dba9abc848334d11a27a9afe543f1365c7254"
97
+ "blob": "eed4b3ca51c6f5a3bab8106adaf0062f3f7d5b56",
98
+ "sha256": "668373feadabcf13bd4dd7431e5429b7d7376c47c367e4b15353c57cb8711892"
99
99
  },
100
100
  {
101
101
  "name": "subagent.js",
@@ -113,6 +113,7 @@ import {
113
113
  defaultMetaReceiversDir,
114
114
  defaultMetaSendersDir,
115
115
  defaultMetaSessionsDir,
116
+ listAllMetaIdentitiesDir,
116
117
  readAddressableMetaIdentity,
117
118
  readMetaReceiverMarker,
118
119
  readMetaSenderMarker,
@@ -125,6 +126,25 @@ import {
125
126
  /** The backend id this contact mints under. */
126
127
  export const PI_DURABLE_BACKEND = "pi-durable" as const;
127
128
 
129
+ /**
130
+ * The garden ids the record store holds for these durable sessions (#143 `--resume`), read only: a session no record
131
+ * names is absent, and an unreadable record is counted, never guessed.
132
+ */
133
+ export function piDurableGardenIds(
134
+ nativeSessionIds: readonly string[],
135
+ sessionsDir: string = defaultMetaSessionsDir(),
136
+ ): { gardens: Map<string, string>; unreadable: number } {
137
+ const wanted = new Set(nativeSessionIds);
138
+ const { identities, errors } = listAllMetaIdentitiesDir(sessionsDir);
139
+ const gardens = new Map<string, string>();
140
+ for (const identity of identities) {
141
+ if (identity.backend === PI_DURABLE_BACKEND && wanted.has(identity.nativeSessionId)) {
142
+ gardens.set(identity.nativeSessionId, identity.gardenId);
143
+ }
144
+ }
145
+ return { gardens, unreadable: errors.length };
146
+ }
147
+
128
148
  /** The durable Extension's name, owned exactly. */
129
149
  export const PI_DURABLE_EXTENSION_NAME = "entwurf";
130
150
 
package/run.sh CHANGED
@@ -273,7 +273,7 @@ Usage:
273
273
  ./run.sh uninstall-meta-bridge # 1.0.0 meta-bridge Phase 2: stateful GLOBAL uninstall (restore only keys/items captured in install-state)
274
274
  ./run.sh doctor-meta-bridge # THE RELEASE ORACLE (#51, Linux-certified repair axis). exit 0 = every required layer was MEASURED on this Linux host: toolchain + state + plugin/MCP + resolved-artifact launch-form classification (all 3 owner hooks + doorbell static contract) + synthetic owner join + store scan + hook errors + SessionStart evidence + REQUIRED live MCP↔marker join + writer-version parity. Missing live evidence is NOT CERTIFIED (open a Claude session and re-run), never a pass; Darwin is NOT CERTIFIED and stays nonzero, on ONE narrowed reason — start-key and ppid are portable there (`ps` fallback, same string the TS core mints), so what holds the lane closed is per-process environment DISCOVERY alone, pending physical-host evidence. Detection power is held by check-meta-doctor-oracle
275
275
  ./run.sh copilot [args...] # #82 RAIL 7: the MANAGED Copilot launch. exec()s the vendor CLI in THIS terminal (cwd/pid/exit preserved) with COPILOT_CLI_ENABLED_FEATURE_FLAGS=EXTENSIONS set for that one invocation — entwurf owns no part of your shell and writes nothing to it, but it owns the process it is about to become. Without that flag Copilot skips the extension scan SILENTLY. Refuses to launch unless the receiver unit is actually installed. Injects `--model auto` when no --model was given and `--yolo` when no explicit permission/surface policy flag was given, both BEFORE any `--`; every argument you pass is forwarded byte-identical. RUNNING THIS IS YOUR CONSENT to that profile — use plain `copilot` for stock vendor behaviour. Not tmux, not fresh-call, mints no citizen (birth is still the first prompt)
276
- ./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/, emitted deterministically from Entwurf's owned source pi/pi-durable/src) and the pi SDK set it resolves — every declared member/direct edge at exactly the pinned version — refusing by name (package-incomplete / carrier-drift / sdk-absent / sdk-mismatch, exit 3), then exec()s node under the carrier resolver with the packaged bootstrap (pi/pi-durable/bootstrap.mjs) in this terminal. A new session needs --provider <p> --model <exact catalog id> --width task-wide [--entwurf-bootstrap <{v:2,task}>]; --continue takes none of them. Nothing outside the package is located, installed or repaired
276
+ ./run.sh pi-durable [args...] # #129/#130: the MANAGED durable launch. Verifies this package's pi-durable CARRIER (pi/pi-durable/carrier/, emitted deterministically from Entwurf's owned source pi/pi-durable/src) and the pi SDK set it resolves — every declared member/direct edge at exactly the pinned version — refusing by name (package-incomplete / carrier-drift / sdk-absent / sdk-mismatch, exit 3), then exec()s node under the carrier resolver with the packaged bootstrap (pi/pi-durable/bootstrap.mjs) in this terminal. A new session needs --provider <p> --model <exact catalog id> --width task-wide [--entwurf-bootstrap <{v:2,task}>]; --continue (this directory's newest session) and --resume (choose one of its sessions) take none of them. Nothing outside the package is located, installed or repaired
277
277
  ./run.sh install-copilot-bridge # #82: GLOBAL install of the Copilot BIRTH plugin (own marketplace root; node+entry baked into the no-argv exec string). MCP wiring and the RECEIVER extension are separate install surfaces (install-copilot-mcp, install-copilot-receive). Also retires the stale Claude unit (--keep-stale-claude-unit opts out)
278
278
  ./run.sh uninstall-copilot-bridge # #86 C3a: honest inverse of install-copilot-bridge from the package-owned install-state (exact qualified id + exact marketplace name/path + recorded assembly only; state deleted LAST; a failing vendor list is UNKNOWN and refuses; never --force, never the stale Claude unit)
279
279
  ./run.sh doctor-copilot-bridge # #82/#86: fail-loud surface for that unit — runtime axis (red = a hook that RAN and failed, or a broken/unbaked artifact; "installed with zero records" is NOT red and is reported as NOT-YET: a Copilot session is born on its FIRST PROMPT, not when the window opens, measured) + ownership axis (install-state shape/binding, marketplace path drift, UNKNOWN vendor lists; legacy no-state install = named non-green, repair = install-copilot-bridge adoption). Either axis red = red
@@ -3464,7 +3464,7 @@ let manifestCount: number;
3464
3464
  "pi-bridge-sender": 4,
3465
3465
  "pi-mcp-bridge": 2,
3466
3466
  "pi-mcp-register": 3,
3467
- "pi-durable": 78,
3467
+ "pi-durable": 84,
3468
3468
  "copilot-receive": 20,
3469
3469
  "entwurf-peers": 1,
3470
3470
  "fresh-call-dispatch": 12,
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "$comment": "SSOT for the herdr binary this repository has MEASURED against. Nothing else may carry a version or a digest: the CI installer, the sandbox gate's boundary check and the launch-rail document all read or cite this file. Re-measuring a new herdr means measuring it FIRST — fetched by digest and verified before it is made executable, then run against a private sandbox server (at least version, status-client protocol and integration rows) — then editing this file and repairing the measured sentences in docs/herdr-launch-rail.md in the same commit; miss one and the gate fails on version mismatch, which is the point. A private baseline is not the full gate: check-herdr-sandbox at a new pin is certified by its exact-SHA CI run unless it was also run locally, and an asset is recorded `executed` only with a receipt bound to this version and its full digest (check-herdr-supply HSUP-EXECUTION-RECEIPT-BOUND).",
3
- "version": "0.9.4",
4
- "tag": "v0.9.4",
5
- "tagCommit": "7e26bad94044eba205033111a0ce8033ede7e24f",
3
+ "version": "0.9.5",
4
+ "tag": "v0.9.5",
5
+ "tagCommit": "2ecf19a92fc3a7fff75b6ce3655ef04f4625ec01",
6
6
  "socketProtocol": 22,
7
- "measuredOn": "2026-10-14",
7
+ "measuredOn": "2026-10-15",
8
8
  "measuredArch": "aarch64",
9
9
  "releaseUrlPrefix": "https://github.com/herdrdev/herdr/releases/download",
10
10
  "integrity": {
@@ -15,16 +15,17 @@
15
15
  "assets": {
16
16
  "x86_64": {
17
17
  "name": "herdr-linux-x86_64",
18
- "sha256": "493304fbefb0ed0ea0455f373b47f48d24e8774bc7974f4c916ad2e937675b23",
19
- "executed": false,
20
- "executedNote": "not yet executed at this pin: the digest is pinned and named by GitHub's release attestation for pkg:github/herdrdev/herdr@v0.9.4, and the first CI check job at a SHA carrying this pin will be its first run. The previous pin's x86_64 asset (v0.9.3, sha256 18a8dc65…) did run there — CI run 38456259822, check-herdr-sandbox 12 assertions — which is dated history of that asset, not evidence for this one"
18
+ "sha256": "14d9e8bc8b4a9e0118d77c32266f76d461f09b40342eb23752960bd89f59929d",
19
+ "executed": true,
20
+ "executedReceipt": "herdr 0.9.5 linux-x86_64 sha256 14d9e8bc8b4a9e0118d77c32266f76d461f09b40342eb23752960bd89f59929d — 2026-10-15, GitHub Actions check job (x86_64), CI run 38667564413 at d8cd8f3, read from that run's check-job log: `install-herdr-ci` verified this digest before execution, and check-herdr-sandbox passed 12 assertions on private herdr 0.9.5, HS-VERSION-BOUNDARY included",
21
+ "executedNote": "the receipt is the full sandbox gate at the SHA that introduced this pin; it is not a claim for any later SHA. The previous pin's x86_64 asset (v0.9.4, sha256 493304fb…) ran in CI run 38479767989 at 9f33dd1, check-herdr-sandbox 12 assertions — dated history of that asset, not evidence for this one"
21
22
  },
22
23
  "aarch64": {
23
24
  "name": "herdr-linux-aarch64",
24
- "sha256": "9511cb70982c1ef115f232058e7ed7cf9640aa5a2c0ca78934cefda3aeb093fd",
25
+ "sha256": "17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c",
25
26
  "executed": true,
26
- "executedReceipt": "herdr 0.9.4 linux-aarch64 sha256 9511cb70982c1ef115f232058e7ed7cf9640aa5a2c0ca78934cefda3aeb093fd — 2026-10-14, oracle aarch64, Phase A private CLI baseline (docs/herdr-launch-rail.md, 재측정 2026-10-14): `herdr --version` 0.9.4; `status client` protocol 22, endpoint_protocol_generation 1; `integration status` 18 rows, zero diff against 0.9.3; pi (v9) and claude (v10) integrations seeded by 0.9.3 read `current` and a 0.9.4 reinstall leaves their three files byte-identical; `server stop` leaves no socket and no process",
27
- "executedNote": "the receipt is a private CLI baseline only. check-herdr-sandbox has NOT run on this architecture at this pin — its operator-untouched cell reads the operator's own panes — so the full gate's first run at this pin is the CI check job, on the x86_64 asset — not this one. The operator host still runs 0.9.3 and was not touched. The 0.9.3 receipt (2026-10-03, #126) stays dated in docs/herdr-launch-rail.md"
27
+ "executedReceipt": "herdr 0.9.5 linux-aarch64 sha256 17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c — 2026-10-15, oracle aarch64, Phase A private CLI baseline (docs/herdr-launch-rail.md, 재측정 2026-10-15): `herdr --version` 0.9.5; `status client` protocol 22, endpoint_protocol_generation 1; `integration status` 18 rows, zero diff against 0.9.4 in a fresh sandbox; over integrations seeded by 0.9.4, pi reads `current (v9)` and claude reads `outdated (v10 < v11)`; a 0.9.5 reinstall leaves the pi extension and claude settings.json byte-identical, changes the claude hook only in its `HERDR_INTEGRATION_VERSION` line (10 → 11), and then reads `current (v11)`; `server stop` leaves no socket and no process",
28
+ "executedNote": "the receipt is a private CLI baseline only. check-herdr-sandbox has NOT run on this architecture at this pin — its operator-untouched cell reads the operator's own panes — so the full gate's first run at this pin was the CI check job on the x86_64 asset (CI run 38667564413) — not this one. The operator host was not touched. The 0.9.4 receipt (2026-10-14) stays dated in docs/herdr-launch-rail.md"
28
29
  }
29
30
  }
30
31
  }
@@ -8,7 +8,7 @@
8
8
  "subject": "scripts/check-herdr-sandbox.ts",
9
9
  "find": ["const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";"],
10
10
  "replace": [
11
- "const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";\nconst PINNED_FALLBACK = \"0.9.4\";\nvoid PINNED_FALLBACK;"
11
+ "const SUPPLY_MANIFEST = \"scripts/fixtures/herdr-supply.json\";\nconst PINNED_FALLBACK = \"0.9.5\";\nvoid PINNED_FALLBACK;"
12
12
  ],
13
13
  "gate": ["bash", "run.sh", "check-herdr-supply"],
14
14
  "timeoutSeconds": 120,
@@ -19,8 +19,8 @@
19
19
  "claim": "HSUP-DIGEST-SHAPE",
20
20
  "title": "a pinned digest is truncated, so the check that is supposed to catch a swapped binary can no longer fail",
21
21
  "subject": "scripts/fixtures/herdr-supply.json",
22
- "find": ["\"sha256\": \"493304fbefb0ed0ea0455f373b47f48d24e8774bc7974f4c916ad2e937675b23\""],
23
- "replace": ["\"sha256\": \"493304fbefb0ed0ea0455f373b47\""],
22
+ "find": ["\"sha256\": \"14d9e8bc8b4a9e0118d77c32266f76d461f09b40342eb23752960bd89f59929d\""],
23
+ "replace": ["\"sha256\": \"14d9e8bc8b4a9e0118d77c32266f\""],
24
24
  "gate": ["bash", "run.sh", "check-herdr-supply"],
25
25
  "timeoutSeconds": 120,
26
26
  "signature": "[QK:HSUP-DIGEST-SHAPE]",
@@ -122,8 +122,8 @@
122
122
  "claim": "HSUP-EXECUTION-RECEIPT-BOUND",
123
123
  "title": "the x86_64 asset is recorded as executed with no receipt bound to this pin's version and its own full digest",
124
124
  "subject": "scripts/fixtures/herdr-supply.json",
125
- "find": ["\"executed\": false"],
126
- "replace": ["\"executed\": true"],
125
+ "find": ["\"executedReceipt\": \"herdr 0.9.5 linux-x86_64"],
126
+ "replace": ["\"unboundReceipt\": \"herdr 0.9.5 linux-x86_64"],
127
127
  "gate": ["bash", "run.sh", "check-herdr-supply"],
128
128
  "timeoutSeconds": 120,
129
129
  "signature": "[QK:HSUP-EXECUTION-RECEIPT-BOUND]",
@@ -458,7 +458,7 @@
458
458
  "claim": "PI-DURABLE-CARRIER-PIN-MATCHES",
459
459
  "title": "the pin records a digest the committed carrier file does not carry, so a carrier edited after emission would still be the declared one",
460
460
  "subject": "pi/pi-durable/overlay/upstream-pin.json",
461
- "find": ["\"sha256\": \"60c2965eadbdbbe78fce1b7eeb89d97eb3104a30b7d58ebca87009d2e9118403\""],
461
+ "find": ["\"sha256\": \"725757156580ad2a2d170c790440763371bb8035052a0c6caae6af5159d4718f\""],
462
462
  "replace": ["\"sha256\": \"0000000000000000000000000000000000000000000000000000000000000000\""],
463
463
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
464
464
  "timeoutSeconds": 300,
@@ -701,8 +701,8 @@
701
701
  "claim": "PI-DURABLE-MODULE-ABSENT-UNCHANGED",
702
702
  "title": "the module phase runs without a --native-module, so a launch that names no module still tries to import one",
703
703
  "subject": "pi/pi-durable/bootstrap.mjs",
704
- "find": ["\tconst { nativeModules = [], ...args } = parseArgs(argv);"],
705
- "replace": ["\tconst { nativeModules = [undefined], ...args } = parseArgs(argv);"],
704
+ "find": ["\tconst { nativeModules = [], resume = false, ...args } = parseArgs(argv);"],
705
+ "replace": ["\tconst { nativeModules = [undefined], resume = false, ...args } = parseArgs(argv);"],
706
706
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
707
707
  "timeoutSeconds": 300,
708
708
  "signature": "[QK:PI-DURABLE-MODULE-ABSENT-UNCHANGED]",
@@ -735,12 +735,12 @@
735
735
  "\tconst extensions = await loadNativeModules(nativeModules);",
736
736
  "\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
737
737
  "\t// load opens no session, mints no citizen and leaves nothing to clean up.",
738
- "\tconst { runDurableTui } = await loadTui();"
738
+ "\tconst tui = await loadTui();"
739
739
  ],
740
740
  "replace": [
741
741
  "\t// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot",
742
742
  "\t// load opens no session, mints no citizen and leaves nothing to clean up.",
743
- "\tconst { runDurableTui } = await loadTui();",
743
+ "\tconst tui = await loadTui();",
744
744
  "\tconst extensions = await loadNativeModules(nativeModules);"
745
745
  ],
746
746
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
@@ -791,8 +791,8 @@
791
791
  "claim": "PI-DURABLE-MODULE-CWD-PINNED",
792
792
  "title": "the app is opened wherever the process is at open time, not in the caller's directory fixed before the module ran",
793
793
  "subject": "pi/pi-durable/bootstrap.mjs",
794
- "find": ["\tconst citizen = await openDurableCitizen({ ...args, cwd, extensions, bridgeEntry, open, contact });"],
795
- "replace": ["\tconst citizen = await openDurableCitizen({ ...args, extensions, bridgeEntry, open, contact });"],
794
+ "find": ["\t\t...(session === undefined ? {} : { session }),\n\t\tcwd,\n\t\textensions,\n\t\tbridgeEntry,"],
795
+ "replace": ["\t\t...(session === undefined ? {} : { session }),\n\t\textensions,\n\t\tbridgeEntry,"],
796
796
  "gate": ["bash", "run.sh", "check-pi-durable-beside"],
797
797
  "timeoutSeconds": 300,
798
798
  "signature": "[QK:PI-DURABLE-MODULE-CWD-PINNED]",
@@ -1142,7 +1142,7 @@
1142
1142
  },
1143
1143
  {
1144
1144
  "claim": "PI-DURABLE-COMPACTION-FAILURE-SHOWN",
1145
- "title": "the TUI drops the conversation's retained compaction failure: a failed background compaction vanishes from the screen once its status is removed",
1145
+ "title": "the TUI drops the conversation's retained compaction failure: a failed threshold compaction vanishes from the screen once its status is removed",
1146
1146
  "subject": "pi/pi-durable/carrier/tui.js",
1147
1147
  "find": ["\tif (compaction !== undefined) {\n\t\tlines.push"],
1148
1148
  "replace": ["\tif (compaction === null) {\n\t\tlines.push"],
@@ -1151,6 +1151,78 @@
1151
1151
  "signature": "[QK:PI-DURABLE-COMPACTION-FAILURE-SHOWN]",
1152
1152
  "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1153
1153
  "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1154
+ },
1155
+ {
1156
+ "claim": "PI-DURABLE-STREAM-TRANSPORT-WIRE",
1157
+ "title": "the Harness settings stop carrying the transport: the provider never continues a response, so every request of a long session resends the whole conversation",
1158
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1159
+ "find": ["\t\t\t\ttransport: settingsManager.getTransport(),"],
1160
+ "replace": ["\t\t\t\t// transport is not forwarded"],
1161
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1162
+ "timeoutSeconds": 300,
1163
+ "signature": "[QK:PI-DURABLE-STREAM-TRANSPORT-WIRE]",
1164
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1165
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1166
+ },
1167
+ {
1168
+ "claim": "PI-DURABLE-STREAM-TRANSPORT-FORWARDED",
1169
+ "title": "the Harness settings fix the transport instead of reading the operator's: a chosen sse or plain websocket is silently replaced by auto",
1170
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1171
+ "find": ["\t\t\t\ttransport: settingsManager.getTransport(),"],
1172
+ "replace": ["\t\t\t\ttransport: \"auto\","],
1173
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1174
+ "timeoutSeconds": 300,
1175
+ "signature": "[QK:PI-DURABLE-STREAM-TRANSPORT-FORWARDED]",
1176
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1177
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1178
+ },
1179
+ {
1180
+ "claim": "PI-DURABLE-COMPACTION-WAITS",
1181
+ "title": "the Harness settings hand the SDK pi's compaction settings alone: its default background policy summarizes beside a running turn instead of the turn waiting",
1182
+ "subject": "pi/pi-durable/carrier/harness-setup.js",
1183
+ "find": ["\t\t\treturn { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };"],
1184
+ "replace": ["\t\t\treturn settingsManager.getCompactionSettings();"],
1185
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1186
+ "timeoutSeconds": 300,
1187
+ "signature": "[QK:PI-DURABLE-COMPACTION-WAITS]",
1188
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1189
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1190
+ },
1191
+ {
1192
+ "claim": "PI-DURABLE-PROMPT-SKILL-SCOPE-BASE",
1193
+ "title": "the prompt hands the skills setting over as written: a relative project entry is read against the conversation's directory and its skills are silently not listed",
1194
+ "subject": "pi/pi-durable/carrier/prompt.js",
1195
+ "find": ["\t\t\t\t\tskillPaths: scopedSkillPaths(settings, agentDir, fallbackCwd),"],
1196
+ "replace": ["\t\t\t\t\tskillPaths: settings.getSkillPaths(),"],
1197
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1198
+ "timeoutSeconds": 300,
1199
+ "signature": "[QK:PI-DURABLE-PROMPT-SKILL-SCOPE-BASE]",
1200
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1201
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1202
+ },
1203
+ {
1204
+ "claim": "PI-DURABLE-RESUME-OPENS-CHOSEN",
1205
+ "title": "the runtime drops the chosen session id: --resume opens a new session instead of the conversation the operator picked",
1206
+ "subject": "pi/pi-durable/carrier/runtime.js",
1207
+ "find": ["\t\toptions.continueSession ?? false,\n\t\toptions.session,\n\t);"],
1208
+ "replace": ["\t\toptions.continueSession ?? false,\n\t);"],
1209
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1210
+ "timeoutSeconds": 300,
1211
+ "signature": "[QK:PI-DURABLE-RESUME-OPENS-CHOSEN]",
1212
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1213
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1214
+ },
1215
+ {
1216
+ "claim": "PI-DURABLE-RESUME-CANCEL-OPENS-NOTHING",
1217
+ "title": "a cancelled --resume falls back to the newest session: the operator said no and a conversation opens anyway",
1218
+ "subject": "pi/pi-durable/bootstrap.mjs",
1219
+ "find": ["\t\t\tprocess.stderr.write(\"[pi-durable] resume-cancelled: nothing was opened\\n\");\n\t\t\treturn;"],
1220
+ "replace": ["\t\t\targs.continueSession = true;"],
1221
+ "gate": ["bash", "run.sh", "check-pi-durable-beside"],
1222
+ "timeoutSeconds": 300,
1223
+ "signature": "[QK:PI-DURABLE-RESUME-CANCEL-OPENS-NOTHING]",
1224
+ "signatureSource": "pi-extensions/lib/pi-durable-carrier.test.ts",
1225
+ "build": { "argv": ["bash", "scripts/build-bridge.sh"], "output": "mcp/entwurf-bridge/dist" }
1154
1226
  }
1155
1227
  ]
1156
1228
  }