@junghanacs/entwurf 0.23.1 → 0.25.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/BASELINE.md +1 -0
- package/CHANGELOG.md +628 -0
- package/DELIVERY.md +24 -1
- package/FAQ.md +47 -0
- package/README.md +5 -3
- package/VERIFY.md +20 -2
- package/demo/README.md +5 -2
- package/docs/acp-backend-rail.md +49 -2
- package/docs/setup-clean-host.md +8 -7
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +38 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/context.js +19 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/callback-env.js +68 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/control-send-receipt.js +101 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +17 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +4 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +53 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +38 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +54 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +20 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +24 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +19 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +35 -42
- package/mcp/entwurf-bridge/dist/protocol.js +0 -7
- package/mcp/entwurf-bridge/dist/scripts/doctor-pi-provider.js +40 -3
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +72 -0
- package/mcp/entwurf-bridge/dist/scripts/probe-bridge-command.js +1 -0
- package/mcp/entwurf-bridge/src/index.ts +46 -5
- package/mcp/entwurf-bridge/test.sh +2 -2
- package/package.json +11 -9
- package/pi-extensions/entwurf-control.ts +83 -154
- package/pi-extensions/lib/acp/backend.ts +16 -6
- package/pi-extensions/lib/acp/context.ts +30 -10
- package/pi-extensions/lib/acp/session-store.ts +2 -2
- package/pi-extensions/lib/callback-env.ts +78 -0
- package/pi-extensions/lib/compaction-send-guard.ts +6 -1
- package/pi-extensions/lib/control-send-receipt.ts +108 -0
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -2
- package/pi-extensions/lib/entwurf-v2-production.ts +19 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +23 -1
- package/pi-extensions/lib/entwurf-v2-send.ts +82 -6
- package/pi-extensions/lib/entwurf-v2-surface.ts +42 -3
- package/pi-extensions/lib/fresh-call-composition.ts +56 -22
- package/pi-extensions/lib/herdr-fresh-call.ts +25 -7
- package/pi-extensions/lib/herdr-placement.ts +24 -4
- package/pi-extensions/lib/mux-fresh-call.ts +23 -8
- package/pi-extensions/meta-bridge-omp.ts +35 -48
- package/protocol.js +0 -8
- package/run.sh +306 -53
- package/scripts/check-acp-backend-preflight.ts +117 -9
- package/scripts/check-acp-carrier-augment.ts +6 -4
- package/scripts/check-acp-event-mapper.ts +17 -6
- package/scripts/check-acp-prompt-builder.ts +17 -9
- package/scripts/check-acp-prompt-lifecycle.ts +23 -6
- package/scripts/check-acp-session-reuse.ts +64 -51
- package/scripts/check-acp-session-store.ts +31 -8
- package/scripts/check-acp-stop-reason.ts +20 -2
- package/scripts/check-acp-stream-hooks.ts +22 -5
- package/scripts/check-acp-usage-accounting.ts +33 -9
- package/scripts/check-entwurf-bridge-boot.ts +16 -1
- package/scripts/check-entwurf-v2-surface.ts +289 -10
- package/scripts/check-fresh-call-dispatch.ts +4 -5
- package/scripts/check-gate-qualification.ts +51 -9
- package/scripts/check-herdr-activation.ts +73 -0
- package/scripts/check-herdr-fresh-call.ts +30 -6
- package/scripts/check-herdr-placement.ts +51 -0
- package/scripts/check-herdr-plugin-build.ts +11 -4
- package/scripts/check-herdr-plugin.ts +745 -38
- package/scripts/check-herdr-sandbox.ts +14 -2
- package/scripts/check-install-container.sh +1 -1
- package/scripts/check-mux-launcher-fence.ts +37 -0
- package/scripts/check-pi-launch.ts +8 -2
- package/scripts/check-release-gate-outcomes.ts +165 -0
- package/scripts/doctor-pi-provider.ts +44 -4
- package/scripts/herdr-rails.mjs +75 -0
- package/scripts/lib/mutation-qualify.ts +1 -1
- package/scripts/meta-bridge-state.py +18 -1
- package/scripts/mutants/acp-backend-preflight.json +20 -0
- package/scripts/mutants/bridge-boot-resume.json +26 -0
- package/scripts/mutants/bridge-command-boot.json +38 -0
- package/scripts/mutants/control-send-receipt.json +235 -0
- package/scripts/mutants/herdr-activation.json +16 -0
- package/scripts/mutants/herdr-fresh-call.json +15 -4
- package/scripts/mutants/herdr-plugin-build.json +7 -11
- package/scripts/mutants/herdr-plugin.json +177 -9
- package/scripts/mutants/meta-retire.json +22 -0
- package/scripts/mutants/mux-fresh-call.json +21 -8
- package/scripts/mutants/omp-fresh.json +5 -9
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/pi-floor.json +17 -0
- package/scripts/mutants/release-gate.json +48 -0
- package/scripts/mutants/retired-sent-surface.json +32 -0
- package/scripts/mutants/v2-surface.json +7 -7
- package/scripts/probe-bridge-command.ts +1 -0
- package/scripts/smoke-acp-session-reuse-live.ts +23 -5
- package/scripts/smoke-agy-install-state.sh +2 -2
- package/scripts/smoke-codex-fresh-live.ts +94 -13
- package/scripts/smoke-copilot-mcp-state.sh +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +71 -36
- package/scripts/smoke-herdr-raw-install-live.sh +90 -5
- package/scripts/smoke-meta-install-state.sh +107 -1
- package/scripts/smoke-mux-lifecycle-live.ts +9 -2
- package/scripts/smoke-omp-fresh-live.ts +4 -2
- package/scripts/smoke-omp-mcp-state.sh +1 -1
- package/scripts/smoke-pi-provider-state.sh +108 -2
- package/scripts/smoke-setup-verdict.sh +18 -2
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
45
|
import { statSync } from "node:fs";
|
|
46
46
|
import path from "node:path";
|
|
47
|
+
import { HERDR_FRESH_CALL_BACKENDS } from "../../scripts/herdr-rails.mjs";
|
|
48
|
+
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
47
49
|
import {
|
|
48
50
|
buildOmpBootstrapPayload,
|
|
49
51
|
composeBackendArgs,
|
|
@@ -54,11 +56,20 @@ import {
|
|
|
54
56
|
TASK_MAX_CHARS,
|
|
55
57
|
} from "./fresh-call-composition.ts";
|
|
56
58
|
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The pilot set, closed. `[#116 decision]` herdr's own `--kind` enum is much larger, and that is
|
|
58
61
|
* NOT evidence of support: every other backend is a pre-mutation named reject here, and there is
|
|
59
62
|
* no fallback to the tmux rail — a caller inside herdr who asks for codex gets a refusal, not a
|
|
60
|
-
* window somewhere else.
|
|
61
|
-
|
|
63
|
+
* window somewhere else.
|
|
64
|
+
*
|
|
65
|
+
* The VALUE now lives in `scripts/herdr-rails.mjs`, the one leaf the status pane and the install
|
|
66
|
+
* narration also read (#116 A7), so a fourth backend cannot appear in prose on one surface while
|
|
67
|
+
* this rail still refuses it. The re-export keeps this module's public name unchanged. The TYPE
|
|
68
|
+
* still has to be a tuple: a `.mjs` leaf infers `readonly string[]` without the JSDoc cast there,
|
|
69
|
+
* and `HerdrFreshCallBackend` would silently widen to `string` while every runtime check still
|
|
70
|
+
* passed. A gate pins that.
|
|
71
|
+
*/
|
|
72
|
+
export { HERDR_FRESH_CALL_BACKENDS };
|
|
62
73
|
export type HerdrFreshCallBackend = (typeof HERDR_FRESH_CALL_BACKENDS)[number];
|
|
63
74
|
|
|
64
75
|
/** Our backend name → the `--kind` token herdr accepts. `[측정 2026-09-14]` both round-tripped with
|
|
@@ -443,7 +454,11 @@ export function herdrAgentNameFromNonce(nonce: string): string {
|
|
|
443
454
|
* not from a screen. Repeating the flag is the grammar; `--env KEY=` injects the EMPTY value
|
|
444
455
|
* rather than dropping the key.
|
|
445
456
|
*/
|
|
446
|
-
export function buildHerdrTabCreateArgs(params: {
|
|
457
|
+
export function buildHerdrTabCreateArgs(params: {
|
|
458
|
+
workspaceId: string;
|
|
459
|
+
cwd?: string;
|
|
460
|
+
callback: { target: string; nonce: string };
|
|
461
|
+
}): string[] {
|
|
447
462
|
return [
|
|
448
463
|
"tab",
|
|
449
464
|
"create",
|
|
@@ -455,6 +470,7 @@ export function buildHerdrTabCreateArgs(params: { workspaceId: string; cwd?: str
|
|
|
455
470
|
"PI_SESSION_ID=",
|
|
456
471
|
"--env",
|
|
457
472
|
"PI_AGENT_ID=",
|
|
473
|
+
...callbackEnvAssignments(params.callback).flatMap((assignment) => ["--env", assignment]),
|
|
458
474
|
];
|
|
459
475
|
}
|
|
460
476
|
|
|
@@ -849,6 +865,7 @@ export async function herdrFreshCall(
|
|
|
849
865
|
buildHerdrTabCreateArgs({
|
|
850
866
|
workspaceId: callerPane.workspaceId,
|
|
851
867
|
...(cwd === undefined ? {} : { cwd }),
|
|
868
|
+
callback: { target: callerGardenId, nonce },
|
|
852
869
|
}),
|
|
853
870
|
);
|
|
854
871
|
if (tabRun.status !== 0) {
|
|
@@ -1205,9 +1222,10 @@ export function renderHerdrFreshCall(result: HerdrFreshCallResult): { text: stri
|
|
|
1205
1222
|
`mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
|
|
1206
1223
|
`coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
|
|
1207
1224
|
`dispatched to them.\n` +
|
|
1208
|
-
`The sibling's garden id arrives separately —
|
|
1209
|
-
`
|
|
1210
|
-
`
|
|
1225
|
+
`The sibling's garden id arrives separately — its first action is the zero-argument callback verb, and ` +
|
|
1226
|
+
`the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
|
|
1227
|
+
`the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
|
|
1228
|
+
`never comes, the pane is visible and can be read directly.`,
|
|
1211
1229
|
isError: false,
|
|
1212
1230
|
};
|
|
1213
1231
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* THE TWO AXES ARE NOT EQUALLY EXACT, AND THE DIFFERENCE IS NAMED. On the claude axis
|
|
22
22
|
* the reported value IS the key, byte for byte. On the pi axis it is a session FILE
|
|
23
23
|
* PATH, and the key is recovered from that filename by a strict conversion measured on
|
|
24
|
-
* pi 0.
|
|
24
|
+
* pi 0.86.0 — a VENDOR FLOOR, not a key equality. Calling both "a unique-key join"
|
|
25
25
|
* would hide which one can drift when a vendor renames a file. The conversion is
|
|
26
26
|
* deliberately strict at both ends so that drift lands as a missed join, never a wrong
|
|
27
27
|
* one, and a listing that declined to read anything says so (see `declinedReports`).
|
|
@@ -112,14 +112,34 @@ const OFFICIAL_REPORTS: Readonly<
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* A pi session filename, measured 2026-09-
|
|
115
|
+
* A pi session filename, measured 2026-09-20 on pi 0.86.0:
|
|
116
116
|
*
|
|
117
|
-
* 2026-09-
|
|
117
|
+
* 2026-09-20T00-35-44-197Z_01a0bc3d-87c4-738e-b391-265c8ba5a1b0.jsonl
|
|
118
118
|
* └────────── start stamp ─────────┘ └──────── nativeSessionId ────────┘
|
|
119
119
|
*
|
|
120
|
+
* `[측정 2026-09-20]` that is a REAL file a sandboxed pi 0.86.0 wrote, and its own
|
|
121
|
+
* `{"type":"session"}` header carries `id: 01a0bc3d-87c4-738e-b391-265c8ba5a1b0` —
|
|
122
|
+
* the exact string `piNativeSessionIdFromPath` recovers from the name. The layout was
|
|
123
|
+
* re-measured rather than carried: pi 0.86.0 rewrote session-manager.ts (+269 −141
|
|
124
|
+
* over v0.85.1), but every line that BUILDS a name is byte-unchanged
|
|
125
|
+
* (`${fileTimestamp}_${this.sessionId}.jsonl`, session-manager.ts:991/1527/1703); the
|
|
126
|
+
* only diff in that file's `.jsonl` lines is a lambda parameter rename on the READER
|
|
127
|
+
* side. A source read alone would not have settled it, so the real file is the receipt.
|
|
128
|
+
*
|
|
129
|
+
* `[측정 2026-09-22, pi 0.87.0]` re-measured, and the shape of the evidence CHANGED for the better.
|
|
130
|
+
* Source: the three lines that BUILD a session name are byte-unchanged from 0.86.0
|
|
131
|
+
* (`packages/coding-agent/src/core/session-manager.ts` `${fileTimestamp}_${sessionId}.jsonl` at
|
|
132
|
+
* :1080/:1666/:1842, moved from :991/:1527/:1703 by the +148/−9 `SessionManager`-canonical work
|
|
133
|
+
* 0.87.0 declares as breaking). File: `scripts/check-herdr-placement.ts` now makes the installed
|
|
134
|
+
* `SessionManager` write a real session file into a temp dir on EVERY run — `create()` plus one
|
|
135
|
+
* `appendMessage`, zero model, zero network, zero child process — and asserts this parser reads
|
|
136
|
+
* that basename back to the id the file's own header declares. So the vendor floor below is no
|
|
137
|
+
* longer a receipt somebody took once; it is re-taken by the gate. The 0.86.0 hand-measured file
|
|
138
|
+
* stays a 0.86.0 fact in the bump ledger rather than being re-dated.
|
|
139
|
+
*
|
|
120
140
|
* WHY THIS IS PINNED HERE AND NOT INFERRED. The uuid is the join key and it lives in
|
|
121
141
|
* a VENDOR filename, so this rule depends on pi's naming and would break silently if
|
|
122
|
-
* pi changed it. **Vendor floor: pi 0.
|
|
142
|
+
* pi changed it. **Vendor floor: pi 0.86.0, measured 2026-09-20** — whether that layout
|
|
123
143
|
* is a vendor contract or a convention is NOT measured, so this is the one place in the
|
|
124
144
|
* axis that can drift under us. It is therefore strict on both ends — the name must end
|
|
125
145
|
* in `.jsonl` and the tail after the last `_` must be a well-formed uuid — and a name
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
* there is no "session created" field because nothing here creates one.
|
|
87
87
|
*/
|
|
88
88
|
|
|
89
|
+
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
89
90
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
90
91
|
import {
|
|
91
92
|
CODEX_CALLER_SEAT_HINT,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
composeFreshCallPrompt,
|
|
112
113
|
FRESH_CALL_BACKENDS,
|
|
113
114
|
FRESH_CALL_CALLBACK_TOOL,
|
|
115
|
+
FRESH_CALL_DELIVERY_TOOL,
|
|
114
116
|
type FreshCallBackend,
|
|
115
117
|
type FreshCallComposition,
|
|
116
118
|
type FreshCallInputRejectReason,
|
|
@@ -158,6 +160,7 @@ export {
|
|
|
158
160
|
buildOmpBootstrapPayload,
|
|
159
161
|
FRESH_CALL_BACKENDS,
|
|
160
162
|
FRESH_CALL_CALLBACK_TOOL,
|
|
163
|
+
FRESH_CALL_DELIVERY_TOOL,
|
|
161
164
|
type FreshCallBackend,
|
|
162
165
|
type FreshCallComposition,
|
|
163
166
|
isSafeFreshCallModel,
|
|
@@ -351,9 +354,11 @@ export type FreshCallResult = { ok: true; receipt: FreshCallReceipt } | { ok: fa
|
|
|
351
354
|
* scrub only on the backend whose measurement surfaced it would encode the claim that the other
|
|
352
355
|
* four are immune, which is false. It costs the legitimate case nothing: a carrier is only ever
|
|
353
356
|
* authoritative when the process that owns it exported it ITSELF, and a fresh `pi` sibling does
|
|
354
|
-
* exactly that after this argv has run. This is a fixed
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
+
* exactly that after this argv has run. This is a fixed seam and deliberately NOT a general env
|
|
358
|
+
* carrier — an arbitrary `-e` passthrough would hand callers the environment-shaping power this
|
|
359
|
+
* rail exists to refuse. Two more `-e` assignments ride beside the scrub: the callback pair
|
|
360
|
+
* (`callback-env.ts`), launcher-computed, so the sibling's first action does not retype an
|
|
361
|
+
* address out of prose.
|
|
357
362
|
*/
|
|
358
363
|
const SCRUBBED_INHERITED_ENV = ["PI_SESSION_ID=", "PI_AGENT_ID="] as const;
|
|
359
364
|
|
|
@@ -371,7 +376,8 @@ export function buildFreshCallArgs(
|
|
|
371
376
|
targetSessionId: string,
|
|
372
377
|
runtimePath: string,
|
|
373
378
|
backendArgs: readonly string[],
|
|
374
|
-
cwd
|
|
379
|
+
cwd: string | undefined,
|
|
380
|
+
callback: { target: string; nonce: string },
|
|
375
381
|
): string[] {
|
|
376
382
|
assertSelector("session", targetSessionId);
|
|
377
383
|
assertLaunchTarget(runtimePath);
|
|
@@ -384,6 +390,7 @@ export function buildFreshCallArgs(
|
|
|
384
390
|
"-d",
|
|
385
391
|
"-a",
|
|
386
392
|
...SCRUBBED_INHERITED_ENV.flatMap((assignment) => ["-e", assignment]),
|
|
393
|
+
...callbackEnvAssignments(callback).flatMap((assignment) => ["-e", assignment]),
|
|
387
394
|
"-t",
|
|
388
395
|
`${targetSessionId}:{end}`,
|
|
389
396
|
...(cwd === undefined ? [] : ["-c", cwd]),
|
|
@@ -569,7 +576,13 @@ export function freshCall(
|
|
|
569
576
|
);
|
|
570
577
|
}
|
|
571
578
|
}
|
|
572
|
-
const run = runTmux(
|
|
579
|
+
const run = runTmux(
|
|
580
|
+
buildFreshCallArgs(targetSessionId, runtimePath, backendArgs, cwd, {
|
|
581
|
+
target: callerGardenId,
|
|
582
|
+
nonce,
|
|
583
|
+
}),
|
|
584
|
+
env,
|
|
585
|
+
);
|
|
573
586
|
assertTmuxOk("new-window", run);
|
|
574
587
|
|
|
575
588
|
let fields: ReturnType<typeof parseWindowFields>;
|
|
@@ -683,9 +696,11 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
|
|
|
683
696
|
`\n` +
|
|
684
697
|
`This is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does ` +
|
|
685
698
|
`NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\n` +
|
|
686
|
-
`The sibling's garden id arrives separately —
|
|
687
|
-
`
|
|
688
|
-
`
|
|
699
|
+
`The sibling's garden id arrives separately — its first action is the zero-argument callback verb ` +
|
|
700
|
+
`(codex is the one arm that still sends the nonce as arguments), and the sender envelope of THAT ` +
|
|
701
|
+
`message is the address. The nonce above is the correlation tag it carries; the sibling reads it ` +
|
|
702
|
+
`from its own launch env, never from this receipt. Nothing is polling for it; if it never comes, ` +
|
|
703
|
+
`the window is visible and can be read directly.`,
|
|
689
704
|
isError: false,
|
|
690
705
|
};
|
|
691
706
|
}
|
|
@@ -83,7 +83,6 @@ import {
|
|
|
83
83
|
type MetaRootBundle,
|
|
84
84
|
ompMetaRootBase,
|
|
85
85
|
ompMetaRoots,
|
|
86
|
-
requireGardenId,
|
|
87
86
|
upsertMetaSession,
|
|
88
87
|
writeMetaSenderMarker,
|
|
89
88
|
} from "./lib/meta-session.ts";
|
|
@@ -162,7 +161,7 @@ export const OMP_BOOTSTRAP_FLAG = "entwurf-bootstrap";
|
|
|
162
161
|
/** Payload grammar version. A payload that does not say exactly this is refused rather than
|
|
163
162
|
* best-guessed: the launcher and this unit ship in the same package, so a mismatch means a
|
|
164
163
|
* STALE installed unit — the one condition `doctor-omp-bridge` exists to name out loud. */
|
|
165
|
-
export const OMP_BOOTSTRAP_VERSION =
|
|
164
|
+
export const OMP_BOOTSTRAP_VERSION = 2;
|
|
166
165
|
|
|
167
166
|
/**
|
|
168
167
|
* The model-facing callback tool, spelled for omp. `[측정]` the vendor's minter sanitises to
|
|
@@ -177,7 +176,7 @@ export const OMP_BOOTSTRAP_VERSION = 1;
|
|
|
177
176
|
* spellings are held equal by `test/omp-fresh-bootstrap.contract.test.ts`, the same shape
|
|
178
177
|
* `check-omp-fresh-preflight` already uses for the preflight's reproduced oracles.
|
|
179
178
|
*/
|
|
180
|
-
export const OMP_BOOTSTRAP_CALLBACK_TOOL = "
|
|
179
|
+
export const OMP_BOOTSTRAP_CALLBACK_TOOL = "mcp__entwurf_bridge_entwurf_callback";
|
|
181
180
|
|
|
182
181
|
/** Task ceiling, held equal to the launcher's `TASK_MAX_CHARS` by the same contract test. */
|
|
183
182
|
export const OMP_BOOTSTRAP_TASK_MAX_CHARS = 16000;
|
|
@@ -195,15 +194,9 @@ export const OMP_BOOTSTRAP_READY_TIMEOUT_MS = 60_000;
|
|
|
195
194
|
* primitive; a FIXED DELAY is forbidden because the gap is a race, not a constant. */
|
|
196
195
|
export const OMP_BOOTSTRAP_POLL_MS = 100;
|
|
197
196
|
|
|
198
|
-
/**
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
/** What the launcher put on the flag. Exactly three fields — a target to call back to, the
|
|
202
|
-
* correlation nonce, and the operator's task. Nothing here is a command, a path or an env
|
|
203
|
-
* name, and the decoder refuses anything wider. */
|
|
197
|
+
/** What the launcher put on the flag. Address rides process env; this payload is the task
|
|
198
|
+
* released after the no-arg callback succeeds. The decoder refuses anything wider. */
|
|
204
199
|
export interface OmpBootstrapPayload {
|
|
205
|
-
target: string;
|
|
206
|
-
nonce: string;
|
|
207
200
|
task: string;
|
|
208
201
|
}
|
|
209
202
|
|
|
@@ -216,8 +209,6 @@ export type OmpBootstrapRejectReason =
|
|
|
216
209
|
| "payload-not-object"
|
|
217
210
|
| "version-unsupported"
|
|
218
211
|
| "payload-unknown-key"
|
|
219
|
-
| "target-invalid"
|
|
220
|
-
| "nonce-invalid"
|
|
221
212
|
| "task-empty"
|
|
222
213
|
| "task-too-long";
|
|
223
214
|
|
|
@@ -228,7 +219,7 @@ export type OmpBootstrapDecode =
|
|
|
228
219
|
/** The wire keys, closed. An unknown key is a REFUSAL rather than an ignored extra: this
|
|
229
220
|
* flag is the one thing a caller can put arbitrary bytes into, so the decoder's job is to
|
|
230
221
|
* make "what the launcher meant" and "what this unit will act on" the same set. */
|
|
231
|
-
const BOOTSTRAP_KEYS = new Set(["v", "
|
|
222
|
+
const BOOTSTRAP_KEYS = new Set(["v", "task"]);
|
|
232
223
|
|
|
233
224
|
/**
|
|
234
225
|
* Read the flag value into a payload, or name why not.
|
|
@@ -256,18 +247,10 @@ export function decodeOmpBootstrapPayload(raw: unknown): OmpBootstrapDecode {
|
|
|
256
247
|
for (const key of Object.keys(obj)) {
|
|
257
248
|
if (!BOOTSTRAP_KEYS.has(key)) return { ok: false, reason: "payload-unknown-key" };
|
|
258
249
|
}
|
|
259
|
-
let target: string;
|
|
260
|
-
try {
|
|
261
|
-
target = requireGardenId(obj.target);
|
|
262
|
-
} catch {
|
|
263
|
-
return { ok: false, reason: "target-invalid" };
|
|
264
|
-
}
|
|
265
|
-
const nonce = obj.nonce;
|
|
266
|
-
if (typeof nonce !== "string" || !BOOTSTRAP_NONCE_RE.test(nonce)) return { ok: false, reason: "nonce-invalid" };
|
|
267
250
|
const task = obj.task;
|
|
268
251
|
if (typeof task !== "string" || task.trim().length === 0) return { ok: false, reason: "task-empty" };
|
|
269
252
|
if (task.length > OMP_BOOTSTRAP_TASK_MAX_CHARS) return { ok: false, reason: "task-too-long" };
|
|
270
|
-
return { ok: true, value: {
|
|
253
|
+
return { ok: true, value: { task } };
|
|
271
254
|
}
|
|
272
255
|
|
|
273
256
|
/**
|
|
@@ -279,18 +262,13 @@ export function decodeOmpBootstrapPayload(raw: unknown): OmpBootstrapDecode {
|
|
|
279
262
|
* no competing goal at all. Stage two exists so this message never has to compete with the
|
|
280
263
|
* work; adding "then do X" back into it would rebuild exactly the prompt that failed.
|
|
281
264
|
*/
|
|
282
|
-
export function buildOmpCallbackOnlyPrompt(
|
|
265
|
+
export function buildOmpCallbackOnlyPrompt(): string {
|
|
283
266
|
return [
|
|
284
267
|
"You are a fresh visible citizen that entwurf opened in the operator's tmux session.",
|
|
285
268
|
"",
|
|
286
|
-
`FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with
|
|
287
|
-
`target=${params.target}, intent=fire-and-forget, wants_reply=false, and ` +
|
|
288
|
-
`message set to exactly ${params.nonce} — that string alone, nothing added.`,
|
|
269
|
+
`FIRST AND ONLY ACTION RIGHT NOW: call ${OMP_BOOTSTRAP_CALLBACK_TOOL} with no arguments.`,
|
|
289
270
|
"That call is how the agent that opened you learns your address. Make the call before",
|
|
290
|
-
"reading files, before planning, and before answering in prose.
|
|
291
|
-
"",
|
|
292
|
-
"Do not inspect environment variables, do not call entwurf_self, and do not start an MCP",
|
|
293
|
-
"server yourself. Your own report of your identity is not the address anyone needs.",
|
|
271
|
+
"reading files, before planning, and before answering in prose. There are no parameters.",
|
|
294
272
|
"",
|
|
295
273
|
"Your actual task arrives as the NEXT user message, immediately after that call succeeds.",
|
|
296
274
|
"Do not ask for it and do not guess at it.",
|
|
@@ -756,20 +734,29 @@ export function ompCallbackToolReady(pi: OmpExtensionApi): boolean {
|
|
|
756
734
|
}
|
|
757
735
|
|
|
758
736
|
/**
|
|
759
|
-
* Does this tool event name OUR callback
|
|
737
|
+
* Does this tool event name OUR callback?
|
|
760
738
|
*
|
|
761
739
|
* The event carries no server name, no session id and no original MCP tool name
|
|
762
|
-
* (`extensions/types.ts:916-1017`), so the match is built from what IS there:
|
|
763
|
-
* minted tool name
|
|
764
|
-
*
|
|
765
|
-
*
|
|
740
|
+
* (`extensions/types.ts:916-1017`), so the match is built from what IS there: exact string
|
|
741
|
+
* equality on the canonical minted tool name — a prefix or `includes` here would let a
|
|
742
|
+
* neighbouring tool release this session's task.
|
|
743
|
+
*
|
|
744
|
+
* The target/nonce half of this predicate is GONE, and that is the point of the out-of-band
|
|
745
|
+
* callback: the address rides the sibling's process env, so the verb takes no arguments and
|
|
746
|
+
* there are no argument values left to compare. What replaces the comparison is the ABSENCE
|
|
747
|
+
* of arguments — a call carrying a target or a message is a model supplying an address, which
|
|
748
|
+
* is the second address axis Hard Rule 2 refuses, and it must not release the task. Correlation
|
|
749
|
+
* to THIS bootstrap is not weakened by the removal: it is carried by the stored `toolCallId`
|
|
750
|
+
* (one session, one live tool call) plus the fact that only this session's env named the caller.
|
|
766
751
|
*/
|
|
767
|
-
function matchesCallback(event: OmpToolCallEvent,
|
|
752
|
+
function matchesCallback(event: OmpToolCallEvent, _payload: OmpBootstrapPayload): boolean {
|
|
768
753
|
if (event?.toolName !== OMP_BOOTSTRAP_CALLBACK_TOOL) return false;
|
|
769
754
|
const input = event?.input;
|
|
770
|
-
if (
|
|
755
|
+
if (input === undefined || input === null) return true;
|
|
756
|
+
if (typeof input !== "object" || Array.isArray(input)) return false;
|
|
771
757
|
const args = input as Record<string, unknown>;
|
|
772
|
-
|
|
758
|
+
// No-arg verb: a supplied target/message is a second address axis — not our callback.
|
|
759
|
+
return args.target === undefined && args.message === undefined;
|
|
773
760
|
}
|
|
774
761
|
|
|
775
762
|
/**
|
|
@@ -822,7 +809,7 @@ export function createOmpBootstrap(opts: {
|
|
|
822
809
|
stopTimer();
|
|
823
810
|
if (isFinal()) return;
|
|
824
811
|
phase = "failed";
|
|
825
|
-
log("WARN", `bootstrap-failed
|
|
812
|
+
log("WARN", `bootstrap-failed: ${why}; the task was NOT sent`);
|
|
826
813
|
}
|
|
827
814
|
|
|
828
815
|
/**
|
|
@@ -858,8 +845,8 @@ export function createOmpBootstrap(opts: {
|
|
|
858
845
|
// two callback prompts for one bootstrap.
|
|
859
846
|
phase = "callback-sent";
|
|
860
847
|
stopTimer();
|
|
861
|
-
log("INFO",
|
|
862
|
-
send(buildOmpCallbackOnlyPrompt(
|
|
848
|
+
log("INFO", "bootstrap-ready: callback tool live, sending callback-only prompt");
|
|
849
|
+
send(buildOmpCallbackOnlyPrompt());
|
|
863
850
|
return;
|
|
864
851
|
}
|
|
865
852
|
if (timers.now() >= deadline) {
|
|
@@ -895,14 +882,14 @@ export function createOmpBootstrap(opts: {
|
|
|
895
882
|
const id = event?.toolCallId;
|
|
896
883
|
if (typeof id !== "string" || id.length === 0) return;
|
|
897
884
|
callId = id;
|
|
898
|
-
log("INFO", `bootstrap-callback-observed
|
|
885
|
+
log("INFO", `bootstrap-callback-observed toolCallId=${id}`);
|
|
899
886
|
},
|
|
900
887
|
onToolResult(event: OmpToolResultEvent): void {
|
|
901
888
|
if (phase !== "callback-sent" || callId === null) return;
|
|
902
889
|
if (event?.toolCallId !== callId) return;
|
|
903
890
|
if (!matchesCallback(event, payload)) return;
|
|
904
891
|
if (event?.isError !== false) {
|
|
905
|
-
log("WARN", `bootstrap-callback-errored
|
|
892
|
+
log("WARN", `bootstrap-callback-errored toolCallId=${callId}; the task was NOT sent`);
|
|
906
893
|
return;
|
|
907
894
|
}
|
|
908
895
|
// RELEASED, AND THE TASK IS NOT SENT HERE. This handler runs inside the callback
|
|
@@ -911,7 +898,7 @@ export function createOmpBootstrap(opts: {
|
|
|
911
898
|
// record that the callback provably succeeded; `onTurnEnd` owns the send.
|
|
912
899
|
phase = "released";
|
|
913
900
|
stopTimer();
|
|
914
|
-
log("INFO", `bootstrap-released
|
|
901
|
+
log("INFO", `bootstrap-released toolCallId=${callId}: task armed for the next turn_end`);
|
|
915
902
|
},
|
|
916
903
|
/**
|
|
917
904
|
* The stage-two boundary: the callback turn is over, so the session is at the edge
|
|
@@ -932,7 +919,7 @@ export function createOmpBootstrap(opts: {
|
|
|
932
919
|
onTurnEnd(): void {
|
|
933
920
|
if (phase !== "released") return;
|
|
934
921
|
phase = "task-sent";
|
|
935
|
-
log("INFO",
|
|
922
|
+
log("INFO", "bootstrap-task-sent: delivering the task at the turn_end boundary");
|
|
936
923
|
send(payload.task);
|
|
937
924
|
},
|
|
938
925
|
invalidate(why: string): void {
|
|
@@ -942,7 +929,7 @@ export function createOmpBootstrap(opts: {
|
|
|
942
929
|
// caller's task must die with the session it was addressed to.
|
|
943
930
|
if (isFinal()) return;
|
|
944
931
|
phase = "failed";
|
|
945
|
-
log("INFO", `bootstrap-invalidated
|
|
932
|
+
log("INFO", `bootstrap-invalidated: ${why}; the task was NOT sent`);
|
|
946
933
|
},
|
|
947
934
|
};
|
|
948
935
|
}
|
|
@@ -1118,7 +1105,7 @@ export function startOmpBootstrap(opts: {
|
|
|
1118
1105
|
const handle = createOmpBootstrap({ payload: decoded.value, pi, timers, log });
|
|
1119
1106
|
activeBootstrap = { sessionId: envelope.nativeSessionId, handle };
|
|
1120
1107
|
bootstrapConsumed = true;
|
|
1121
|
-
log("INFO",
|
|
1108
|
+
log("INFO", "bootstrap-armed: payload admitted, waiting for callback tool");
|
|
1122
1109
|
handle.start();
|
|
1123
1110
|
}
|
|
1124
1111
|
|
package/protocol.js
CHANGED
|
@@ -21,11 +21,3 @@
|
|
|
21
21
|
* AGENTS.md section from its own pi-context augment.
|
|
22
22
|
*/
|
|
23
23
|
export const ENTWURF_PROJECT_CONTEXT_OPEN_TAG = "<project-context";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Sender-side UI echo custom message type for successful entwurf_v2 send calls.
|
|
27
|
-
* The bridge emits it, the control renderer draws it, and the provider-level
|
|
28
|
-
* context filter removes it before LLM conversion. Keep as one SSOT: a typo
|
|
29
|
-
* here otherwise becomes a silent UI/context split.
|
|
30
|
-
*/
|
|
31
|
-
export const ENTWURF_SENT_MESSAGE_TYPE = "entwurf-sent";
|