@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 +96 -0
- package/README.md +5 -0
- package/VERIFY.md +8 -1
- package/docs/setup-clean-host.md +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +16 -1
- package/package.json +3 -2
- package/pi/pi-durable/bootstrap.mjs +113 -11
- package/pi/pi-durable/carrier/harness-setup.js +4 -1
- package/pi/pi-durable/carrier/prompt.js +31 -2
- package/pi/pi-durable/carrier/runtime.js +11 -2
- package/pi/pi-durable/carrier/sessions.js +75 -5
- package/pi/pi-durable/carrier/tui.js +23 -0
- package/pi/pi-durable/overlay/upstream-pin.json +10 -10
- package/pi-extensions/meta-bridge-pi-durable.ts +20 -0
- package/run.sh +1 -1
- package/scripts/check-gate-qualification.ts +1 -1
- package/scripts/fixtures/herdr-supply.json +11 -10
- package/scripts/mutants/herdr-supply.json +5 -5
- package/scripts/mutants/pi-durable.json +80 -8
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.
|
|
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
|
package/docs/setup-clean-host.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
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
|
|
482
|
-
const
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
18
|
-
|
|
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 (
|
|
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() &&
|
|
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": "
|
|
74
|
-
"sha256": "
|
|
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": "
|
|
80
|
-
"sha256": "
|
|
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": "
|
|
86
|
-
"sha256": "
|
|
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": "
|
|
92
|
-
"sha256": "
|
|
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": "
|
|
98
|
-
"sha256": "
|
|
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
|
|
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
|
|
@@ -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
|
-
"tag": "v0.9.
|
|
5
|
-
"tagCommit": "
|
|
3
|
+
"version": "0.9.5",
|
|
4
|
+
"tag": "v0.9.5",
|
|
5
|
+
"tagCommit": "2ecf19a92fc3a7fff75b6ce3655ef04f4625ec01",
|
|
6
6
|
"socketProtocol": 22,
|
|
7
|
-
"measuredOn": "2026-10-
|
|
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": "
|
|
19
|
-
"executed":
|
|
20
|
-
"
|
|
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": "
|
|
25
|
+
"sha256": "17d7536f2da50859523680ef279dffcac8e4a235f4e46fe8da6c2ec44141897c",
|
|
25
26
|
"executed": true,
|
|
26
|
-
"executedReceipt": "herdr 0.9.
|
|
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
|
|
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.
|
|
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\": \"
|
|
23
|
-
"replace": ["\"sha256\": \"
|
|
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": ["\"
|
|
126
|
-
"replace": ["\"
|
|
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\": \"
|
|
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
|
|
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
|
|
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": ["\
|
|
795
|
-
"replace": ["\
|
|
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
|
|
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
|
}
|