@junghanacs/entwurf 0.30.1 → 0.32.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/AGENTS.md +4 -2
- package/CHANGELOG.md +194 -0
- package/DELIVERY.md +1 -0
- package/README.md +64 -14
- package/VERIFY.md +36 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +121 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +236 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +746 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +19 -19
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +404 -0
- package/pi/pi-durable/carrier/LICENSE +21 -0
- package/pi/pi-durable/carrier/harness-setup.js +123 -0
- package/pi/pi-durable/carrier/prompt.js +74 -0
- package/pi/pi-durable/carrier/runtime.js +402 -0
- package/pi/pi-durable/carrier/sessions.js +56 -0
- package/pi/pi-durable/carrier/subagent.js +55 -0
- package/pi/pi-durable/carrier/tui.js +655 -0
- package/pi/pi-durable/carrier-relocation.mjs +160 -0
- package/pi/pi-durable/carrier-resolver.mjs +38 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +108 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +32 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +318 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1024 -0
- package/run.sh +624 -184
- package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +87 -34
- package/scripts/check-herdr-activation.ts +5 -4
- package/scripts/check-pi-durable-contact.ts +767 -0
- package/scripts/check-pi-durable-receive.ts +1079 -0
- package/scripts/check-pi-durable-send.ts +1821 -0
- package/scripts/emit-pi-durable-carrier.ts +355 -0
- package/scripts/lib/mutation-qualify.ts +21 -45
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-operator-guard.ts +89 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/lib/qualification-receipt.ts +7 -11
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/gate-qualification.json +13 -0
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +723 -0
- package/scripts/mutants/pi-floor.json +24 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +284 -0
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +22 -3
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// The R1 cell's go/no-go predicates for check-pi-durable-send (#129 L-send), as pure functions so
|
|
2
|
+
// the exact conditions the gate stops on can be checked on their own. Each returns the reasons it
|
|
3
|
+
// refuses; an empty list is the only pass.
|
|
4
|
+
|
|
5
|
+
function canonicalJson(value: unknown): string {
|
|
6
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
7
|
+
if (value !== null && typeof value === "object") {
|
|
8
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
9
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
10
|
+
}
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Verdict {
|
|
15
|
+
ok: boolean;
|
|
16
|
+
reasons: string[];
|
|
17
|
+
}
|
|
18
|
+
const verdict = (reasons: string[]): Verdict => ({ ok: reasons.length === 0, reasons });
|
|
19
|
+
|
|
20
|
+
/** The reported frame: a root `pi.tool` task running in `execute`, and the same-task slot for the call. */
|
|
21
|
+
export function intentFrameVerdict(frame: unknown, callId: string): Verdict {
|
|
22
|
+
const f = frame as {
|
|
23
|
+
node?: { id?: unknown; kind?: unknown; conversationId?: unknown; state?: { status?: unknown; phase?: unknown } };
|
|
24
|
+
slot?: { taskId?: unknown; name?: unknown; callId?: unknown; status?: unknown };
|
|
25
|
+
} | null;
|
|
26
|
+
const reasons: string[] = [];
|
|
27
|
+
if (f?.node?.kind !== "pi.tool") reasons.push("node is not pi.tool");
|
|
28
|
+
if (f?.node?.conversationId !== 1) reasons.push("node is not on the root conversation");
|
|
29
|
+
if (f?.node?.state?.status !== "running") reasons.push("node is not running");
|
|
30
|
+
if (f?.node?.state?.phase !== "execute") reasons.push("node phase is not execute");
|
|
31
|
+
if (f?.slot?.taskId === undefined || f.slot.taskId !== f.node?.id) reasons.push("slot taskId is not the node's");
|
|
32
|
+
if (f?.slot?.name !== "entwurf_v2") reasons.push("slot is not entwurf_v2");
|
|
33
|
+
if (f?.slot?.callId !== callId) reasons.push(`slot is not ${callId}`);
|
|
34
|
+
if (f?.slot?.status !== "running") reasons.push("slot is not running");
|
|
35
|
+
return verdict(reasons);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PreCopyFacts {
|
|
39
|
+
/** host1's actual exit signal. */
|
|
40
|
+
hostExitSignal: string | null | undefined;
|
|
41
|
+
/** bridge1's /proc state when its counters were last read. */
|
|
42
|
+
bridgeStateAtRead: string | null;
|
|
43
|
+
bridgeGone: boolean;
|
|
44
|
+
readDelta: { rchar: number; syscr: number };
|
|
45
|
+
/** Wall-clock ms when `go` was written and when host1 was signalled. */
|
|
46
|
+
goAt: number;
|
|
47
|
+
killAt: number;
|
|
48
|
+
deadlineMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Everything that must hold before the database set is copied and the session reopened. */
|
|
52
|
+
export function preCopyVerdict(f: PreCopyFacts): Verdict {
|
|
53
|
+
const reasons: string[] = [];
|
|
54
|
+
if (f.hostExitSignal !== "SIGKILL") reasons.push(`host1 exit signal is ${String(f.hostExitSignal)}, not SIGKILL`);
|
|
55
|
+
if (f.bridgeStateAtRead !== "T") reasons.push(`bridge1 state at read is ${String(f.bridgeStateAtRead)}, not T`);
|
|
56
|
+
if (!f.bridgeGone) reasons.push("bridge1 is not gone");
|
|
57
|
+
if (f.readDelta.rchar !== 0 || f.readDelta.syscr !== 0) {
|
|
58
|
+
reasons.push(`bridge1 counted reads (rchar +${f.readDelta.rchar}, syscr +${f.readDelta.syscr})`);
|
|
59
|
+
}
|
|
60
|
+
const elapsed = f.killAt - f.goAt;
|
|
61
|
+
if (!(elapsed >= 0 && elapsed <= f.deadlineMs)) {
|
|
62
|
+
reasons.push(`go→kill took ${elapsed}ms, outside the ${f.deadlineMs}ms validity cutoff`);
|
|
63
|
+
}
|
|
64
|
+
return verdict(reasons);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CheckpointFacts {
|
|
68
|
+
row: { id?: unknown; conversation_id?: unknown; status?: unknown } | null;
|
|
69
|
+
record: {
|
|
70
|
+
id?: unknown;
|
|
71
|
+
kind?: unknown;
|
|
72
|
+
conversationId?: unknown;
|
|
73
|
+
input?: { callId?: unknown };
|
|
74
|
+
state?: { status?: unknown; checkpoint?: { phase?: unknown; replay?: unknown; arguments?: unknown } };
|
|
75
|
+
} | null;
|
|
76
|
+
resultEntriesForCall: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The copied database holds THIS call's committed, unsafe intent and no result for it. */
|
|
80
|
+
export function checkpointVerdict(
|
|
81
|
+
f: CheckpointFacts,
|
|
82
|
+
expect: { nodeId: number; callId: string; args: unknown },
|
|
83
|
+
): Verdict {
|
|
84
|
+
const reasons: string[] = [];
|
|
85
|
+
if (f.row === null || f.record === null) return verdict(["no task row for the observed task id"]);
|
|
86
|
+
if (f.row.id !== expect.nodeId) reasons.push("queried row id is not the observed task id");
|
|
87
|
+
if (f.record.id !== expect.nodeId) reasons.push("record id is not the observed task id");
|
|
88
|
+
if (f.record.kind !== "pi.tool") reasons.push("record is not pi.tool");
|
|
89
|
+
if (f.record.conversationId !== 1) reasons.push("record is not on the root conversation");
|
|
90
|
+
if (f.record.input?.callId !== expect.callId) reasons.push(`record input callId is not ${expect.callId}`);
|
|
91
|
+
if (f.record.state?.status !== "running") reasons.push("record is not running");
|
|
92
|
+
if (f.record.state?.checkpoint?.phase !== "execute") reasons.push("checkpoint phase is not execute");
|
|
93
|
+
if (f.record.state?.checkpoint?.replay !== "unsafe") reasons.push("checkpoint replay is not unsafe");
|
|
94
|
+
if (canonicalJson(f.record.state?.checkpoint?.arguments) !== canonicalJson(expect.args)) {
|
|
95
|
+
reasons.push("checkpoint arguments are not the scripted arguments");
|
|
96
|
+
}
|
|
97
|
+
if (f.resultEntriesForCall !== 0)
|
|
98
|
+
reasons.push(`${f.resultEntriesForCall} result entr(ies) already exist for the call`);
|
|
99
|
+
return verdict(reasons);
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The cleanup verdict of check-pi-durable-send (#129 L-send), as a pure function so the exact
|
|
2
|
+
// predicate the gate uses can be checked on its own.
|
|
3
|
+
//
|
|
4
|
+
// Green only when every owned process is gone, no signal failed, the scripted endpoint closed, and
|
|
5
|
+
// every owned host's children were OBSERVED before it went away. A host that exited before /proc
|
|
6
|
+
// could be asked about its children leaves an unknown, and an unknown is not a clean reap.
|
|
7
|
+
|
|
8
|
+
export interface ReapFacts {
|
|
9
|
+
/** Owned processes still alive with their recorded start key. */
|
|
10
|
+
alive: readonly number[];
|
|
11
|
+
/** Signal or exit failures met while reaping. */
|
|
12
|
+
problems: readonly string[];
|
|
13
|
+
endpointClosed: boolean;
|
|
14
|
+
endpointListening: boolean;
|
|
15
|
+
/** Hosts that exited before their children were observed. */
|
|
16
|
+
observationNotes: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ownedReapVerdict(facts: ReapFacts): { green: boolean; reasons: string[] } {
|
|
20
|
+
const reasons = [
|
|
21
|
+
...(facts.alive.length > 0 ? [`still alive: ${facts.alive.join(",")}`] : []),
|
|
22
|
+
...facts.problems,
|
|
23
|
+
...(facts.endpointClosed && !facts.endpointListening ? [] : ["the scripted endpoint did not close"]),
|
|
24
|
+
...facts.observationNotes.map((note) => `unobserved: ${note}`),
|
|
25
|
+
];
|
|
26
|
+
return { green: reasons.length === 0, reasons };
|
|
27
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// The receive cell's go/no-go predicates for check-pi-durable-receive (#129 L-receive), as pure
|
|
2
|
+
// functions so the exact conditions the gate stops on can be checked on their own. Each returns the
|
|
3
|
+
// reasons it refuses; an empty list is the only pass. Nothing here imports the adapter: the doorbell
|
|
4
|
+
// notice and the request id are recognised by the contract's public words, not by its constants.
|
|
5
|
+
|
|
6
|
+
export interface Verdict {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
reasons: string[];
|
|
9
|
+
}
|
|
10
|
+
const verdict = (reasons: string[]): Verdict => ({ ok: reasons.length === 0, reasons });
|
|
11
|
+
|
|
12
|
+
/** The mailbox file a meta-mailbox ENQUEUE receipt names, or undefined when the text is not one. */
|
|
13
|
+
export function enqueuedFile(receipt: string): string | undefined {
|
|
14
|
+
return /^entwurf_v2 meta-mailbox → enqueued \(([^()/]+\.msg)\)$/.exec(receipt.trim())?.[1];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HeldCaptureFacts {
|
|
18
|
+
hostGid: string;
|
|
19
|
+
hostPid: number;
|
|
20
|
+
hostStartKey: string;
|
|
21
|
+
/** The ENQUEUE receipt's file name. */
|
|
22
|
+
enqueued: string | undefined;
|
|
23
|
+
/** The host's mailbox directory listing at the hold. */
|
|
24
|
+
listing: string[];
|
|
25
|
+
/** Bytes of `<enqueued>.delivered` at the hold, or null when absent. */
|
|
26
|
+
deliveredBody: string | null;
|
|
27
|
+
sentMessage: string;
|
|
28
|
+
signalPresent: boolean;
|
|
29
|
+
/** The host's raw receiver marker at the hold, or null when absent. */
|
|
30
|
+
receiverMarker: Record<string, unknown> | null;
|
|
31
|
+
/** The newest user message text of the held request #1. */
|
|
32
|
+
request1UserText: string;
|
|
33
|
+
/** The tool names request #1 offers. */
|
|
34
|
+
request1Tools: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Everything that must hold while response #1 is withheld: the sender's ENQUEUE named a file, the
|
|
39
|
+
* host's doorbell has already stamped exactly that file `.delivered` (a wake attempt, not a read), its
|
|
40
|
+
* bytes carry the sent message, the signal and the host-owned receiver marker are in place, and the
|
|
41
|
+
* request that the root admission started is the announce — garden and tool named, body absent.
|
|
42
|
+
*/
|
|
43
|
+
export function heldCaptureVerdict(f: HeldCaptureFacts): Verdict {
|
|
44
|
+
const reasons: string[] = [];
|
|
45
|
+
if (f.enqueued === undefined) return verdict(["the sender's receipt is not a meta-mailbox ENQUEUE"]);
|
|
46
|
+
if (f.listing.includes(f.enqueued)) reasons.push(`${f.enqueued} is still a fresh .msg — no doorbell stamped it`);
|
|
47
|
+
if (!f.listing.includes(`${f.enqueued}.delivered`)) reasons.push(`${f.enqueued}.delivered is absent`);
|
|
48
|
+
if (f.listing.some((n) => n.endsWith(".read"))) reasons.push("a body was already archived before the hold");
|
|
49
|
+
if (f.deliveredBody === null || !f.deliveredBody.includes(f.sentMessage)) {
|
|
50
|
+
reasons.push("the delivered bytes do not carry the sent message");
|
|
51
|
+
}
|
|
52
|
+
if (!f.signalPresent) reasons.push("inbox.signal is absent");
|
|
53
|
+
const m = f.receiverMarker;
|
|
54
|
+
if (m === null) reasons.push("the host's receiver marker is absent");
|
|
55
|
+
else {
|
|
56
|
+
if (m.gardenId !== f.hostGid) reasons.push("the receiver marker names another garden");
|
|
57
|
+
if (m.ownerPid !== f.hostPid) reasons.push("the receiver marker is not owned by the spawned host");
|
|
58
|
+
if (m.ownerStartKey !== `linux:${f.hostStartKey}`)
|
|
59
|
+
reasons.push("the receiver marker's start key is not the host's");
|
|
60
|
+
if (m.ownerKind !== "pi-durable-host") reasons.push("the receiver marker is not pi-durable-host");
|
|
61
|
+
}
|
|
62
|
+
const text = f.request1UserText;
|
|
63
|
+
if (!text.startsWith("[entwurf inbox] 1 unread mailbox message available for garden ")) {
|
|
64
|
+
reasons.push("request #1's user input is not a one-message doorbell notice");
|
|
65
|
+
}
|
|
66
|
+
if (!text.includes(f.hostGid) || !text.includes("entwurf_inbox_read")) {
|
|
67
|
+
reasons.push("the notice does not name the host's garden and entwurf_inbox_read");
|
|
68
|
+
}
|
|
69
|
+
if (text.includes(f.sentMessage)) reasons.push("the notice carries the body");
|
|
70
|
+
if (!f.request1Tools.includes("entwurf_inbox_read")) reasons.push("request #1 does not offer entwurf_inbox_read");
|
|
71
|
+
return verdict(reasons);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* How upstream writes the indexed `submissions.request_id` column: `JSON.stringify` of the request id
|
|
76
|
+
* (`[read packages/durable/src/storage/sqlite/storage.ts:64 encodeIndexedString, :762 insert, :442
|
|
77
|
+
* lookup]` @cd32f77), the same encoding R1 measured on `tasks.kind`. The record JSON keeps it raw.
|
|
78
|
+
*/
|
|
79
|
+
export const encodedRequestIdColumn = (requestId: string): string => JSON.stringify(requestId);
|
|
80
|
+
|
|
81
|
+
export interface SubmissionFacts {
|
|
82
|
+
/** The `submissions` row whose parsed record carries the request id, with that record, or null. */
|
|
83
|
+
row: { id?: unknown; conversation_id?: unknown; request_id?: unknown; status?: unknown } | null;
|
|
84
|
+
record: {
|
|
85
|
+
id?: unknown;
|
|
86
|
+
conversationId?: unknown;
|
|
87
|
+
requestId?: unknown;
|
|
88
|
+
type?: unknown;
|
|
89
|
+
status?: unknown;
|
|
90
|
+
entry?: unknown;
|
|
91
|
+
} | null;
|
|
92
|
+
/** Rows whose parsed record carries the request id, over the whole table. */
|
|
93
|
+
rowsForRequest: number;
|
|
94
|
+
/** Rows whose indexed `request_id` column holds the request id as upstream encodes it. */
|
|
95
|
+
rowsForColumn: number;
|
|
96
|
+
/** The entry the record names, parsed, or null. */
|
|
97
|
+
entry: { kind?: unknown; model?: { role?: unknown; content?: unknown }[] } | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The copied database holds exactly one input submission for the doorbell's request id — found both
|
|
102
|
+
* by its record and by its indexed column — on the root conversation, whose row id AND record id are
|
|
103
|
+
* the id the receiver reported, placed as the root user entry carrying the notice and answered. A row
|
|
104
|
+
* still queued is reported as queued — never read as placed.
|
|
105
|
+
*/
|
|
106
|
+
export function submissionVerdict(
|
|
107
|
+
f: SubmissionFacts,
|
|
108
|
+
expect: { submissionId: unknown; requestId: string; notice: string },
|
|
109
|
+
): Verdict {
|
|
110
|
+
const reasons: string[] = [];
|
|
111
|
+
if (f.rowsForRequest !== 1) reasons.push(`${f.rowsForRequest} records carry the request id`);
|
|
112
|
+
if (f.rowsForColumn !== 1) reasons.push(`${f.rowsForColumn} rows hold the encoded request id in the indexed column`);
|
|
113
|
+
if (f.row === null || f.record === null) return verdict([...reasons, "no submission row for the request id"]);
|
|
114
|
+
if (f.row.id !== expect.submissionId) reasons.push("the row id is not the reported submission id");
|
|
115
|
+
if (f.record.id === undefined) reasons.push("the record carries no id");
|
|
116
|
+
else if (f.record.id !== f.row.id || f.record.id !== expect.submissionId) {
|
|
117
|
+
reasons.push("the record id is not the row id and the reported submission id");
|
|
118
|
+
}
|
|
119
|
+
if (f.row.conversation_id !== 1 || f.record.conversationId !== 1)
|
|
120
|
+
reasons.push("the row is not on the root conversation");
|
|
121
|
+
if (f.row.request_id !== encodedRequestIdColumn(expect.requestId) || f.record.requestId !== expect.requestId) {
|
|
122
|
+
reasons.push("the row's request id is not the doorbell's");
|
|
123
|
+
}
|
|
124
|
+
if (f.record.type !== "input") reasons.push("the submission is not an input");
|
|
125
|
+
if (f.row.status !== f.record.status) reasons.push("the row status and the record status disagree");
|
|
126
|
+
if (f.record.status === "queued") reasons.push("the submission is still queued, not placed");
|
|
127
|
+
else if (f.record.status !== "done") reasons.push(`the submission is ${String(f.record.status)}, not done`);
|
|
128
|
+
const message = f.entry?.model?.[0];
|
|
129
|
+
if (f.entry === null) reasons.push("the record names no readable entry");
|
|
130
|
+
else {
|
|
131
|
+
if (f.entry.kind !== "pi.user") reasons.push("the placed entry is not pi.user");
|
|
132
|
+
if (message?.role !== "user" || message.content !== expect.notice) {
|
|
133
|
+
reasons.push("the placed entry's content is not the notice the model received");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return verdict(reasons);
|
|
137
|
+
}
|
|
@@ -38,12 +38,12 @@ import { createHash } from "node:crypto";
|
|
|
38
38
|
import * as fs from "node:fs";
|
|
39
39
|
import * as path from "node:path";
|
|
40
40
|
import {
|
|
41
|
-
createRepoSnapshot,
|
|
42
41
|
type MutantSpec,
|
|
43
42
|
originHead,
|
|
44
43
|
originWorkSurfaceSha,
|
|
45
44
|
qualifyMutants,
|
|
46
45
|
reportPassed,
|
|
46
|
+
withOwnSnapshot,
|
|
47
47
|
} from "./mutation-qualify.ts";
|
|
48
48
|
|
|
49
49
|
export const RECEIPT_SCHEMA = "entwurf.qualification-group-receipt/v0";
|
|
@@ -471,16 +471,12 @@ export async function runGroupReceipt(
|
|
|
471
471
|
const declaredInputs = declaredInputPaths(repoDir, groupSpecs, gate);
|
|
472
472
|
const inputsBefore = fileFingerprints(repoDir, declaredInputs);
|
|
473
473
|
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
report = await qualifyMutants(snap, groupSpecs, opts.log);
|
|
481
|
-
} finally {
|
|
482
|
-
fs.rmSync(snap.baseDir, { recursive: true, force: true });
|
|
483
|
-
}
|
|
474
|
+
const report = await withOwnSnapshot(repoDir, opts.tmpRoot, (snap) => {
|
|
475
|
+
opts.log(
|
|
476
|
+
`[gate-qualification --group] PARTIAL: ${groupSpecs.length}/${selected.length} mutants of ${groupKey}; snapshot ${snap.repoDir} (origin HEAD ${headBefore.slice(0, 12)})`,
|
|
477
|
+
);
|
|
478
|
+
return qualifyMutants(snap, groupSpecs, opts.log);
|
|
479
|
+
});
|
|
484
480
|
|
|
485
481
|
const headAfter = originHead(repoDir);
|
|
486
482
|
const workSurfaceAfter = originWorkSurfaceSha(repoDir);
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"claim": "PI-CORE-PIN-TRACKS-FLOOR",
|
|
182
182
|
"title": "the queue oracle's pi-agent-core pin drifts off the floor the rest of the pi runtime is pinned to, so the oracle keeps answering honestly — about a pi nobody in this repo runs",
|
|
183
183
|
"subject": "package.json",
|
|
184
|
-
"find": ["\"@earendil-works/pi-agent-core\": \"1.0.
|
|
184
|
+
"find": ["\"@earendil-works/pi-agent-core\": \"1.0.4\","],
|
|
185
185
|
"replace": ["\"@earendil-works/pi-agent-core\": \"0.87.1\","],
|
|
186
186
|
"gate": ["bash", "run.sh", "check-dep-versions"],
|
|
187
187
|
"timeoutSeconds": 300,
|
|
@@ -133,10 +133,10 @@
|
|
|
133
133
|
"title": "closing the reader half leaves the writer talking to nobody — the hook writes a copilot marker, the bridge never looks in that directory, and the citizen's sends are refused as anonymous (the #46 shape, which is why writer and reader open in one change)",
|
|
134
134
|
"subject": "pi-extensions/lib/meta-sender-identity.ts",
|
|
135
135
|
"find": [
|
|
136
|
-
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\"
|
|
136
|
+
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\n\t\"claude-code\",\n\t\"antigravity\",\n\t\"copilot\",\n\t\"omp\",\n\t\"pi-durable\",\n];"
|
|
137
137
|
],
|
|
138
138
|
"replace": [
|
|
139
|
-
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\"
|
|
139
|
+
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\n\t\"claude-code\",\n\t\"antigravity\",\n\t\"omp\",\n\t\"pi-durable\",\n];"
|
|
140
140
|
],
|
|
141
141
|
"gate": ["bash", "run.sh", "check-copilot-birth-hook"],
|
|
142
142
|
"timeoutSeconds": 300,
|
|
@@ -82,6 +82,19 @@
|
|
|
82
82
|
"timeoutSeconds": 120,
|
|
83
83
|
"signature": "[QK:QUALIFY-BUILD-GROUP-CONSISTENT]",
|
|
84
84
|
"signatureSource": "scripts/check-gate-qualification.ts"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"claim": "QUALIFY-PRIOR-RUN-EVIDENCE-KEPT",
|
|
88
|
+
"title": "the snapshot seam lists its tmp root and removes every snapshot-prefixed root before making its own — the retired ambient sweep is back, and a prior run's evidence is deleted by the qualifier instead of left to the operator (scoped to an explicit tmpRoot, which only self-test fixtures pass, so the defect never reaches the real tmp dir)",
|
|
89
|
+
"subject": "scripts/lib/mutation-qualify.ts",
|
|
90
|
+
"find": ["\tconst snap = createRepoSnapshot(originDir, tmpRoot);"],
|
|
91
|
+
"replace": [
|
|
92
|
+
"\tif (tmpRoot !== undefined) for (const name of fs.readdirSync(tmpRoot)) if (name.startsWith(SNAPSHOT_PREFIX)) fs.rmSync(path.join(tmpRoot, name), { recursive: true, force: true });\n\tconst snap = createRepoSnapshot(originDir, tmpRoot);"
|
|
93
|
+
],
|
|
94
|
+
"gate": ["bash", "run.sh", "check-gate-manifests"],
|
|
95
|
+
"timeoutSeconds": 120,
|
|
96
|
+
"signature": "[QK:QUALIFY-PRIOR-RUN-EVIDENCE-KEPT]",
|
|
97
|
+
"signatureSource": "scripts/check-gate-qualification.ts"
|
|
85
98
|
}
|
|
86
99
|
]
|
|
87
100
|
}
|
|
@@ -182,9 +182,11 @@
|
|
|
182
182
|
"claim": "MHSS-JOIN-SCOPE-OWNERKIND",
|
|
183
183
|
"title": "admitting copilot-extension into the sender-marker join makes every Copilot citizen permanently mailbox-undeliverable: its watch owner is a forked child whose pid never carries a sender marker, so the join can only ever answer no",
|
|
184
184
|
"subject": "pi-extensions/lib/entwurf-deliverability.ts",
|
|
185
|
-
"find": [
|
|
185
|
+
"find": [
|
|
186
|
+
"export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\", \"pi-durable-host\"];"
|
|
187
|
+
],
|
|
186
188
|
"replace": [
|
|
187
|
-
"export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\", \"copilot-extension\"];"
|
|
189
|
+
"export const SENDER_JOINED_RECEIVER_OWNER_KINDS: readonly string[] = [\"claude-code-cli\", \"pi-durable-host\", \"copilot-extension\"];"
|
|
188
190
|
],
|
|
189
191
|
"gate": ["bash", "run.sh", "check-meta-hook-session-switch"],
|
|
190
192
|
"timeoutSeconds": 300,
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"title": "MCP drops the required model field from fresh-call while the composition still expects one",
|
|
152
152
|
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
153
153
|
"find": [
|
|
154
|
-
"\t\tmodel: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.max(200)\n\t\t\t// The `\\[` below is USELESS TO JS AND LOAD-BEARING TO THE HOST. zod emits this source\n\t\t\t// text verbatim as the JSON Schema `pattern`, and the host's tool-schema validator is a\n\t\t\t// Rust regex engine that rejects an unescaped `[` inside a character class as \"unclosed\n\t\t\t// character class\" — a 400 on tools/list that stops EVERY Claude session from opening\n\t\t\t// (7-M-fix). Biome's safe fix removes it; do not accept that fix. The suppression must\n\t\t\t// stay on the line DIRECTLY above the expression, or it attaches to nothing and reads\n\t\t\t// as an unused suppression while the escape goes back to being fixable.\n\t\t\t// biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above\n\t\t\t.regex(/^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$/)\n\t\t\t.describe(\n\t\t\t\t\"Required runtime model: canonical provider/model for pi, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.\",\n\t\t\t),\n"
|
|
154
|
+
"\t\tmodel: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.max(200)\n\t\t\t// The `\\[` below is USELESS TO JS AND LOAD-BEARING TO THE HOST. zod emits this source\n\t\t\t// text verbatim as the JSON Schema `pattern`, and the host's tool-schema validator is a\n\t\t\t// Rust regex engine that rejects an unescaped `[` inside a character class as \"unclosed\n\t\t\t// character class\" — a 400 on tools/list that stops EVERY Claude session from opening\n\t\t\t// (7-M-fix). Biome's safe fix removes it; do not accept that fix. The suppression must\n\t\t\t// stay on the line DIRECTLY above the expression, or it attaches to nothing and reads\n\t\t\t// as an unused suppression while the escape goes back to being fixable.\n\t\t\t// biome-ignore lint/complexity/noUselessEscapeInRegex: emitted to a Rust regex validator, see above\n\t\t\t.regex(/^[A-Za-z0-9][A-Za-z0-9._/:\\[\\]-]*$/)\n\t\t\t.describe(\n\t\t\t\t\"Required runtime model: canonical provider/model for pi, provider/<exact catalog model id> for pi-durable, a Claude Code model id/alias, or a Copilot/OMP/Codex model name.\",\n\t\t\t),\n"
|
|
155
155
|
],
|
|
156
156
|
"replace": [""],
|
|
157
157
|
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
@@ -323,8 +323,8 @@
|
|
|
323
323
|
"claim": "FRESHCALL-BACKEND-SET-SURFACE-PARITY",
|
|
324
324
|
"title": "a backend the composition can open is missing from the MCP surface enum — the capability exists and no external host can reach it",
|
|
325
325
|
"subject": "mcp/entwurf-bridge/src/index.ts",
|
|
326
|
-
"find": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\", \"codex\"])"],
|
|
327
|
-
"replace": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\"])"],
|
|
326
|
+
"find": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\", \"codex\", \"pi-durable\"])"],
|
|
327
|
+
"replace": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\", \"pi-durable\"])"],
|
|
328
328
|
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
329
329
|
"timeoutSeconds": 180,
|
|
330
330
|
"signature": "[QK:FRESHCALL-BACKEND-SET-SURFACE-PARITY]",
|
|
@@ -700,6 +700,72 @@
|
|
|
700
700
|
"timeoutSeconds": 120,
|
|
701
701
|
"signature": "[QK:FRESHCALL-CALLBACK-ENV]",
|
|
702
702
|
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"claim": "FRESHCALL-PI-DURABLE-ARGV",
|
|
706
|
+
"title": "pi-durable loses its explicit task-wide width, so the packaged bootstrap refuses a launch the composition said it built",
|
|
707
|
+
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
708
|
+
"find": ["\t\t\t\t\"--width\",\n\t\t\t\t\"task-wide\",\n"],
|
|
709
|
+
"replace": [""],
|
|
710
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
711
|
+
"timeoutSeconds": 120,
|
|
712
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-ARGV]",
|
|
713
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"claim": "FRESHCALL-PI-DURABLE-PREFLIGHT-PREMUTATION",
|
|
717
|
+
"title": "the pi-durable preflight (model, framed input cap, carrier, units) is skipped, so a refusal it owns is discovered only after a window was placed",
|
|
718
|
+
"subject": "pi-extensions/lib/mux-fresh-call.ts",
|
|
719
|
+
"find": ["\tif (params.backend === \"pi-durable\") {\n"],
|
|
720
|
+
"replace": ["\tif (false) {\n"],
|
|
721
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
722
|
+
"timeoutSeconds": 120,
|
|
723
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-PREFLIGHT-PREMUTATION]",
|
|
724
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"claim": "FRESHCALL-PI-DURABLE-MANAGED-RUNTIME",
|
|
728
|
+
"title": "pi-durable opens the bare app instead of the managed `entwurf pi-durable`, so nothing verifies the carrier and its SDK set before its resolver runs",
|
|
729
|
+
"subject": "pi-extensions/lib/mux-fresh-call.ts",
|
|
730
|
+
"find": ["\t\"pi-durable\": \"entwurf\","],
|
|
731
|
+
"replace": ["\t\"pi-durable\": \"pi\","],
|
|
732
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
733
|
+
"timeoutSeconds": 120,
|
|
734
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-MANAGED-RUNTIME]",
|
|
735
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"claim": "FRESHCALL-PI-DURABLE-CALLBACK-DIALECT",
|
|
739
|
+
"title": "pi-durable is told the composed MCP callback name, which the durable contact never registers — the contact offers the bridge verbs under their bare names",
|
|
740
|
+
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
741
|
+
"find": ["\t\"pi-durable\": \"entwurf_callback\","],
|
|
742
|
+
"replace": ["\t\"pi-durable\": \"mcp__entwurf-bridge__entwurf_callback\","],
|
|
743
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
744
|
+
"timeoutSeconds": 120,
|
|
745
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-CALLBACK-DIALECT]",
|
|
746
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
"claim": "PD-FRESH-LIVE-FOREIGN-SENDER-REFUSED",
|
|
750
|
+
"title": "the pi-durable LIVE gate's Pi-side oracle drops its expected-sender check, so the exact payload relayed by any other citizen counts as pd's delivery",
|
|
751
|
+
"subject": "scripts/lib/pi-durable-fresh-live-receipts.ts",
|
|
752
|
+
"find": ["\t\tif (senders.length === 1 && senders[0] === expectedSender) hits.push({ row, index });"],
|
|
753
|
+
"replace": ["\t\tif (senders.length === 1) hits.push({ row, index });"],
|
|
754
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
755
|
+
"timeoutSeconds": 120,
|
|
756
|
+
"signature": "[QK:PD-FRESH-LIVE-FOREIGN-SENDER-REFUSED]",
|
|
757
|
+
"signatureSource": "test/pi-durable-fresh-live-receipts.test.ts"
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"claim": "PIIMPORT-CARRIER-FENCE",
|
|
761
|
+
"title": "the carrier fence stops naming bare imports, so a pi-coding-agent specifier or an undeclared package in an emitted carrier file passes the floor",
|
|
762
|
+
"subject": "run.sh",
|
|
763
|
+
"find": ["if (name === \"@earendil-works/pi-coding-agent\" || !declared.has(name)) console.log("],
|
|
764
|
+
"replace": ["if (false) console.log("],
|
|
765
|
+
"gate": ["bash", "run.sh", "check-pi-import-surface"],
|
|
766
|
+
"timeoutSeconds": 60,
|
|
767
|
+
"signature": "[QK:PIIMPORT-CARRIER-FENCE]",
|
|
768
|
+
"signatureSource": "run.sh"
|
|
703
769
|
}
|
|
704
770
|
]
|
|
705
771
|
}
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
"title": "a backend whose hook writes a marker but is absent from the reader list is INVISIBLE — the bridge holds the owner pid, never looks in that directory, and every send is refused as anonymous for a reason nothing logs (the #46 defect)",
|
|
67
67
|
"subject": "pi-extensions/lib/meta-sender-identity.ts",
|
|
68
68
|
"find": [
|
|
69
|
-
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\"
|
|
69
|
+
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\n\t\"claude-code\",\n\t\"antigravity\",\n\t\"copilot\",\n\t\"omp\",\n\t\"pi-durable\",\n];"
|
|
70
70
|
],
|
|
71
71
|
"replace": [
|
|
72
|
-
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\"claude-code\"
|
|
72
|
+
"export const META_SENDER_BACKENDS: readonly MetaBackend[] = [\n\t\"claude-code\",\n\t\"antigravity\",\n\t\"copilot\",\n\t\"pi-durable\",\n];"
|
|
73
73
|
],
|
|
74
74
|
"gate": ["bash", "run.sh", "check-omp-birth-hook"],
|
|
75
75
|
"timeoutSeconds": 300,
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
"title": "a post-#82 harness admitted as a full D6 citizen while entwurf_fresh_call cannot open it — the exact state the Bundle A+B candidate shipped in, where every gate was green and only prose said the backend was unsupported. This is the defect the whole gate exists for: without it, an `unsupported` note reads as a partial-release permit",
|
|
8
8
|
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
9
9
|
"find": [
|
|
10
|
-
"export const FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\", \"copilot\", \"omp\", \"codex\"] as const;"
|
|
10
|
+
"export const FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\", \"copilot\", \"omp\", \"codex\", \"pi-durable\"] as const;"
|
|
11
|
+
],
|
|
12
|
+
"replace": [
|
|
13
|
+
"export const FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\", \"copilot\", \"codex\", \"pi-durable\"] as const;"
|
|
11
14
|
],
|
|
12
|
-
"replace": ["export const FRESH_CALL_BACKENDS = [\"pi\", \"claude-code\", \"copilot\", \"codex\"] as const;"],
|
|
13
15
|
"gate": ["bash", "run.sh", "check-harness-admission-parity"],
|
|
14
16
|
"timeoutSeconds": 300,
|
|
15
17
|
"signature": "[QK:ADMISSION-FRESH-PARITY]",
|
|
@@ -119,8 +121,8 @@
|
|
|
119
121
|
"claim": "FRESHCALL-BACKEND-SET-SURFACE-PARITY-SKILL",
|
|
120
122
|
"title": "a backend added to the composition and both schema surfaces but not to the operator skill — the third public surface step 9 names, and the one that had no gate at all. The skill is what an operator reads to decide what can be opened, so a stale list makes the backend unreachable in practice exactly the way a missing enum value makes it unreachable in schema",
|
|
121
123
|
"subject": ".claude/skills/entwurf-dev/SKILL.md",
|
|
122
|
-
"find": ["backend는 정확히 `pi | claude-code | copilot | omp | codex`이고"],
|
|
123
|
-
"replace": ["backend는 정확히 `pi | claude-code | copilot | omp`이고"],
|
|
124
|
+
"find": ["backend는 정확히 `pi | claude-code | copilot | omp | codex | pi-durable`이고"],
|
|
125
|
+
"replace": ["backend는 정확히 `pi | claude-code | copilot | omp | pi-durable`이고"],
|
|
124
126
|
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
125
127
|
"timeoutSeconds": 300,
|
|
126
128
|
"signature": "[QK:FRESHCALL-BACKEND-SET-SURFACE-PARITY-SKILL]",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"claim": "PACK-INSTALL-PIN-MATCHER-BOUNDED",
|
|
7
7
|
"title": "the pin-leak matcher loses its version boundary and blesses substring lookalikes — @0.85.10 reads as the pinned 0.85.1 and the gate announces a verified tree it never verified (independent review, 2026-08-25)",
|
|
8
8
|
"subject": "run.sh",
|
|
9
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0\\.
|
|
9
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0\\.4(_|$)' || true"],
|
|
10
10
|
"replace": [" grep '^@earendil-works+' | grep -v '@0\\.87\\.1' || true"],
|
|
11
11
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
12
12
|
"timeoutSeconds": 120,
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"claim": "PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE",
|
|
18
18
|
"title": "the pin-leak matcher narrows back to the pi-* families and goes blind to a non-pi member of the same runtime closure — @earendil-works/chord (new in pi 0.85.0, a runtime dependency of pi-coding-agent/pi-agent-core/pi-client/pi-protocol) floats unpinned while the gate prints a verified pin (measured 2026-09-06 on a real 0.85.1 install tree)",
|
|
19
19
|
"subject": "run.sh",
|
|
20
|
-
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0\\.
|
|
21
|
-
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.0\\.
|
|
20
|
+
"find": [" grep '^@earendil-works+' | grep -Ev '@1\\.0\\.4(_|$)' || true"],
|
|
21
|
+
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.0\\.4(_|$)' || true"],
|
|
22
22
|
"gate": ["bash", "run.sh", "check-pack-pin-matcher"],
|
|
23
23
|
"timeoutSeconds": 120,
|
|
24
24
|
"signature": "[QK:PACK-INSTALL-PIN-MATCHER-COVERS-CLOSURE]",
|