@junghanacs/entwurf 0.30.1 → 0.31.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 +1 -0
- package/CHANGELOG.md +112 -0
- package/DELIVERY.md +1 -0
- package/README.md +47 -7
- package/VERIFY.md +17 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +104 -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 +30 -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 +288 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +13 -10
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +281 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +16 -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 +34 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
- package/run.sh +200 -33
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +2 -1
- package/scripts/check-pi-durable-contact.ts +834 -0
- package/scripts/check-pi-durable-receive.ts +1116 -0
- package/scripts/check-pi-durable-send.ts +1661 -0
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -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/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- 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 +390 -0
- package/scripts/mutants/pi-floor.json +2 -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 +213 -0
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +21 -2
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The oracles of `smoke-pi-durable-fresh-live`, as pure functions over raw evidence, so the same code
|
|
3
|
+
* that judges a live run can re-judge its saved raw (Pi's JSONL and the pd native-store snapshot).
|
|
4
|
+
*
|
|
5
|
+
* Pi side: a message lands only as an exact payload with ONE anchored terminal sender envelope —
|
|
6
|
+
* `piCallbackSenders` already decides that shape; this adds the expected sender and returns the row.
|
|
7
|
+
* A body that merely MENTIONS the token and the sender's id (the fixture's own instruction did) is
|
|
8
|
+
* not a delivery.
|
|
9
|
+
*
|
|
10
|
+
* pd side: the native store's ROOT conversation only (no owner conversation or task), read through
|
|
11
|
+
* its entry kinds — `pi.system` (tool offering, applied as toolsAdded/toolsRemoved in order),
|
|
12
|
+
* `pi.assistant` (the model that answered, and the tool calls in order) and `pi.tool-result` (each
|
|
13
|
+
* call's settled result, joined by toolCallId).
|
|
14
|
+
*/
|
|
15
|
+
import { piCallbackSenders } from "./codex-fresh-source-receipts.ts";
|
|
16
|
+
|
|
17
|
+
/** The one Pi transcript row whose exact payload arrived from `expectedSender`, or null. */
|
|
18
|
+
export function piExactDeliveryRow(
|
|
19
|
+
entries: readonly unknown[],
|
|
20
|
+
payload: string,
|
|
21
|
+
expectedSender: string,
|
|
22
|
+
): { row: unknown; index: number } | null {
|
|
23
|
+
const hits: { row: unknown; index: number }[] = [];
|
|
24
|
+
entries.forEach((row, index) => {
|
|
25
|
+
const senders = piCallbackSenders([row], payload);
|
|
26
|
+
if (senders.length === 1 && senders[0] === expectedSender) hits.push({ row, index });
|
|
27
|
+
});
|
|
28
|
+
return hits.length === 1 ? (hits[0] as { row: unknown; index: number }) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PdNativeRow {
|
|
32
|
+
id: number;
|
|
33
|
+
conversation_id: number;
|
|
34
|
+
record: string;
|
|
35
|
+
}
|
|
36
|
+
export interface PdNativeSubmissionRow {
|
|
37
|
+
conversation_id: number;
|
|
38
|
+
status: string;
|
|
39
|
+
record: string;
|
|
40
|
+
}
|
|
41
|
+
export interface PdToolCall {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
arguments: Record<string, unknown>;
|
|
45
|
+
result: { text: string; isError: boolean } | null;
|
|
46
|
+
}
|
|
47
|
+
export interface PdRootView {
|
|
48
|
+
offering: string[];
|
|
49
|
+
models: string[];
|
|
50
|
+
calls: PdToolCall[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Message {
|
|
54
|
+
role?: unknown;
|
|
55
|
+
provider?: unknown;
|
|
56
|
+
model?: unknown;
|
|
57
|
+
content?: unknown;
|
|
58
|
+
toolsAdded?: unknown;
|
|
59
|
+
toolsRemoved?: unknown;
|
|
60
|
+
toolCallId?: unknown;
|
|
61
|
+
isError?: unknown;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function messagesOf(record: Record<string, unknown>): Message[] {
|
|
65
|
+
return Array.isArray(record.model) ? (record.model as Message[]) : [];
|
|
66
|
+
}
|
|
67
|
+
function names(list: unknown): string[] {
|
|
68
|
+
return Array.isArray(list)
|
|
69
|
+
? list.flatMap((t) =>
|
|
70
|
+
t !== null && typeof t === "object" && typeof (t as { name?: unknown }).name === "string"
|
|
71
|
+
? [(t as { name: string }).name]
|
|
72
|
+
: [],
|
|
73
|
+
)
|
|
74
|
+
: [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The root conversation's offering, models and tool calls, from its own entries in id order. */
|
|
78
|
+
export function pdRootView(entries: readonly PdNativeRow[], rootConversationId: number): PdRootView {
|
|
79
|
+
let offering: string[] = [];
|
|
80
|
+
const models = new Set<string>();
|
|
81
|
+
const calls: PdToolCall[] = [];
|
|
82
|
+
const byId = new Map<string, PdToolCall>();
|
|
83
|
+
for (const row of [...entries].sort((a, b) => a.id - b.id)) {
|
|
84
|
+
if (row.conversation_id !== rootConversationId) continue;
|
|
85
|
+
const record = JSON.parse(row.record) as Record<string, unknown>;
|
|
86
|
+
for (const message of messagesOf(record)) {
|
|
87
|
+
if (record.kind === "pi.system") {
|
|
88
|
+
const removed = new Set(names(message.toolsRemoved));
|
|
89
|
+
offering = [...offering.filter((n) => !removed.has(n)), ...names(message.toolsAdded)];
|
|
90
|
+
} else if (record.kind === "pi.assistant" && message.role === "assistant") {
|
|
91
|
+
if (typeof message.provider === "string" && typeof message.model === "string")
|
|
92
|
+
models.add(`${message.provider}/${message.model}`);
|
|
93
|
+
for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
94
|
+
const b = block as { type?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
|
|
95
|
+
if (b.type !== "toolCall" || typeof b.id !== "string" || typeof b.name !== "string") continue;
|
|
96
|
+
const call: PdToolCall = {
|
|
97
|
+
id: b.id,
|
|
98
|
+
name: b.name,
|
|
99
|
+
arguments: (b.arguments ?? {}) as Record<string, unknown>,
|
|
100
|
+
result: null,
|
|
101
|
+
};
|
|
102
|
+
calls.push(call);
|
|
103
|
+
byId.set(b.id, call);
|
|
104
|
+
}
|
|
105
|
+
} else if (
|
|
106
|
+
record.kind === "pi.tool-result" &&
|
|
107
|
+
message.role === "toolResult" &&
|
|
108
|
+
typeof message.toolCallId === "string"
|
|
109
|
+
) {
|
|
110
|
+
const call = byId.get(message.toolCallId);
|
|
111
|
+
if (call === undefined) continue;
|
|
112
|
+
const text = (Array.isArray(message.content) ? message.content : [])
|
|
113
|
+
.map((c) =>
|
|
114
|
+
(c as { type?: unknown; text?: unknown }).type === "text" ? String((c as { text: unknown }).text) : "",
|
|
115
|
+
)
|
|
116
|
+
.join("");
|
|
117
|
+
call.result = { text, isError: message.isError === true };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { offering, models: [...models], calls };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The doorbell submission for exactly this mailbox message, on the root conversation. */
|
|
125
|
+
export function pdDoorbellSubmission(
|
|
126
|
+
submissions: readonly PdNativeSubmissionRow[],
|
|
127
|
+
rootConversationId: number,
|
|
128
|
+
requestId: string,
|
|
129
|
+
): { status: string; record: Record<string, unknown> } | null {
|
|
130
|
+
const hits = submissions
|
|
131
|
+
.map((s) => ({ s, record: JSON.parse(s.record) as Record<string, unknown> }))
|
|
132
|
+
.filter(({ s, record }) => s.conversation_id === rootConversationId && record.requestId === requestId);
|
|
133
|
+
return hits.length === 1
|
|
134
|
+
? {
|
|
135
|
+
status: (hits[0] as { s: PdNativeSubmissionRow }).s.status,
|
|
136
|
+
record: (hits[0] as { record: Record<string, unknown> }).record,
|
|
137
|
+
}
|
|
138
|
+
: null;
|
|
139
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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.2\","],
|
|
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,
|
|
@@ -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"],
|
|
@@ -192,6 +192,17 @@
|
|
|
192
192
|
"signature": "[QK:PIIMPORT-WORK-SURFACE]",
|
|
193
193
|
"signatureSource": "run.sh"
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
"claim": "PIIMPORT-DURABLE-BOOTSTRAP-EXACT",
|
|
197
|
+
"title": "the #129 durable exception loses its file scope: the two experimental durable specifiers become allowed in every file, so a new pi private-subpath import elsewhere passes the floor",
|
|
198
|
+
"subject": "run.sh",
|
|
199
|
+
"find": [" | sed -E \"/^pi\\/pi-durable\\/bootstrap\\.mjs:[0-9]+:/ s#"],
|
|
200
|
+
"replace": [" | sed -E \"s#"],
|
|
201
|
+
"gate": ["bash", "run.sh", "check-pi-import-surface"],
|
|
202
|
+
"timeoutSeconds": 60,
|
|
203
|
+
"signature": "[QK:PIIMPORT-DURABLE-BOOTSTRAP-EXACT]",
|
|
204
|
+
"signatureSource": "run.sh"
|
|
205
|
+
},
|
|
195
206
|
{
|
|
196
207
|
"claim": "VITEST-FAILED-TITLE-ATTRIBUTION",
|
|
197
208
|
"title": "the pilot's kill attribution collects PASSING test titles instead of failed ones, recreating the code-frame false-KILLED hole this lane exists to close",
|
|
@@ -323,8 +334,8 @@
|
|
|
323
334
|
"claim": "FRESHCALL-BACKEND-SET-SURFACE-PARITY",
|
|
324
335
|
"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
336
|
"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\"])"],
|
|
337
|
+
"find": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\", \"codex\", \"pi-durable\"])"],
|
|
338
|
+
"replace": ["\t\t\t.enum([\"pi\", \"claude-code\", \"copilot\", \"omp\", \"pi-durable\"])"],
|
|
328
339
|
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
329
340
|
"timeoutSeconds": 180,
|
|
330
341
|
"signature": "[QK:FRESHCALL-BACKEND-SET-SURFACE-PARITY]",
|
|
@@ -700,6 +711,61 @@
|
|
|
700
711
|
"timeoutSeconds": 120,
|
|
701
712
|
"signature": "[QK:FRESHCALL-CALLBACK-ENV]",
|
|
702
713
|
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
"claim": "FRESHCALL-PI-DURABLE-ARGV",
|
|
717
|
+
"title": "pi-durable loses its explicit task-wide width, so the packaged bootstrap refuses a launch the composition said it built",
|
|
718
|
+
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
719
|
+
"find": ["\t\t\t\t\"--width\",\n\t\t\t\t\"task-wide\",\n"],
|
|
720
|
+
"replace": [""],
|
|
721
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
722
|
+
"timeoutSeconds": 120,
|
|
723
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-ARGV]",
|
|
724
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
"claim": "FRESHCALL-PI-DURABLE-PREFLIGHT-PREMUTATION",
|
|
728
|
+
"title": "the pi-durable runtime/unit preflight is skipped, so an absent or drifted operator runtime is discovered only after a window was placed",
|
|
729
|
+
"subject": "pi-extensions/lib/mux-fresh-call.ts",
|
|
730
|
+
"find": ["\tif (params.backend === \"pi-durable\") {\n"],
|
|
731
|
+
"replace": ["\tif (false) {\n"],
|
|
732
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
733
|
+
"timeoutSeconds": 120,
|
|
734
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-PREFLIGHT-PREMUTATION]",
|
|
735
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
"claim": "FRESHCALL-PI-DURABLE-MANAGED-RUNTIME",
|
|
739
|
+
"title": "pi-durable opens the bare app instead of the managed `entwurf pi-durable`, so nothing verifies the operator runtime against the pin before its resolver runs",
|
|
740
|
+
"subject": "pi-extensions/lib/mux-fresh-call.ts",
|
|
741
|
+
"find": ["\t\"pi-durable\": \"entwurf\","],
|
|
742
|
+
"replace": ["\t\"pi-durable\": \"pi\","],
|
|
743
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
744
|
+
"timeoutSeconds": 120,
|
|
745
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-MANAGED-RUNTIME]",
|
|
746
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
"claim": "FRESHCALL-PI-DURABLE-CALLBACK-DIALECT",
|
|
750
|
+
"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",
|
|
751
|
+
"subject": "pi-extensions/lib/fresh-call-composition.ts",
|
|
752
|
+
"find": ["\t\"pi-durable\": \"entwurf_callback\","],
|
|
753
|
+
"replace": ["\t\"pi-durable\": \"mcp__entwurf-bridge__entwurf_callback\","],
|
|
754
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
755
|
+
"timeoutSeconds": 120,
|
|
756
|
+
"signature": "[QK:FRESHCALL-PI-DURABLE-CALLBACK-DIALECT]",
|
|
757
|
+
"signatureSource": "test/mux-fresh-call.test.ts"
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"claim": "PD-FRESH-LIVE-FOREIGN-SENDER-REFUSED",
|
|
761
|
+
"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",
|
|
762
|
+
"subject": "scripts/lib/pi-durable-fresh-live-receipts.ts",
|
|
763
|
+
"find": ["\t\tif (senders.length === 1 && senders[0] === expectedSender) hits.push({ row, index });"],
|
|
764
|
+
"replace": ["\t\tif (senders.length === 1) hits.push({ row, index });"],
|
|
765
|
+
"gate": ["bash", "run.sh", "check-mux-fresh-call"],
|
|
766
|
+
"timeoutSeconds": 120,
|
|
767
|
+
"signature": "[QK:PD-FRESH-LIVE-FOREIGN-SENDER-REFUSED]",
|
|
768
|
+
"signatureSource": "test/pi-durable-fresh-live-receipts.test.ts"
|
|
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\\.2(_|$)' || 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\\.2(_|$)' || true"],
|
|
21
|
+
"replace": [" grep '^@earendil-works+pi-' | grep -Ev '@1\\.0\\.2(_|$)' || 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]",
|