@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entwurf-v2-contract — the FROZEN contract surface for the unified `entwurf_v2`
|
|
3
|
+
* verb (0.11 Stage 0 step 4-pre / 동결결정 10). PURE: TypeBox schemas + the
|
|
4
|
+
* intent×liveness decision table + the reject taxonomy + a pure resolver.
|
|
5
|
+
* NO runtime dispatch, NO spawn/send, NO I/O — step 5 wires this to transports.
|
|
6
|
+
*
|
|
7
|
+
* Why a frozen contract BEFORE the fact-provider (step 4): with the legacy
|
|
8
|
+
* 3-verb surface (`entwurf`/`entwurf_resume`/`entwurf_send`) still live, building
|
|
9
|
+
* discovery first bakes verb-routing into the fact layer and `entwurf_peers`
|
|
10
|
+
* goes wrong (동결결정 10 순서 근거). So the SHAPE is locked here; the facts read
|
|
11
|
+
* it; dispatch computes from facts at call time (step 5). The legacy 3-verb
|
|
12
|
+
* surface is untouched — this is purely additive (동결결정 10 scope A).
|
|
13
|
+
*
|
|
14
|
+
* Source-verified invariants folded in (Opus 실측 + GPT 보정 + Fable R1-R5, 2026-06-11):
|
|
15
|
+
* - F1: caller intent is DECLARED in the input, so the contract a caller
|
|
16
|
+
* receives is deterministic — never computed from liveness at call time.
|
|
17
|
+
* `owned-outcome` (caller owns completion) ≠ `fire-and-forget` (ack only).
|
|
18
|
+
* - R1: the liveness predicate is defined PER-BACKEND. Only pi (direct-inject,
|
|
19
|
+
* control-socket) has one initially; claude-code is self-fetch with no socket,
|
|
20
|
+
* so its liveness is `unsupported`, NOT folded into dead/indeterminate — that
|
|
21
|
+
* fold is the identity-split trap. `unsupported` is a 4th FACT value, not a
|
|
22
|
+
* 4th dispatch column: an out-of-domain backend rejects before the table.
|
|
23
|
+
* - R2: `target` is the garden-id of an EXISTING citizen. spawn-new is out of
|
|
24
|
+
* v2 scope (legacy `entwurf` keeps it; additive later). Absent/typo gid =
|
|
25
|
+
* `bad-target` (so F6 "오타 gid가 신규 spawn 사고 막기" holds automatically).
|
|
26
|
+
* - N1/F3: an `indeterminate` target never spawns. N2: `fire-and-forget` to a
|
|
27
|
+
* `dormant` target is "reject for now" (mailbox-wake lacks a reply-correlation
|
|
28
|
+
* id in the substrate; an additive extension later, not a permanent no).
|
|
29
|
+
* - Q2: every cell is a SINGLE verdict — no "default", no escape hatch (a
|
|
30
|
+
* "default reject" would re-admit the call-time nondeterminism F1 closes).
|
|
31
|
+
* - F-mailbox: a `fire-and-forget` to an `unsupported` citizen (claude-code etc.)
|
|
32
|
+
* is NOT a reject — the 0.10.0 meta-bridge mailbox delivers without liveness.
|
|
33
|
+
* `unsupported` is the "no liveness predicate" fact, not a delivery verdict; so
|
|
34
|
+
* ff+unsupported routes to the `meta-mailbox` transport, gated by a SEPARATE
|
|
35
|
+
* `mailboxDeliverable` fact (NOT a column of the 6-cell table — Fable (i)).
|
|
36
|
+
* owned-outcome+unsupported still rejects (self-fetch needs real liveness).
|
|
37
|
+
*
|
|
38
|
+
* The decision table here is a constant; `check-entwurf-v2-contract` asserts it
|
|
39
|
+
* exhaustively + proves the "table cell ↔ receipt" round-trip. THAT round-trip
|
|
40
|
+
* is the machine proof of F6 "결정표가 코드로 강제됨" — the executable contract,
|
|
41
|
+
* not prose.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
45
|
+
import { SESSION_ID_RE } from "./session-id.js";
|
|
46
|
+
import type { SocketLiveness } from "./socket-probe.ts";
|
|
47
|
+
|
|
48
|
+
// ── Caller-declared intent (F1) ────────────────────────────────────────────
|
|
49
|
+
// The outcome contract is an INPUT, not an inference. `fire-and-forget` = the
|
|
50
|
+
// RPC ack is the end of the contract (entwurf-control.ts:29-37). `owned-outcome`
|
|
51
|
+
// = the caller owns the dispatched session's completion.
|
|
52
|
+
export const ENTWURF_INTENTS = ["fire-and-forget", "owned-outcome"] as const;
|
|
53
|
+
export type EntwurfIntent = (typeof ENTWURF_INTENTS)[number];
|
|
54
|
+
|
|
55
|
+
// ── Liveness axes ──────────────────────────────────────────────────────────
|
|
56
|
+
// FactLiveness (R1/R3b) = what `entwurf_peers` exposes: the 3 socket-probe
|
|
57
|
+
// values PLUS `unsupported` (predicate undefined for this backend). Four values.
|
|
58
|
+
export const FACT_LIVENESSES = ["alive", "dead", "indeterminate", "unsupported"] as const;
|
|
59
|
+
export type FactLiveness = SocketLiveness | "unsupported";
|
|
60
|
+
|
|
61
|
+
// DispatchLiveness = the in-domain routing axis the table is keyed on. The
|
|
62
|
+
// socket result maps: alive→live (send), dead→dormant (resume from disk),
|
|
63
|
+
// indeterminate→indeterminate (never spawn). `unsupported` is NOT here — it is
|
|
64
|
+
// handled by the domain guard before the table is consulted.
|
|
65
|
+
export const DISPATCH_LIVENESSES = ["live", "dormant", "indeterminate"] as const;
|
|
66
|
+
export type DispatchLiveness = (typeof DISPATCH_LIVENESSES)[number];
|
|
67
|
+
|
|
68
|
+
// ── Backend liveness domain (R1 + F4) ──────────────────────────────────────
|
|
69
|
+
// Backends whose liveness predicate is DEFINED. Initial = pi only (control-socket
|
|
70
|
+
// connect + RPC `get_info`, entwurf-control.ts). claude-code (self-fetch, no
|
|
71
|
+
// socket) and codex/antigravity (direct-inject without a probe surface yet) are
|
|
72
|
+
// OUT of domain → `unsupported`. Widening this set is a deliberate future
|
|
73
|
+
// decision (Stage 1+), gated by a REAL liveness predicate for that backend —
|
|
74
|
+
// never by silently mapping its sessions to dead/indeterminate (R1 핵심).
|
|
75
|
+
export const LIVENESS_DOMAIN_BACKENDS = ["pi"] as const;
|
|
76
|
+
export type LivenessDomainBackend = (typeof LIVENESS_DOMAIN_BACKENDS)[number];
|
|
77
|
+
|
|
78
|
+
export function isLivenessSupported(backend: string): boolean {
|
|
79
|
+
return (LIVENESS_DOMAIN_BACKENDS as readonly string[]).includes(backend);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compose the 4-value FACT liveness from a backend and its socket probe.
|
|
84
|
+
* Out-of-domain backend → `unsupported` (NOT dead/indeterminate, R1). An
|
|
85
|
+
* in-domain backend with no probe result yet → `indeterminate` (no proof → the
|
|
86
|
+
* table will refuse to spawn; we never coerce absence of proof into `dead`).
|
|
87
|
+
*/
|
|
88
|
+
export function factLivenessOf(backend: string, socket: SocketLiveness | null): FactLiveness {
|
|
89
|
+
if (!isLivenessSupported(backend)) return "unsupported";
|
|
90
|
+
return socket ?? "indeterminate";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Map an in-domain socket-probe result to the table's routing axis. */
|
|
94
|
+
export function dispatchLivenessOf(socket: SocketLiveness): DispatchLiveness {
|
|
95
|
+
return socket === "alive" ? "live" : socket === "dead" ? "dormant" : "indeterminate";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Reject taxonomy (R5) ───────────────────────────────────────────────────
|
|
99
|
+
// SCOPE: these are PRE-DISPATCH reject reasons — decided before any transport is
|
|
100
|
+
// attempted. A post-dispatch "send-fail fallback" (transport failed after the
|
|
101
|
+
// verdict) is a SEPARATE axis (bucket B) and must NOT be merged into this enum.
|
|
102
|
+
export const ENTWURF_V2_REJECT_REASONS = [
|
|
103
|
+
"indeterminate-no-spawn", // N1/F3: never spawn an indeterminate target
|
|
104
|
+
"dormant-fire-forget-unsupported", // N2: fire-and-forget to a dormant target — reject for now
|
|
105
|
+
"owned-live-no-autosend", // Q2/F1: owned-outcome to a live target is not an auto-send
|
|
106
|
+
"backend-liveness-unsupported", // R1: backend has no liveness predicate (e.g. claude-code) — owned-outcome only
|
|
107
|
+
"mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
|
|
108
|
+
"bad-target", // R2: absent/typo garden-id (no existing citizen); spawn-new out of v2 scope
|
|
109
|
+
"untrusted-fail-fast", // 동결결정 5: controlled launch into an untrusted cwd
|
|
110
|
+
"socket-only-no-resume-authority", // A1: a record-less socket-only endpoint resolved to a resume verdict (owned-outcome × dormant), but spawn-bg cannot open into it — no trusted cwd/resume authority. Post-probe guard reject (NOT pre-probe, NOT a table resolver cell): the in-domain probe ran and measured the liveness, then `allowResume:false` refused the resume. Carries the honest measured FactLiveness (non-null), unlike the pre-probe `bad-target` it replaces here — a live/addressable socket-only citizen must NEVER be mislabeled absent.
|
|
111
|
+
"target-locked", // R5 pre-claim for bucket B F2 per-gid lockfile conflict
|
|
112
|
+
"target-address-conflict", // F3: a quarantined citizen (garden-id-socket-conflict / symlinked socket) — the gid resolves to two different receivers (record vs socket), so dispatch refuses to pick. The ONLY in-band honest channel for a dispatch-level identity-split (the listing diagnostic channel is not visible to a v2 caller, who only gets a receipt). Pre-resolver, like bad-target/target-locked — NOT a RESOLVER_REJECT_REASONS member.
|
|
113
|
+
] as const;
|
|
114
|
+
export type EntwurfV2RejectReason = (typeof ENTWURF_V2_REJECT_REASONS)[number];
|
|
115
|
+
|
|
116
|
+
// ── Pre-probe reject reasons (?6 — observedLiveness = null) ────────────────
|
|
117
|
+
// These three rejects are decided BEFORE any liveness probe runs, so there is no
|
|
118
|
+
// honest 4-value FactLiveness to stamp: `bad-target` (no citizen/backend),
|
|
119
|
+
// `target-locked` (5a lock conflict, before lstat/connect), `target-address-conflict`
|
|
120
|
+
// (address-subject conflict → probing is forbidden). `indeterminate` means an
|
|
121
|
+
// in-domain probe was inconclusive (≠ "not looked yet"); `unsupported` means the
|
|
122
|
+
// backend has no predicate (≠ "pre-probe"). So a pre-probe reject's
|
|
123
|
+
// observedLiveness is `null`, NOT one of the four values. Every OTHER reject —
|
|
124
|
+
// the RESOLVER_REJECT_REASONS (5, post-probe) plus `untrusted-fail-fast` (1B: it
|
|
125
|
+
// now runs AFTER the lock+probe, only on a resume verdict, so its observedLiveness
|
|
126
|
+
// is the honest measured `dormant`) — carries a non-null FactLiveness, as does
|
|
127
|
+
// every success. This null/non-null split is REASON-DEPENDENT, so the receipt
|
|
128
|
+
// schema (which allows null on every reject branch) cannot enforce it alone — the
|
|
129
|
+
// semantic fixture in `check-entwurf-v2-contract` does, via `isPreProbeReject` /
|
|
130
|
+
// `rejectObservedLivenessWellFormed` below (the SSOT 5b mints against).
|
|
131
|
+
export const PRE_PROBE_REJECT_REASONS = [
|
|
132
|
+
"bad-target",
|
|
133
|
+
"target-locked",
|
|
134
|
+
"target-address-conflict",
|
|
135
|
+
] as const satisfies readonly EntwurfV2RejectReason[];
|
|
136
|
+
export type PreProbeRejectReason = (typeof PRE_PROBE_REJECT_REASONS)[number];
|
|
137
|
+
|
|
138
|
+
export function isPreProbeReject(reason: EntwurfV2RejectReason): reason is PreProbeRejectReason {
|
|
139
|
+
return (PRE_PROBE_REJECT_REASONS as readonly string[]).includes(reason);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The ?6 well-formedness rule for a reject receipt's `observedLiveness`, made a
|
|
144
|
+
* pure SSOT predicate so 5b mints against it and the gate proves it: a pre-probe
|
|
145
|
+
* reject MUST carry `null`; every other reject MUST carry a non-null FactLiveness.
|
|
146
|
+
* Catches the illegal `{ok:false, reason:"bad-target", observedLiveness:"indeterminate"}`
|
|
147
|
+
* (pre-probe with a stamped value) and `{ok:false, reason:"owned-live-no-autosend",
|
|
148
|
+
* observedLiveness:null}` (post-probe with no value) — both reason-dependent, so
|
|
149
|
+
* unreachable by the schema's blanket `FactLiveness | null`.
|
|
150
|
+
*
|
|
151
|
+
* NOTE this predicate FREEZES the 1B ordering into the contract: classifying
|
|
152
|
+
* `untrusted-fail-fast` as post-probe (non-null required) encodes "preflight runs
|
|
153
|
+
* AFTER the probe". Moving preflight back ahead of the probe would make its
|
|
154
|
+
* observedLiveness un-measured (null) and reopen this predicate + the enum split.
|
|
155
|
+
*/
|
|
156
|
+
export function rejectObservedLivenessWellFormed(
|
|
157
|
+
reason: EntwurfV2RejectReason,
|
|
158
|
+
observedLiveness: FactLiveness | null,
|
|
159
|
+
): boolean {
|
|
160
|
+
return isPreProbeReject(reason) ? observedLiveness === null : observedLiveness !== null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reasons the RESOLVER emits — the in-domain 6-cell table cells PLUS the
|
|
164
|
+
// unsupported domain-guard mini-table (backend-liveness-unsupported for
|
|
165
|
+
// owned-outcome, mailbox-undeliverable for a fail-closed fire-and-forget). NOT
|
|
166
|
+
// just the 6-cell table (the F-mailbox mini-table emits two of these), hence
|
|
167
|
+
// RESOLVER_ not TABLE_. The remaining taxonomy members are produced by stages
|
|
168
|
+
// OTHER than the resolver: `bad-target` (target resolution) and `target-locked`
|
|
169
|
+
// (lockfile) run BEFORE the resolver, while `untrusted-fail-fast` is decided
|
|
170
|
+
// AFTER it — preflight runs only behind a resume verdict (1B), so it is a LATER
|
|
171
|
+
// stage, not an earlier one. All three are pre-claimed in the enum so bucket B
|
|
172
|
+
// does not reopen it.
|
|
173
|
+
export const RESOLVER_REJECT_REASONS = [
|
|
174
|
+
"indeterminate-no-spawn",
|
|
175
|
+
"dormant-fire-forget-unsupported",
|
|
176
|
+
"owned-live-no-autosend",
|
|
177
|
+
"backend-liveness-unsupported",
|
|
178
|
+
"mailbox-undeliverable",
|
|
179
|
+
] as const satisfies readonly EntwurfV2RejectReason[];
|
|
180
|
+
|
|
181
|
+
// ── Transport + verdict ────────────────────────────────────────────────────
|
|
182
|
+
// `meta-mailbox` (F-mailbox) = liveness-free delivery via the 0.10.0 meta-bridge
|
|
183
|
+
// mailbox + doorbell. The ack is "enqueued + doorbell rung", NOT a read and NOT a
|
|
184
|
+
// turn injection — so `mode` (steer/follow_up) is meaningless on this transport.
|
|
185
|
+
export const ENTWURF_V2_TRANSPORTS = ["control-socket", "spawn-bg", "tmux-live", "meta-mailbox"] as const;
|
|
186
|
+
export type EntwurfV2Transport = (typeof ENTWURF_V2_TRANSPORTS)[number];
|
|
187
|
+
|
|
188
|
+
// Allow-branch facets (exported so the schema↔types gate asserts every enum).
|
|
189
|
+
export const ENTWURF_V2_ACTIONS = ["send", "resume"] as const;
|
|
190
|
+
export const ENTWURF_V2_OWNERSHIPS = ["ack-only", "owned"] as const;
|
|
191
|
+
// Delivery mode of the message to the target (how it is injected) — steer =
|
|
192
|
+
// interrupt the current turn, follow_up = queue after it. A SEPARATE axis from
|
|
193
|
+
// both the intent/ownership axis (F1) and the liveness-routing axis; the legacy
|
|
194
|
+
// entwurf_send carries the same steer|follow_up surface.
|
|
195
|
+
export const ENTWURF_V2_MODES = ["steer", "follow_up"] as const;
|
|
196
|
+
|
|
197
|
+
export type DispatchVerdict =
|
|
198
|
+
| { action: "send"; transport: "control-socket" | "meta-mailbox"; ownership: "ack-only" }
|
|
199
|
+
| { action: "resume"; transport: "spawn-bg" | "tmux-live"; ownership: "owned" }
|
|
200
|
+
| { action: "reject"; reason: EntwurfV2RejectReason };
|
|
201
|
+
|
|
202
|
+
// ── The FROZEN decision table ──────────────────────────────────────────────
|
|
203
|
+
// intent × dispatch-liveness → exactly one verdict (Q2). v2-initial ALLOWS
|
|
204
|
+
// exactly two cells (fire-and-forget+live = send; owned-outcome+dormant =
|
|
205
|
+
// resume); the other four reject. The reject cells are honest "지금은 없음"
|
|
206
|
+
// locks (N2) — the legacy 3-verb surface still covers those flows unchanged.
|
|
207
|
+
export const DISPATCH_TABLE: Record<EntwurfIntent, Record<DispatchLiveness, DispatchVerdict>> = {
|
|
208
|
+
"fire-and-forget": {
|
|
209
|
+
live: { action: "send", transport: "control-socket", ownership: "ack-only" },
|
|
210
|
+
dormant: { action: "reject", reason: "dormant-fire-forget-unsupported" },
|
|
211
|
+
indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
|
|
212
|
+
},
|
|
213
|
+
"owned-outcome": {
|
|
214
|
+
// wants_reply is etiquette, not ownership — owned+live never auto-sends (Q2/F1).
|
|
215
|
+
live: { action: "reject", reason: "owned-live-no-autosend" },
|
|
216
|
+
dormant: { action: "resume", transport: "spawn-bg", ownership: "owned" },
|
|
217
|
+
indeterminate: { action: "reject", reason: "indeterminate-no-spawn" },
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// ── The unsupported-backend mailbox mini-table (F-mailbox) ─────────────────
|
|
222
|
+
// SEPARATE from the in-domain 6-cell DISPATCH_TABLE (Fable (i)): an `unsupported`
|
|
223
|
+
// backend (claude-code self-fetch, codex/agy without a probe surface) has NO
|
|
224
|
+
// liveness predicate, so it never enters the liveness-keyed table. Instead the
|
|
225
|
+
// domain guard routes it here, keyed on intent alone:
|
|
226
|
+
// - fire-and-forget needs no liveness — the 0.10.0 meta-bridge mailbox delivers
|
|
227
|
+
// to any DELIVERABLE citizen. This cell is the deliverable path; resolveDispatch
|
|
228
|
+
// downgrades it to `mailbox-undeliverable` when the separate mailboxDeliverable
|
|
229
|
+
// fact is false (fail-closed). The ack is enqueue+doorbell, NOT read, and
|
|
230
|
+
// observedLiveness stays `unsupported` — the receipt's `meta-mailbox` transport
|
|
231
|
+
// is what says "this went to the mailbox".
|
|
232
|
+
// - owned-outcome has no real liveness to own on a self-fetch backend → reject.
|
|
233
|
+
//
|
|
234
|
+
// N2 asymmetry (명문화 — without this the two tables read as contradictory):
|
|
235
|
+
// fire-and-forget+dormant-PI = reject vs fire-and-forget+unsupported-CITIZEN = mailbox.
|
|
236
|
+
// In-domain `dormant` is a CONFIRMED not-running pi, so enqueuing would be a
|
|
237
|
+
// silent pileup (resume is the honest place). `unsupported` is UNKNOWN liveness
|
|
238
|
+
// on a backend we cannot probe, so a best-effort mailbox doorbell is the most we
|
|
239
|
+
// can honestly offer — there is nothing to resume into.
|
|
240
|
+
export const UNSUPPORTED_DISPATCH_TABLE: Record<EntwurfIntent, DispatchVerdict> = {
|
|
241
|
+
"fire-and-forget": { action: "send", transport: "meta-mailbox", ownership: "ack-only" },
|
|
242
|
+
"owned-outcome": { action: "reject", reason: "backend-liveness-unsupported" },
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// ── Dispatch receipt (R3) ──────────────────────────────────────────────────
|
|
246
|
+
// Carries `observedLiveness` + the transport/action so `check-entwurf-v2-contract`
|
|
247
|
+
// can assert a "table cell ↔ receipt" round-trip — the machine proof of F6.
|
|
248
|
+
export type EntwurfV2Receipt =
|
|
249
|
+
| {
|
|
250
|
+
ok: true;
|
|
251
|
+
action: "send" | "resume";
|
|
252
|
+
transport: EntwurfV2Transport;
|
|
253
|
+
ownership: "ack-only" | "owned";
|
|
254
|
+
observedLiveness: FactLiveness;
|
|
255
|
+
}
|
|
256
|
+
// observedLiveness is `FactLiveness | null` (?6): null for the pre-probe
|
|
257
|
+
// rejects (PRE_PROBE_REJECT_REASONS — no honest value to stamp before a probe),
|
|
258
|
+
// non-null for every other reject. The split is reason-dependent, enforced by
|
|
259
|
+
// `rejectObservedLivenessWellFormed`, not by this union alone.
|
|
260
|
+
| { ok: false; reason: EntwurfV2RejectReason; observedLiveness: FactLiveness | null };
|
|
261
|
+
|
|
262
|
+
// The reject branch on its own — `makeRejectReceipt` returns exactly this (NOT the
|
|
263
|
+
// widened `EntwurfV2Receipt`), so a consumer that mints a reject keeps the precise
|
|
264
|
+
// type without a cast (the 5b decider's `DispatchDecision` reject branch carries it
|
|
265
|
+
// directly). A type-only precision alias over the union above; discriminant unchanged.
|
|
266
|
+
export type EntwurfV2RejectReceipt = Extract<EntwurfV2Receipt, { ok: false }>;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The ONLY sanctioned way to mint a reject receipt (?6 enforcement). A pure
|
|
270
|
+
* predicate (`rejectObservedLivenessWellFormed`) cannot force a caller to consult
|
|
271
|
+
* it — 5b could hand-assemble `{ok:false, reason:"bad-target",
|
|
272
|
+
* observedLiveness:"indeterminate"}`, which the blanket `FactLiveness | null`
|
|
273
|
+
* schema accepts. This constructor THROWS on a well-formedness violation, so
|
|
274
|
+
* every reject path (resolveDispatch's own mints below + the 5b stages that
|
|
275
|
+
* produce bad-target / target-locked / target-address-conflict / untrusted-
|
|
276
|
+
* fail-fast) routes through one chokepoint and the bypass surface is zero. 5b
|
|
277
|
+
* MUST build rejects with this, never by object literal.
|
|
278
|
+
*/
|
|
279
|
+
export function makeRejectReceipt(
|
|
280
|
+
reason: EntwurfV2RejectReason,
|
|
281
|
+
observedLiveness: FactLiveness | null,
|
|
282
|
+
): EntwurfV2RejectReceipt {
|
|
283
|
+
if (!rejectObservedLivenessWellFormed(reason, observedLiveness)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`entwurf_v2: ill-formed reject receipt — reason '${reason}' requires ${
|
|
286
|
+
isPreProbeReject(reason) ? "observedLiveness=null (pre-probe)" : "a non-null observedLiveness (post-probe)"
|
|
287
|
+
}, got ${JSON.stringify(observedLiveness)}.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
return { ok: false, reason, observedLiveness };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* PURE dispatch decision over already-resolved facts. Before reaching here the
|
|
295
|
+
* caller has resolved the target (→ `bad-target` if no existing citizen) and, for
|
|
296
|
+
* an in-domain backend, acquired the per-gid lock (→ `target-locked`) and probed
|
|
297
|
+
* liveness UNDER that lock. This function only decides the liveness-routed
|
|
298
|
+
* verdict. preflight (→ `untrusted-fail-fast`) is NOT a precondition here: per 1B
|
|
299
|
+
* it runs only AFTER this resolver returns a resume verdict (the sole branch that
|
|
300
|
+
* launches a child into a target cwd), so a send/mailbox verdict never touches it.
|
|
301
|
+
* Do NOT reintroduce a global pre-resolver preflight — that re-breaks F-mailbox.
|
|
302
|
+
*
|
|
303
|
+
* Two facts in: `liveness` (the 4-value FactLiveness) and `mailboxDeliverable`
|
|
304
|
+
* (F-mailbox — a SEPARATE axis from liveness, NOT a column of either table, NOT
|
|
305
|
+
* an entwurf_peers row field; step 5's target/capability/presence layer supplies it
|
|
306
|
+
* via the required mailboxDeliverabilityFor seam — wake-mode capability AND a live
|
|
307
|
+
* active-receiver (SE-2 2d-3) — and unknown deliverability MUST be passed as false =
|
|
308
|
+
* fail-closed). The deliverable
|
|
309
|
+
* fact is consulted ONLY on the `unsupported` mailbox path; for an in-domain (pi)
|
|
310
|
+
* backend the liveness-routed table is authoritative and the flag is ignored.
|
|
311
|
+
*
|
|
312
|
+
* R1 domain guard runs first: an `unsupported` liveness is routed through the
|
|
313
|
+
* UNSUPPORTED_DISPATCH_TABLE (mailbox mini-table), never the 6-cell table.
|
|
314
|
+
* No spawn, no send, no I/O — step 5 executes the chosen transport.
|
|
315
|
+
*/
|
|
316
|
+
export function resolveDispatch(
|
|
317
|
+
intent: EntwurfIntent,
|
|
318
|
+
liveness: FactLiveness,
|
|
319
|
+
mailboxDeliverable: boolean,
|
|
320
|
+
): EntwurfV2Receipt {
|
|
321
|
+
if (liveness === "unsupported") {
|
|
322
|
+
// R1 domain guard → the mailbox mini-table (intent-keyed), NOT the 6-cell table.
|
|
323
|
+
const mboxCell = UNSUPPORTED_DISPATCH_TABLE[intent];
|
|
324
|
+
if (mboxCell.action === "reject") {
|
|
325
|
+
return makeRejectReceipt(mboxCell.reason, liveness);
|
|
326
|
+
}
|
|
327
|
+
// fire-and-forget allow cell, gated by the separate deliverability fact.
|
|
328
|
+
if (!mailboxDeliverable) {
|
|
329
|
+
return makeRejectReceipt("mailbox-undeliverable", liveness);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
action: mboxCell.action,
|
|
334
|
+
transport: mboxCell.transport,
|
|
335
|
+
ownership: mboxCell.ownership,
|
|
336
|
+
observedLiveness: liveness,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// liveness is now narrowed to SocketLiveness; deliverability does not apply.
|
|
340
|
+
const cell = DISPATCH_TABLE[intent][dispatchLivenessOf(liveness)];
|
|
341
|
+
if (cell.action === "reject") {
|
|
342
|
+
return makeRejectReceipt(cell.reason, liveness);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
action: cell.action,
|
|
347
|
+
transport: cell.transport,
|
|
348
|
+
ownership: cell.ownership,
|
|
349
|
+
observedLiveness: liveness,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ── TypeBox schemas (for step 5 MCP tool params + the gate's structural assert) ──
|
|
354
|
+
// StringEnum (typebox 1.x) inside Type.Object (typebox 0.34) — same mix the
|
|
355
|
+
// existing entwurf tools use (entwurf-control.ts:92-95). The logic types above
|
|
356
|
+
// are hand-written unions, NOT `Static<>` inferences, so the 0.34/1.x widening
|
|
357
|
+
// caveat does not touch them; the gate keeps schema ↔ types in lockstep.
|
|
358
|
+
export const EntwurfV2InputSchema = Type.Object(
|
|
359
|
+
{
|
|
360
|
+
// R2/F6 executable: the garden-id shape is enforced by pattern, not prose —
|
|
361
|
+
// a malformed/typo gid fails the schema (→ bad-target) and can never reach a
|
|
362
|
+
// spawn. SSOT regex = SESSION_ID_RE (pi-extensions/lib/session-id.js).
|
|
363
|
+
target: Type.String({
|
|
364
|
+
pattern: SESSION_ID_RE.source,
|
|
365
|
+
description:
|
|
366
|
+
"garden-id of an EXISTING citizen (pattern-enforced). spawn-new is out of v2 scope (legacy entwurf keeps it); a malformed/typo gid is bad-target.",
|
|
367
|
+
}),
|
|
368
|
+
intent: StringEnum(ENTWURF_INTENTS, {
|
|
369
|
+
description:
|
|
370
|
+
"caller's declared outcome contract (F1): fire-and-forget = ack only, owned-outcome = caller owns completion.",
|
|
371
|
+
}),
|
|
372
|
+
mode: Type.Optional(
|
|
373
|
+
StringEnum(ENTWURF_V2_MODES, {
|
|
374
|
+
description:
|
|
375
|
+
"delivery mode (steer = interrupt current turn, follow_up = queue) — NOT the ownership axis (F1) nor liveness routing. MEANINGLESS on the meta-mailbox transport (F-mailbox): a mailbox ack is enqueue+doorbell, not a turn injection, so steer/follow_up does not apply when the verdict transport is meta-mailbox.",
|
|
376
|
+
}),
|
|
377
|
+
),
|
|
378
|
+
wantsReply: Type.Optional(
|
|
379
|
+
Type.Boolean({
|
|
380
|
+
description: "conversation etiquette only — NOT ownership; never triggers an auto-send (Q2).",
|
|
381
|
+
}),
|
|
382
|
+
),
|
|
383
|
+
// `additionalProperties: false` — a frozen contract input is exact; an unknown
|
|
384
|
+
// key is a caller error, not silently ignored.
|
|
385
|
+
},
|
|
386
|
+
{ additionalProperties: false },
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Receipt = a DISCRIMINATED union on `ok` (R3/F6) — NOT one flat object with
|
|
390
|
+
// optionals. Each branch is EXACT (`additionalProperties: false`): without it,
|
|
391
|
+
// JSON Schema's default admits extra keys, so an illegal receipt like
|
|
392
|
+
// {ok:true, ..., reason:"bad-target"} would validate against the success branch.
|
|
393
|
+
// With it, success carries action/transport/ownership and rejects a stray reason;
|
|
394
|
+
// reject carries reason and rejects any allow facet — the branches are mutually
|
|
395
|
+
// exclusive at the schema level, not merely by declared-property convention.
|
|
396
|
+
export const EntwurfV2ReceiptSuccessSchema = Type.Object(
|
|
397
|
+
{
|
|
398
|
+
ok: Type.Literal(true),
|
|
399
|
+
action: StringEnum(ENTWURF_V2_ACTIONS),
|
|
400
|
+
transport: StringEnum(ENTWURF_V2_TRANSPORTS),
|
|
401
|
+
ownership: StringEnum(ENTWURF_V2_OWNERSHIPS),
|
|
402
|
+
observedLiveness: StringEnum(FACT_LIVENESSES, {
|
|
403
|
+
description: "the 4-value fact liveness the verdict was computed from (R1/R3).",
|
|
404
|
+
}),
|
|
405
|
+
},
|
|
406
|
+
{ additionalProperties: false },
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
export const EntwurfV2ReceiptRejectSchema = Type.Object(
|
|
410
|
+
{
|
|
411
|
+
ok: Type.Literal(false),
|
|
412
|
+
reason: StringEnum(ENTWURF_V2_REJECT_REASONS),
|
|
413
|
+
// ?6: required-nullable, NOT optional — a reject branch ALWAYS carries the
|
|
414
|
+
// key, and it is `null` for the pre-probe rejects (PRE_PROBE_REJECT_REASONS)
|
|
415
|
+
// and a real FactLiveness otherwise. Optional would lose the "key always
|
|
416
|
+
// present, value may be null" shape and weaken the discriminated union; the
|
|
417
|
+
// reason-dependent null/non-null rule is enforced semantically (the gate's
|
|
418
|
+
// rejectObservedLivenessWellFormed fixture), not by this blanket union.
|
|
419
|
+
observedLiveness: Type.Union([StringEnum(FACT_LIVENESSES), Type.Null()], {
|
|
420
|
+
description:
|
|
421
|
+
"the 4-value fact liveness the reject was computed from (R1/R3); null for the pre-probe rejects (bad-target / target-locked / target-address-conflict) where no probe ran.",
|
|
422
|
+
}),
|
|
423
|
+
},
|
|
424
|
+
{ additionalProperties: false },
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
export const EntwurfV2ReceiptSchema = Type.Union([EntwurfV2ReceiptSuccessSchema, EntwurfV2ReceiptRejectSchema]);
|