@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
|
@@ -83,6 +83,16 @@ import { joinKeyOf, parseHerdrPaneList } from "../pi-extensions/lib/herdr-placem
|
|
|
83
83
|
import { assessLauncherCleanup, snapshotClaudeLauncher, verifyClaudeLauncher } from "./lib/claude-launcher-fence.ts";
|
|
84
84
|
import { skipLive } from "./lib/live-skip.ts";
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* #120 P2: the control-socket rail answers with the ACCEPTANCE BOUNDARY the receiver observed, and
|
|
88
|
+
* every member of that set is a success. The caller here is mid-turn while its child calls back,
|
|
89
|
+
* so `queued-steer` is the ORDINARY answer — the old literal `→ sent` pin would have gone red on
|
|
90
|
+
* the common case and passed only when the caller happened to be idle. What must still be absent
|
|
91
|
+
* is a REFUSAL, which the negative beside it now names directly (`rejected` / `failed`) instead of
|
|
92
|
+
* as "anything that is not the word sent".
|
|
93
|
+
*/
|
|
94
|
+
const CONTROL_ACCEPTED = /entwurf_v2 control-socket → (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/;
|
|
95
|
+
|
|
86
96
|
const LABEL = "smoke-herdr-fresh-call-live";
|
|
87
97
|
const REPO_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
88
98
|
const REAL_HOME = process.env.HOME ?? os.homedir();
|
|
@@ -186,17 +196,38 @@ function transcriptRecords(file: string): string[] {
|
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
/**
|
|
189
|
-
* The
|
|
190
|
-
*
|
|
199
|
+
* The BARE verb names, as the two surfaces this smoke reads actually spell them — NOT the
|
|
200
|
+
* model-facing dialect. `[측정 2026-09-20, this smoke's own red fixture]` the claude MCP activity
|
|
201
|
+
* log carries `Calling MCP tool: entwurf_callback` / `Tool 'entwurf_callback' completed`, and pi
|
|
202
|
+
* writes `"type":"toolCall","name":"entwurf_callback"`; neither records
|
|
203
|
+
* `mcp__entwurf-bridge__entwurf_callback`. The per-host dialect is owned by
|
|
204
|
+
* `FRESH_CALL_CALLBACK_TOOL` / `FRESH_CALL_DELIVERY_TOOL` in `fresh-call-composition.ts` and is
|
|
205
|
+
* what the FRAMING says; these two are what an OBSERVER sees, and conflating them is what this
|
|
206
|
+
* pair of constants exists to stop.
|
|
207
|
+
*/
|
|
208
|
+
const CALLBACK_VERB = "entwurf_callback";
|
|
209
|
+
const DELIVERY_VERB = "entwurf_v2";
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The id of the CALLBACK toolCall in this record, or null. Pi writes a call and its result as two
|
|
213
|
+
* records; this is one half of the join that replaces a same-record read.
|
|
214
|
+
*
|
|
215
|
+
* WHAT MAKES IT THE CALLBACK `[ff09522]`. It used to be the ARGUMENTS: message === nonce, target
|
|
216
|
+
* === the caller, intent === fire-and-forget. That predicate is gone because the thing it read is
|
|
217
|
+
* gone — the birth verb takes ZERO arguments and reads the caller and the nonce out of its own
|
|
218
|
+
* process env, so there is nothing in the call for a model to get wrong and nothing here to
|
|
219
|
+
* compare. What identifies the callback now is the verb plus the ABSENCE of an address: a call
|
|
220
|
+
* carrying a target or a message is a model supplying an address, which is the second address axis
|
|
221
|
+
* Hard Rule 2 refuses, and it is not this act.
|
|
191
222
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* delivery. The arguments are an object, so they are read as one: the message must BE the nonce,
|
|
197
|
-
* the target must be the caller that minted it, and the intent must be the one the framing names.
|
|
223
|
+
* The nonce↔sender correlation the old predicate carried did not disappear with it — it moved to
|
|
224
|
+
* the DELIVERED ARTIFACT, which this smoke checks independently and earlier ("the child's callback
|
|
225
|
+
* ARRIVED … its body is a production nonce and its sender is the child the direct witness resolved
|
|
226
|
+
* to"). Two records, one event, same as before; only the half that names the call has changed.
|
|
198
227
|
*/
|
|
199
228
|
function entwurfCallIdFor(record: string, nonce: string, callerGid: string): string | null {
|
|
229
|
+
void nonce;
|
|
230
|
+
void callerGid;
|
|
200
231
|
let parsed: unknown;
|
|
201
232
|
try {
|
|
202
233
|
parsed = JSON.parse(record);
|
|
@@ -207,16 +238,14 @@ function entwurfCallIdFor(record: string, nonce: string, callerGid: string): str
|
|
|
207
238
|
if (!Array.isArray(content)) return null;
|
|
208
239
|
for (const part of content) {
|
|
209
240
|
const call = part as { type?: unknown; name?: unknown; id?: unknown; arguments?: unknown };
|
|
210
|
-
if (call.type !== "toolCall" || call.name !==
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// The framing asks for no reply; a call that asked for one is a different act.
|
|
219
|
-
if (args.wants_reply === true) continue;
|
|
241
|
+
if (call.type !== "toolCall" || call.name !== CALLBACK_VERB) continue;
|
|
242
|
+
// Zero-argument verb: absent, null and `{}` are all the shape it is called with. A supplied
|
|
243
|
+
// target or message is a model naming an address the env already owns — not this act.
|
|
244
|
+
const args = call.arguments as { message?: unknown; target?: unknown } | undefined | null;
|
|
245
|
+
if (args !== undefined && args !== null) {
|
|
246
|
+
if (typeof args !== "object" || Array.isArray(args)) continue;
|
|
247
|
+
if (args.target !== undefined || args.message !== undefined) continue;
|
|
248
|
+
}
|
|
220
249
|
return typeof call.id === "string" ? call.id : null;
|
|
221
250
|
}
|
|
222
251
|
return null;
|
|
@@ -830,10 +859,7 @@ async function main(): Promise<void> {
|
|
|
830
859
|
callId === null
|
|
831
860
|
? -1
|
|
832
861
|
: childRecords.findIndex(
|
|
833
|
-
(record, i) =>
|
|
834
|
-
i > callAt &&
|
|
835
|
-
record.includes("entwurf_v2 control-socket → sent") &&
|
|
836
|
-
toolResultIdOf(record) === callId,
|
|
862
|
+
(record, i) => i > callAt && CONTROL_ACCEPTED.test(record) && toolResultIdOf(record) === callId,
|
|
837
863
|
);
|
|
838
864
|
// The LAST mention of the task token, so the birth prompt — which carries it, first —
|
|
839
865
|
// cannot be what satisfies "the work came after".
|
|
@@ -845,14 +871,14 @@ async function main(): Promise<void> {
|
|
|
845
871
|
}
|
|
846
872
|
}
|
|
847
873
|
ok(
|
|
848
|
-
`${cell.label}: the child decoded the one-line birth argv and ran the task only AFTER its callback — the
|
|
874
|
+
`${cell.label}: the child decoded the one-line birth argv and ran the task only AFTER its callback — the ACCEPTED outcome belongs, by toolCallId, to the very call that carried the delivered nonce, and the task token appears in a record after it`,
|
|
849
875
|
nonce.length > 0 && callAt >= 0 && sentAt > callAt && workedAt > sentAt,
|
|
850
876
|
);
|
|
851
877
|
ok(
|
|
852
|
-
`${cell.label}: the child's OWN tool result says the callback was
|
|
853
|
-
|
|
878
|
+
`${cell.label}: the child's OWN tool result says the callback was ACCEPTED on the rail its caller answers on — one of \`sent|queued-steer|queued-follow-up|accepted-unknown-boundary\` — not a timeout, not a reject, not a dirty lock`,
|
|
879
|
+
CONTROL_ACCEPTED.test(childText) &&
|
|
854
880
|
!/entwurf_v2 [a-z-]+ (?:execution failed:|DELIVERED \()/.test(childText) &&
|
|
855
|
-
!/entwurf_v2 control-socket → (
|
|
881
|
+
!/entwurf_v2 control-socket → (?:rejected|failed)/.test(childText),
|
|
856
882
|
);
|
|
857
883
|
} else {
|
|
858
884
|
const childActivity = mcpActivityBySession(root).get(childNative) ?? [];
|
|
@@ -864,23 +890,27 @@ async function main(): Promise<void> {
|
|
|
864
890
|
// OFFERS exactly that corroboration ("you can corroborate the caller first if you
|
|
865
891
|
// want to"), so the old oracle contradicted our own prompt and would have forbidden
|
|
866
892
|
// the behaviour we asked for. What still must hold is everything the claim was
|
|
867
|
-
// actually about — the callback lands before any work, it completes, and
|
|
868
|
-
//
|
|
893
|
+
// actually about — the callback lands before any work, it completes, and neither the
|
|
894
|
+
// callback verb nor the delivery verb failed or timed out anywhere in the log.
|
|
895
|
+
// `[ff09522]` the verb this joins on MOVED: the birth callback is `entwurf_callback`
|
|
896
|
+
// and `entwurf_v2` is now only the DELIVERY the closing line asks for. Joining on the
|
|
897
|
+
// old name does not merely miss — it mis-attributes, finding the result-delivery call
|
|
898
|
+
// and then reporting the real callback as forbidden work before it.
|
|
869
899
|
// ONLY what the framing actually offers. `entwurf_self` used to sit in this set and
|
|
870
900
|
// nothing ever proposed it to the child — an allowance for a tool we do not mention
|
|
871
901
|
// widens the oracle without widening the contract (sol D1, 2026-09-18).
|
|
872
902
|
const READ_ONLY_FIRST = new Set(["entwurf_peers"]);
|
|
873
|
-
const beforeCallback = order.slice(0, Math.max(order.indexOf(
|
|
903
|
+
const beforeCallback = order.slice(0, Math.max(order.indexOf(CALLBACK_VERB), 0));
|
|
874
904
|
// THE JOIN THIS AXIS CAN ACTUALLY MAKE. The claude MCP log records WHICH tool was
|
|
875
905
|
// called and whether it completed — never its arguments or its result body — so
|
|
876
|
-
// "the first
|
|
906
|
+
// "the first callback completed" alone would also be true of a call that delivered
|
|
877
907
|
// somebody else's nonce or came back as a semantic reject over a successful
|
|
878
908
|
// transport. The second artifact closes it: the caller's own delivered message
|
|
879
909
|
// carries the EXACT nonce and the child as its sender, and its enqueue timestamp has
|
|
880
910
|
// to fall inside the window of that first call. Two independent records, one event.
|
|
881
|
-
const callAt = indexOfEntry(childActivity, (e) => (e.debug ?? "") ===
|
|
911
|
+
const callAt = indexOfEntry(childActivity, (e) => (e.debug ?? "") === `Calling MCP tool: ${CALLBACK_VERB}`);
|
|
882
912
|
const doneAt = indexOfEntry(childActivity, (e) =>
|
|
883
|
-
(e.debug ?? "").startsWith(
|
|
913
|
+
(e.debug ?? "").startsWith(`Tool '${CALLBACK_VERB}' completed successfully`),
|
|
884
914
|
);
|
|
885
915
|
const callbackStamp = deliveredMessages(String(fenced.ENTWURF_META_MAILBOX_DIR), callerGid)[0]?.file ?? "";
|
|
886
916
|
const stampedAt = Date.parse(
|
|
@@ -902,14 +932,19 @@ async function main(): Promise<void> {
|
|
|
902
932
|
// here, and a claim about "any work" is wider than the oracle. What IS observed, and
|
|
903
933
|
// is the thing the framing actually asks for, is the ORDER OF TOOLS: the first
|
|
904
934
|
// non-read-only tool this child called was the callback.
|
|
905
|
-
`${cell.label}: the child's FIRST non-read-only tool call was the
|
|
935
|
+
`${cell.label}: the child's FIRST non-read-only tool call was the zero-argument ${CALLBACK_VERB} — it completed successfully and the delivered callback carrying this exact nonce was enqueued inside that call's own window, preceded only by the read-only corroboration the framing offers, with neither ${CALLBACK_VERB} nor ${DELIVERY_VERB} failing or timing out anywhere in its log (this cell reads tool activity only; assistant text is not observable on this rail)`,
|
|
906
936
|
childActivity.length > 0 &&
|
|
907
|
-
order.includes(
|
|
937
|
+
order.includes(CALLBACK_VERB) &&
|
|
908
938
|
beforeCallback.every((name) => READ_ONLY_FIRST.has(name)) &&
|
|
909
939
|
within &&
|
|
910
940
|
callbackArrived &&
|
|
911
|
-
|
|
912
|
-
|
|
941
|
+
// BOTH verbs, because the child uses both: the birth callback and, after the
|
|
942
|
+
// work, the delivery verb the framing's closing line names. A failed delivery
|
|
943
|
+
// is as fatal to this rail as a failed callback.
|
|
944
|
+
![CALLBACK_VERB, DELIVERY_VERB].some((verb) =>
|
|
945
|
+
childActivity.some(
|
|
946
|
+
(e) => (e.debug ?? "").startsWith(`Tool '${verb}' failed`) || (e.error ?? "").includes(verb),
|
|
947
|
+
),
|
|
913
948
|
),
|
|
914
949
|
);
|
|
915
950
|
}
|
|
@@ -490,7 +490,7 @@ for (const name of ["entwurf", "entwurf-bridge", "entwurf-statusline"]) {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// The REAL subcommand, from the installed bin, under node_modules — which is the branch of
|
|
493
|
-
// start.sh that runs the prebuilt dist. Its own oracle is an EXACT set (run.sh
|
|
493
|
+
// start.sh that runs the prebuilt dist. Its own oracle is an EXACT set (`EXPECT_TOOLS` in run.sh check-bridge), so a
|
|
494
494
|
// zero exit here is the artifact listing all seven garden verbs and no eighth.
|
|
495
495
|
const run = spawnSync(path.join(binDir, "entwurf"), ["check-bridge"], { encoding: "utf8" });
|
|
496
496
|
const said = `${run.stdout ?? ""}${run.stderr ?? ""}`;
|
|
@@ -507,6 +507,7 @@ const verbs = (said.match(/entwurf_[a-z0-9_]+/g) ?? []).filter((v, i, a) => a.in
|
|
|
507
507
|
// one; the two disagreeing is the signal, and a verb genuinely added upstream reddens this
|
|
508
508
|
// smoke until somebody says so here too.
|
|
509
509
|
const EXPECTED_VERBS = [
|
|
510
|
+
"entwurf_callback",
|
|
510
511
|
"entwurf_fresh_call",
|
|
511
512
|
"entwurf_inbox_read",
|
|
512
513
|
"entwurf_peers",
|
|
@@ -519,7 +520,7 @@ console.log(` VERBS ${verbs.join(",") || "<none>"}`);
|
|
|
519
520
|
if (run.status === 0) ok(`entwurf check-bridge exit 0 from the installed bin (${verbs.length} verbs listed)`);
|
|
520
521
|
else no(`entwurf check-bridge exit ${run.status}: ${said.trim().slice(0, 400)}`);
|
|
521
522
|
JSON.stringify(verbs) === JSON.stringify(EXPECTED_VERBS)
|
|
522
|
-
? ok(`the receipt names EXACTLY the
|
|
523
|
+
? ok(`the receipt names EXACTLY the eight garden verbs (${EXPECTED_VERBS.length}), parsed from what the installed bin answered`)
|
|
523
524
|
: no(`verb set mismatch — want ${JSON.stringify(EXPECTED_VERBS)} got ${JSON.stringify(verbs)}`);
|
|
524
525
|
process.exit(bad);
|
|
525
526
|
' "$ACTIVE" "$REGISTRY" "$PLUGIN_ID" || fail=1
|
|
@@ -553,13 +554,19 @@ const HOME = process.env.HOME;
|
|
|
553
554
|
const SOCKET_DIR = path.join(HOME, ".pi", "entwurf-control");
|
|
554
555
|
const META_DIR = path.join(HOME, ".pi", "agent", "meta-sessions");
|
|
555
556
|
const ENTWURF_BIN = process.argv[2];
|
|
557
|
+
// The ENTRY POINT is an argument, because there are two of them and they are different
|
|
558
|
+
// claims. `pi --entwurf-control` proves the WIRING; `entwurf pi` proves the LAUNCHER a user
|
|
559
|
+
// received from npm. One program, so neither claim drifts away from the other.
|
|
560
|
+
const LABEL = process.argv[3];
|
|
561
|
+
const [COMMAND, ...COMMAND_ARGS] = process.argv.slice(4);
|
|
556
562
|
|
|
557
563
|
let bad = 0;
|
|
558
564
|
const ok = (m) => console.log(" ok " + m);
|
|
559
565
|
const no = (m) => { console.log(" FAIL " + m); bad = 1; };
|
|
560
566
|
const ls = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
|
|
561
567
|
|
|
562
|
-
|
|
568
|
+
console.log(` ENTRY ${LABEL}: ${COMMAND} ${COMMAND_ARGS.join(" ")}`);
|
|
569
|
+
const child = spawn(COMMAND, COMMAND_ARGS, { stdio: ["pipe", "pipe", "pipe"] });
|
|
563
570
|
let stderr = "";
|
|
564
571
|
child.stderr.on("data", (d) => { stderr += d.toString(); });
|
|
565
572
|
|
|
@@ -655,14 +662,14 @@ child.stdout.on("data", (d) => {
|
|
|
655
662
|
child.on("error", (err) => { no(`pi failed to spawn: ${err.message}`); done(1); });
|
|
656
663
|
child.on("exit", (code) => {
|
|
657
664
|
if (settled) return;
|
|
658
|
-
no(
|
|
665
|
+
no(`${LABEL}: the session exited ${code} before answering get_state — stderr: ${stderr.trim().slice(0, 600)}`);
|
|
659
666
|
done(1);
|
|
660
667
|
});
|
|
661
668
|
|
|
662
669
|
setTimeout(() => { child.stdin.write(`${JSON.stringify({ type: "get_state", id: "g1" })}\n`); }, 500);
|
|
663
670
|
setTimeout(() => { if (!settled) { no("pi did not answer get_state within 60s"); done(1); } }, 60_000);
|
|
664
671
|
DRIVER_EOF
|
|
665
|
-
node /tmp/use-path-drive.mjs "$ACTIVE/node_modules/.bin/entwurf" || fail=1
|
|
672
|
+
node /tmp/use-path-drive.mjs "$ACTIVE/node_modules/.bin/entwurf" "the plugin wiring" pi --entwurf-control --mode rpc || fail=1
|
|
666
673
|
|
|
667
674
|
# ── 8. the status fan, driven by the REAL binaries this install produced ─────
|
|
668
675
|
# `check-herdr-plugin` already drives `lib/status.mjs` against stub `entwurf` and `herdr`
|
|
@@ -734,8 +741,86 @@ if printf '%s' "$fan_with_server" | grep -qx '1 citizen(s) not shown: nobody cou
|
|
|
734
741
|
else
|
|
735
742
|
bad "the fan drew no '1 citizen(s) not shown ... (unobserved)' accounting for the citizen born in [7]"
|
|
736
743
|
fi
|
|
744
|
+
|
|
745
|
+
# ── 8b. the SAME fan with the ENTWURF_BIN crutch REMOVED (#116 A-D3=C) ──────
|
|
746
|
+
# The two runs above hand the pane `ENTWURF_BIN`, which is exactly the operator crutch D3=C exists
|
|
747
|
+
# to delete: with it, this cell stays green whether or not the ledger fallback exists, so it can
|
|
748
|
+
# certify nothing about the first screen a real user sees. `plugins/herdr/README.md` says the plugin
|
|
749
|
+
# puts NOTHING on PATH, so pathless is the state a correct install actually leaves behind.
|
|
750
|
+
# The herdr server from [8] is still up — the fan's second read needs it, and [9] stops it below.
|
|
751
|
+
PATHLESS="$(printf '%s' "$PATH" | tr ':' '\n' | while IFS= read -r d; do [ -n "$d" ] && [ -x "$d/entwurf" ] || printf '%s\n' "$d"; done | paste -sd:)"
|
|
752
|
+
run_fan_pathless() {
|
|
753
|
+
env -u ENTWURF_BIN PATH="$PATHLESS" HERDR_BIN_PATH="$(command -v herdr)" node "$FAN" </dev/null 2>&1
|
|
754
|
+
}
|
|
755
|
+
if command -v entwurf >/dev/null 2>&1 && PATH="$PATHLESS" command -v entwurf >/dev/null 2>&1; then
|
|
756
|
+
bad "the pathless PATH still resolves an entwurf — this cell would prove nothing"
|
|
757
|
+
else
|
|
758
|
+
ok "the pathless PATH resolves no entwurf (the crutch is really gone, not merely unset)"
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
fan_pathless="$(run_fan_pathless)"; fan_pathless_rc=$?
|
|
762
|
+
echo "$fan_pathless" | sed 's/^/ /'
|
|
763
|
+
echo " → exit $fan_pathless_rc"
|
|
764
|
+
# The IDENTICAL literal the crutched run asserts at the cell above, so the two runs are comparable
|
|
765
|
+
# rather than two different claims.
|
|
766
|
+
if [ "$fan_pathless_rc" -eq 0 ] && printf '%s' "$fan_pathless" | grep -qx '1 citizen(s) not shown: nobody could observe placement for them (unobserved)\.'; then
|
|
767
|
+
ok "[QK:HRIL-FAN-LEDGER-FALLBACK] with NO ENTWURF_BIN and no entwurf on PATH, the certified activation ledger supplies the runtime and the citizen read completes — the same accounting sentence, from a binary nobody put on PATH"
|
|
768
|
+
else
|
|
769
|
+
bad "the ledger-fallback claim failed: pathless fan rc=$fan_pathless_rc did not reproduce the crutched run's citizen accounting"
|
|
770
|
+
fi
|
|
771
|
+
if printf '%s' "$fan_pathless" | grep -q 'via activation-ledger'; then
|
|
772
|
+
ok "the pathless run says WHICH source supplied the binary, so the fallback cannot be mistaken for a PATH hit"
|
|
773
|
+
else
|
|
774
|
+
bad "the pathless run did not name activation-ledger as the binary's source"
|
|
775
|
+
fi
|
|
776
|
+
|
|
777
|
+
# ── 8c. pathless AND no ledger: the CITIZEN axis alone skips, and it is not red ──
|
|
778
|
+
# The other half of the same contract. A host that never activated must not read as broken, and the
|
|
779
|
+
# three blocks that need no entwurf binary must still answer.
|
|
780
|
+
# XDG_STATE_HOME is redirected TOO, and that is the whole isolation: the ledger lives under XDG
|
|
781
|
+
# **state** ($LEDGER above) while the runtime lives under XDG **data**. Moving only the second would
|
|
782
|
+
# leave this cell reading the real host's ledger, and "no ledger here" would be unfalsifiable.
|
|
783
|
+
HOMELESS="$(mktemp -d)"
|
|
784
|
+
fan_no_ledger="$(env -u ENTWURF_BIN PATH="$PATHLESS" HOME="$HOMELESS" XDG_DATA_HOME="$HOMELESS/.local/share" \
|
|
785
|
+
XDG_STATE_HOME="$HOMELESS/.local/state" \
|
|
786
|
+
HERDR_BIN_PATH="$(command -v herdr)" node "$FAN" </dev/null 2>&1)"; fan_no_ledger_rc=$?
|
|
787
|
+
echo "$fan_no_ledger" | sed 's/^/ /'
|
|
788
|
+
echo " → exit $fan_no_ledger_rc"
|
|
789
|
+
if [ "$fan_no_ledger_rc" -eq 0 ] &&
|
|
790
|
+
printf '%s' "$fan_no_ledger" | grep -q 'entwurf-not-found' &&
|
|
791
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[1\] HERDR INTEGRATION' &&
|
|
792
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[2\] ENTWURF ACTIVATION' &&
|
|
793
|
+
printf '%s' "$fan_no_ledger" | grep -q '\[3\] RAILS'; then
|
|
794
|
+
ok "[QK:HRIL-FAN-NO-LEDGER-SKIPS-CITIZENS-ONLY] with no ledger and no runtime the CITIZEN block alone skips by name at exit 0, and blocks 1-3 still render — never activating is a state, not a defect, and this is what stops D3=C drifting into 'install it from the pane'"
|
|
795
|
+
else
|
|
796
|
+
bad "the no-ledger citizen-skip claim failed: fan rc=$fan_no_ledger_rc did not render blocks 1-3 with a citizen-only skip"
|
|
797
|
+
fi
|
|
798
|
+
rm -rf "$HOMELESS"
|
|
799
|
+
|
|
737
800
|
herdr server stop >/dev/null 2>&1 || true
|
|
738
801
|
|
|
802
|
+
# ── 8a. the SHIPPED LAUNCHER, from the bin the user actually received ───────
|
|
803
|
+
# [7] proves the wiring by running `pi` off PATH. That is not the same claim as the one #118
|
|
804
|
+
# hop 2 makes, and astra named the gap: a user who installed this plugin does not have this
|
|
805
|
+
# checkout, so the only launcher that means anything to them is the one inside the npm
|
|
806
|
+
# artifact the lock names. Measured on the published tarball before this cell existed, the
|
|
807
|
+
# `pi)` case IS in `run.sh` at 0.23.1 — so the claim is now testable, and this cell is what
|
|
808
|
+
# tests it: the same 0-token birth, driven through `<active>/node_modules/.bin/entwurf pi`.
|
|
809
|
+
#
|
|
810
|
+
# The proof that it EXEC'd pi is the birth itself. A launcher that silently did nothing, or
|
|
811
|
+
# resolved to something else, cannot produce a V3 record and a gardenId-keyed socket.
|
|
812
|
+
#
|
|
813
|
+
# IT SITS AFTER [8] ON PURPOSE: the fan cell asserts the unobserved count as an exact
|
|
814
|
+
# literal, and a second citizen born before it would change that number. Order here is the
|
|
815
|
+
# cheaper of the two ways to keep that oracle exact.
|
|
816
|
+
echo; echo "[8a] entwurf pi — the launcher inside the npm artifact, same 0-token birth"
|
|
817
|
+
ENTWURF_INSTALLED_BIN="$ACTIVE/node_modules/.bin/entwurf"
|
|
818
|
+
if [ -x "$ENTWURF_INSTALLED_BIN" ]; then
|
|
819
|
+
node /tmp/use-path-drive.mjs "$ENTWURF_INSTALLED_BIN" "the shipped launcher" "$ENTWURF_INSTALLED_BIN" pi --mode rpc || fail=1
|
|
820
|
+
else
|
|
821
|
+
bad "no installed entwurf bin at $ENTWURF_INSTALLED_BIN — the launcher claim cannot be measured"
|
|
822
|
+
fi
|
|
823
|
+
|
|
739
824
|
# ── 9. deactivate: the roundtrip, on the npm source (#118 H1-5) ──────────────
|
|
740
825
|
# `check-herdr-activation` cell 16 proves this against fixtures: [QK:HAC-DEACTIVATE-ROUNDTRIP]
|
|
741
826
|
# asserts the pi settings return to their ORIGINAL MEANING (JSON equality, not bytes — the
|
|
@@ -380,6 +380,74 @@ cell(
|
|
|
380
380
|
"#94 compaction keys relinquish across nested/string axes: restore, delete, operator-changed, fresh, type-strict",
|
|
381
381
|
compaction_cells,
|
|
382
382
|
)
|
|
383
|
+
|
|
384
|
+
TURN_DURATION = "showTurnDuration"
|
|
385
|
+
|
|
386
|
+
def turn_duration_cells():
|
|
387
|
+
# GLG's ruling: showTurnDuration reports how long a turn took. That is
|
|
388
|
+
# operator INFORMATION, not a single-driver policy surface, so entwurf must
|
|
389
|
+
# neither own it nor force it to false.
|
|
390
|
+
#
|
|
391
|
+
# The claim is measured against the two constants and the real leaf, never
|
|
392
|
+
# quoted from a comment: a drift that puts the key back under management has
|
|
393
|
+
# to redden HERE and not only at the drive level, because the drive level
|
|
394
|
+
# runs one install shape while the constant governs every host.
|
|
395
|
+
managed = m.managed_keys()["settings"]["scalar"]
|
|
396
|
+
assert TURN_DURATION not in managed, managed
|
|
397
|
+
assert TURN_DURATION not in m._managed_scalar_names
|
|
398
|
+
assert TURN_DURATION in m._retired_scalar_names
|
|
399
|
+
retired = {name: last for name, _path, last in m.RETIRED_SETTINGS_SCALARS}
|
|
400
|
+
# The last managed value is the comparison relinquishment turns on. A wrong
|
|
401
|
+
# one here would either skip a value we really wrote or overwrite one we
|
|
402
|
+
# never did, so it is pinned by type as well as equality.
|
|
403
|
+
assert retired[TURN_DURATION] is False, retired[TURN_DURATION]
|
|
404
|
+
|
|
405
|
+
path = [TURN_DURATION]
|
|
406
|
+
|
|
407
|
+
def call(current=MISSING, old=MISSING):
|
|
408
|
+
settings = {} if current is MISSING else {TURN_DURATION: current}
|
|
409
|
+
keys = {} if old is MISSING else {TURN_DURATION: copy.deepcopy(old)}
|
|
410
|
+
state = {"files": {"settings": {"keys": keys}}}
|
|
411
|
+
m.relinquish_retired_scalar(state, settings, TURN_DURATION, path, False)
|
|
412
|
+
return state, settings
|
|
413
|
+
|
|
414
|
+
def led(existed, value):
|
|
415
|
+
return {"kind": "scalar", "path": path, "original": {"existed": existed, "value": value}}
|
|
416
|
+
|
|
417
|
+
# THE MEASURED HOST SHAPE (2026-09-23): ledger says we wrote false over a
|
|
418
|
+
# pre-install false, and the operator has since set true. Their true must
|
|
419
|
+
# survive, and the entry must be consumed so uninstall cannot reach back.
|
|
420
|
+
state, settings = call(True, led(True, False))
|
|
421
|
+
assert settings[TURN_DURATION] is True, settings
|
|
422
|
+
assert TURN_DURATION not in state["files"]["settings"]["keys"]
|
|
423
|
+
# Consumption is the inverse: restore_entry is the only writer uninstall has,
|
|
424
|
+
# and with the row gone there is no entry left to hand it.
|
|
425
|
+
before = copy.deepcopy((state, settings))
|
|
426
|
+
m.relinquish_retired_scalar(state, settings, TURN_DURATION, path, False)
|
|
427
|
+
assert (state, settings) == before
|
|
428
|
+
|
|
429
|
+
# Still carrying OUR false, with provenance -> undo our own write honestly.
|
|
430
|
+
state, settings = call(False, led(True, False))
|
|
431
|
+
assert settings[TURN_DURATION] is False, settings
|
|
432
|
+
state, settings = call(False, led(False, None))
|
|
433
|
+
assert TURN_DURATION not in settings, settings
|
|
434
|
+
|
|
435
|
+
# No provenance at all -> the value is the operator's whatever it says.
|
|
436
|
+
for current in [True, False, MISSING]:
|
|
437
|
+
state, settings = call(current)
|
|
438
|
+
assert settings == ({} if current is MISSING else {TURN_DURATION: current})
|
|
439
|
+
|
|
440
|
+
# Type strictness on the bool axis: `0 == False` while `type(0) is not bool`,
|
|
441
|
+
# so an untyped comparison would rewrite a value that was never ours.
|
|
442
|
+
state, settings = call(0, led(True, False))
|
|
443
|
+
assert settings[TURN_DURATION] == 0 and type(settings[TURN_DURATION]) is int
|
|
444
|
+
|
|
445
|
+
cell(
|
|
446
|
+
"[QK:META-RETIRE-TURN-DURATION-OPERATOR-OWNED]",
|
|
447
|
+
"showTurnDuration is operator-owned: unmanaged in the SSOT, retired at false, "
|
|
448
|
+
"and relinquishment preserves the measured live true while consuming the ledger entry",
|
|
449
|
+
turn_duration_cells,
|
|
450
|
+
)
|
|
383
451
|
sys.exit(failed)
|
|
384
452
|
PY
|
|
385
453
|
then ok "#71 retirement leaf: provenance, bool strictness, malformed refusal, one-shot convergence"
|
|
@@ -409,6 +477,10 @@ assert s['files']['settings']['keys']['promptSuggestionEnabled']['original']['va
|
|
|
409
477
|
assert 'autoCompactEnabled' not in s['files']['settings']['keys']
|
|
410
478
|
assert 'env.DISABLE_AUTOCOMPACT' not in s['files']['settings']['keys']
|
|
411
479
|
assert 'skipDangerousModePermissionPrompt' not in s['files']['settings']['keys']
|
|
480
|
+
# Same for showTurnDuration: a fresh install takes no ownership evidence for it,
|
|
481
|
+
# so on a host entwurf meets for the first time there is nothing to relinquish
|
|
482
|
+
# and nothing uninstall could ever restore.
|
|
483
|
+
assert 'showTurnDuration' not in s['files']['settings']['keys']
|
|
412
484
|
assert s['files']['claudeRoot']['keys']['mcpServers.entwurf-bridge']['original']['value']['command'] == 'old'
|
|
413
485
|
PY
|
|
414
486
|
then ok "state captures original scalar/map values, excludes the retired scalar, and is mode 0600"; else bad "state did not capture original values / retirement / mode 0600"; fi
|
|
@@ -440,6 +512,18 @@ state['files']['settings']['keys']['env.DISABLE_AUTOCOMPACT']={
|
|
|
440
512
|
'kind':'scalar', 'path':['env','DISABLE_AUTOCOMPACT'],
|
|
441
513
|
'original':{'existed':True, 'value':'0'}
|
|
442
514
|
}
|
|
515
|
+
# showTurnDuration: the upgrade shape MEASURED on the source host 2026-09-23,
|
|
516
|
+
# planted verbatim so apply() is judged against the state a real prior version
|
|
517
|
+
# left behind. The ledger proves entwurf wrote `false` over a pre-install
|
|
518
|
+
# `false`; the live value is the `true` the operator set afterwards. This is the
|
|
519
|
+
# only branch where relinquishment could plausibly damage an operator choice —
|
|
520
|
+
# restoring the snapshot here would hand `false` back to somebody who had since
|
|
521
|
+
# turned the display on — so it is the branch the drive level must carry.
|
|
522
|
+
settings['showTurnDuration']=True
|
|
523
|
+
state['files']['settings']['keys']['showTurnDuration']={
|
|
524
|
+
'kind':'scalar', 'path':['showTurnDuration'],
|
|
525
|
+
'original':{'existed':True, 'value':False}
|
|
526
|
+
}
|
|
443
527
|
json.dump(settings, open(sp,'w'), indent=2); open(sp,'a').write('\n')
|
|
444
528
|
json.dump(state, open(stp,'w'), indent=2); open(stp,'a').write('\n')
|
|
445
529
|
PY
|
|
@@ -459,9 +543,16 @@ assert settings['env']['DISABLE_AUTOCOMPACT'] == '0'
|
|
|
459
543
|
assert 'autoCompactEnabled' not in settings
|
|
460
544
|
assert settings['env']['KEEP_ME'] == 'yes'
|
|
461
545
|
assert settings['statusLine']['command'] == os.environ['REPO'] + '/scripts/meta-bridge-statusline.sh'
|
|
462
|
-
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','
|
|
546
|
+
for key in ['promptSuggestionEnabled','awaySummaryEnabled','autoMemoryEnabled','verbose','terminalProgressBarEnabled','useAutoModeDuringPlan','enableWorkflows','workflowKeywordTriggerEnabled']:
|
|
463
547
|
assert settings[key] is False, key
|
|
464
548
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
549
|
+
# showTurnDuration retirement, drive level. Two facts that must not be read off
|
|
550
|
+
# one assertion: the operator's PRE-install `true` (fixture) is still `true`
|
|
551
|
+
# AFTER apply, and it is still `true` because apply never wrote the key rather
|
|
552
|
+
# than because it happened to write the same value. The upgrade shape planted
|
|
553
|
+
# above supplies the second half — a ledger entry proving entwurf wrote `false`
|
|
554
|
+
# here, over a live `true` the operator set afterwards.
|
|
555
|
+
assert settings['showTurnDuration'] is True
|
|
465
556
|
# #94 drive-level relinquishment, both branches:
|
|
466
557
|
# nested path -> the operator's pre-entwurf '0' is back, and the env map that
|
|
467
558
|
# carries it survives with the neighbour key untouched.
|
|
@@ -473,6 +564,9 @@ state=json.load(open(cfg + '/entwurf.install-state.json'))
|
|
|
473
564
|
assert 'skipDangerousModePermissionPrompt' not in state['files']['settings']['keys']
|
|
474
565
|
assert 'autoCompactEnabled' not in state['files']['settings']['keys']
|
|
475
566
|
assert 'env.DISABLE_AUTOCOMPACT' not in state['files']['settings']['keys']
|
|
567
|
+
# The consumed entry is the whole inverse: with no ledger row left, uninstall()
|
|
568
|
+
# has no authority over this key and can never write our `false` back.
|
|
569
|
+
assert 'showTurnDuration' not in state['files']['settings']['keys']
|
|
476
570
|
for item in ['Bash','Read','Write','Edit','Grep','Glob','WebFetch','WebSearch','Skill','mcp__entwurf-bridge__*']:
|
|
477
571
|
assert item in settings['permissions']['allow'], item
|
|
478
572
|
assert settings['permissions']['allow'].count('Read') == 1
|
|
@@ -493,6 +587,13 @@ then ok "apply installs managed keyset without clobbering unrelated keys"; else
|
|
|
493
587
|
if py check >/dev/null 2>&1; then ok "survival check passes after relinquishing the retired scalar"; else bad "survival check failed right after retirement apply"; fi
|
|
494
588
|
MANAGED_JSON="$(py managed-keys)"
|
|
495
589
|
if printf '%s' "$MANAGED_JSON" | grep -q 'skipDangerousModePermissionPrompt'; then bad "managed-keys still claims the retired operator key"; else ok "managed-keys returns skipDangerousModePermissionPrompt to the operator"; fi
|
|
590
|
+
# managed-keys is the CROSS-REPO contract: check-keyset-overlap and the
|
|
591
|
+
# agent-config fragment read it to stay disjoint. Measured 2026-09-23 —
|
|
592
|
+
# `check-keyset-overlap agent-config/claude/settings.fragment.json` failed on
|
|
593
|
+
# exactly one key, showTurnDuration, because both sides claimed it. Dropping our
|
|
594
|
+
# claim is what resolves that collision, so the SSOT output is asserted here and
|
|
595
|
+
# not only in the Python constants.
|
|
596
|
+
if printf '%s' "$MANAGED_JSON" | grep -q 'showTurnDuration'; then bad "managed-keys still claims showTurnDuration — a consumer fragment setting the operator's own value would keep colliding"; else ok "managed-keys returns showTurnDuration to the operator (cross-repo collision resolved at the SSOT)"; fi
|
|
496
597
|
# A provenance-less true cannot be changed safely, but doctor/check must make it visible.
|
|
497
598
|
python3 - <<'PY'
|
|
498
599
|
import json, os
|
|
@@ -577,6 +678,11 @@ assert settings['env']['KEEP_ME'] == 'yes'
|
|
|
577
678
|
assert settings['statusLine']['command'] == '/old/user/statusline.sh'
|
|
578
679
|
assert settings['promptSuggestionEnabled'] is True
|
|
579
680
|
assert settings['skipDangerousModePermissionPrompt'] is False
|
|
681
|
+
# showTurnDuration survives uninstall at the operator's value. The install above
|
|
682
|
+
# ran against a ledger row saying entwurf had written `false` here; relinquish
|
|
683
|
+
# consumed that row, so uninstall reaches this key with no authority at all and
|
|
684
|
+
# the `true` the operator set is still `true`. The key must also be ABSENT from
|
|
685
|
+
# the removal list below — a retired key is neither restored nor deleted.
|
|
580
686
|
assert settings['showTurnDuration'] is True
|
|
581
687
|
# One list, one meaning: these are the keys UNINSTALL removed. `autoCompactEnabled`
|
|
582
688
|
# is not among them post-#94 — that entwurf never introduced it is a different
|
|
@@ -786,9 +786,16 @@ async function main(): Promise<void> {
|
|
|
786
786
|
mode: "follow_up",
|
|
787
787
|
message: `Scene 2 (${liveNeedle}): you were told a color. Reply with that color only, then stop.`,
|
|
788
788
|
});
|
|
789
|
+
// #120 P2: the rail answers with the ACCEPTANCE BOUNDARY, and this send names `follow_up`
|
|
790
|
+
// against a citizen that may well be mid-turn — `queued-follow-up` is a correct, normal
|
|
791
|
+
// answer here. Pinning the word `sent` would have demanded an idle receiver the scene
|
|
792
|
+
// never promises. What is asserted is the rail plus an accepted boundary; a refusal
|
|
793
|
+
// still fails through `isError`.
|
|
789
794
|
ok(
|
|
790
|
-
`${cell}: tools/call entwurf_v2 fire-and-forget
|
|
791
|
-
!send.isError &&
|
|
795
|
+
`${cell}: tools/call entwurf_v2 fire-and-forget was ACCEPTED on the CONTROL-SOCKET rail by the fresh citizen`,
|
|
796
|
+
!send.isError &&
|
|
797
|
+
send.text.includes("control-socket") &&
|
|
798
|
+
/→ (?:sent|queued-steer|queued-follow-up|accepted-unknown-boundary)/.test(send.text),
|
|
792
799
|
`--- response ---\n${send.text}\n--- bridge stderr ---\n${bridge.stderrTail()}`,
|
|
793
800
|
);
|
|
794
801
|
receipts[`${cell}/3-live-send`] = send.text;
|
|
@@ -434,7 +434,9 @@ ok(
|
|
|
434
434
|
// callback-only prompt is delivered first, and the task is released only by the exact
|
|
435
435
|
// successful callback result. The transcript is where both are visible, in order, in one
|
|
436
436
|
// native session.
|
|
437
|
-
|
|
437
|
+
// Since the callback moved out of band (ENTWURF_CALLBACK_TARGET/NONCE in the sibling env), the
|
|
438
|
+
// callback-only prompt carries no address; the receipt is the no-arg first action itself.
|
|
439
|
+
const bootstrapLine = buildOmpCallbackOnlyPrompt()
|
|
438
440
|
.split("\n")
|
|
439
441
|
.find((l) => l.includes("FIRST AND ONLY ACTION")) as string;
|
|
440
442
|
const transcript = fs.readFileSync(transcriptPath as string, "utf8");
|
|
@@ -442,7 +444,7 @@ const callbackAt = transcript.indexOf(bootstrapLine);
|
|
|
442
444
|
const taskAt = transcript.indexOf(sceneFact);
|
|
443
445
|
receipts["5-two-stage-order"] = `callback-only prompt @${callbackAt}, task scene fact @${taskAt}`;
|
|
444
446
|
ok(
|
|
445
|
-
"stage one is in the sibling's own transcript: the extension delivered the callback-ONLY prompt
|
|
447
|
+
"stage one is in the sibling's own transcript: the extension delivered the callback-ONLY prompt (no-arg first action; the address rides the sibling env, never this prompt)",
|
|
446
448
|
callbackAt >= 0,
|
|
447
449
|
` expected line: ${bootstrapLine}`,
|
|
448
450
|
);
|
|
@@ -53,7 +53,7 @@ cat > "$SB/bin/entwurf-bridge" <<'FAKE'
|
|
|
53
53
|
while IFS= read -r line; do
|
|
54
54
|
case "$line" in
|
|
55
55
|
*'"id":1'*) printf '%s\n' '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{},"serverInfo":{"name":"fake-entwurf-bridge","version":"0"}}}' ;;
|
|
56
|
-
*'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"}]}}' ;;
|
|
56
|
+
*'"id":2'*) printf '%s\n' '{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"entwurf_v2"},{"name":"entwurf_self"},{"name":"entwurf_peers"},{"name":"entwurf_inbox_read"},{"name":"entwurf_register_native"},{"name":"entwurf_fresh_call"},{"name":"entwurf_resume_call"},{"name":"entwurf_callback"}]}}' ;;
|
|
57
57
|
esac
|
|
58
58
|
done
|
|
59
59
|
FAKE
|