@extrovert.dev/sdk 0.1.0-pre.5 → 0.1.0-pre.7

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/README.md CHANGED
@@ -4,19 +4,19 @@
4
4
 
5
5
  **A real inbox for your agent, in one call.**
6
6
 
7
- The TypeScript SDK for [Extrovert](https://extrovert.dev) Message Science's agent-email
7
+ The TypeScript SDK for [Extrovert](https://extrovert.dev): Message Science's agent-email
8
8
  platform. Extrovert gives an AI agent a real, persistent inbox on a domain we own: created in one
9
9
  call, sends and receives, behind a scoped key that expires and revokes on its own.
10
10
 
11
- - **One call to a live inbox.** `agent7@smtp.extrovert.dev`, sending and receiving, on a
12
- pre-verified `smtp.extrovert.dev` subdomain no DNS, no waiting.
11
+ - **One call to a live inbox.** Paid accounts use `agent7@extrovertmail.com`; free
12
+ signups use `agent7@free.extrovertmail.com`. No DNS setup is required.
13
13
  - **`waitForEmail`, the killer primitive.** Block until the next matching message lands and get the
14
14
  OTP code / verification link extracted as a structured field. No polling loop.
15
15
  - **Typed everything.** Request and response models matching the Extrovert `/v1` contract, a typed
16
16
  `ApiError` hierarchy, full `.d.ts` declarations.
17
17
  - **Runs where your agent runs.** Pure `fetch`. Node 18+, Cloudflare Workers, Vercel Edge, Deno,
18
18
  the browser. Zero runtime dependencies.
19
- - **Don't hand an MCP host your master key.** Redeem a scoped enrollment key; mint an agent key that
19
+ - **Don't hand an MCP host your master key.** Redeem a scoped enrollment key; issue an agent key that
20
20
  expires and revokes on its own.
21
21
 
22
22
  ---
@@ -30,7 +30,7 @@ for a stable release:
30
30
  npm install @extrovert.dev/sdk@next
31
31
  ```
32
32
 
33
- Pin `@extrovert.dev/sdk@0.1.0-pre.5` when a dogfood test needs a reproducible contract snapshot.
33
+ Pin `@extrovert.dev/sdk@0.1.0-pre.7` when a dogfood test needs a reproducible contract snapshot.
34
34
  Requires Node 18+ for global `fetch` and Web Crypto.
35
35
 
36
36
  ## Build and use from source
@@ -48,7 +48,11 @@ REST API and served OpenAPI document remain the underlying contract.
48
48
 
49
49
  ---
50
50
 
51
- ## Quickstart inbox in one call
51
+ ## Quickstart: inbox in one call
52
+
53
+ Use the scoped key issued for this agent on your existing account. Do not sign up for another
54
+ account to connect a new agent. Confirm the connection with `extrovert.whoami()` before creating
55
+ resources; its capability summary explains what the connection can do.
52
56
 
53
57
  ```ts
54
58
  import { Extrovert, isQueuedForReview } from "@extrovert.dev/sdk";
@@ -57,10 +61,10 @@ const extrovert = new Extrovert({ apiKey: process.env.EXTROVERT_API_KEY! });
57
61
 
58
62
  // One call. A real, send-and-receive-capable inbox.
59
63
  const inbox = await extrovert.inboxes.create({ display_name: "Support Bot" });
60
- console.log(inbox.address); // agent7@smtp.extrovert.dev
64
+ console.log(inbox.address); // agent7@extrovertmail.com
61
65
 
62
66
  // Queue a message for a human. `intent.summary` is what the reviewer reads
63
- // first, and it is REQUIRED under the default `require_review` policy a send
67
+ // first, and it is REQUIRED: under the default `require_review` policy a send
64
68
  // without one is refused 422 `intent_required` (nothing sent, nothing queued).
65
69
  const outcome = await inbox.send({
66
70
  to: "ops@acme.test",
@@ -77,17 +81,56 @@ if (isQueuedForReview(outcome)) {
77
81
 
78
82
  **Your agent queues mail; it does not send it.** A message parks as a review request, a human
79
83
  approves / edits / rejects it, and your agent watches for that outcome and redrafts as needed.
80
- `allow_direct` inboxes and graduated categories are the exceptions, not the default read
84
+ `allow_direct` inboxes and graduated categories are the exceptions, not the default: read
81
85
  `inbox.record?.effective_review_policy` once rather than learning the policy by being refused. The
82
86
  other half of the loop is the [agent contract](https://docs.extrovert.dev/review-loop/agent-contract/).
83
87
 
84
- `inboxes.create()` returns an `InboxHandle` an ergonomic handle bound to one address, so the rest
88
+ `inboxes.create()` returns an `InboxHandle`: an ergonomic handle bound to one address, so the rest
85
89
  of your agent code reads naturally: `inbox.send(...)`, `inbox.messages()`, `inbox.waitForEmail(...)`,
86
90
  `inbox.delete()`. This is the curl-style sugar that resolves to your key's default project.
87
91
 
88
92
  ---
89
93
 
90
- ## The canonical chain `projects.inboxes.*`
94
+ ## Check whether your domain is ready
95
+
96
+ ```ts
97
+ const domain = await extrovert.domains.get("mail.example.com");
98
+ const readiness = domain.readiness;
99
+ if (!readiness) throw new Error("This server did not return domain readiness.");
100
+ console.log(readiness.label, readiness.summary);
101
+ console.log(readiness.inboxes); // counts visible to this connection, not a global total
102
+
103
+ if (readiness.ready_for_inboxes && readiness.next_action === "create_inbox") {
104
+ console.log("Ready to create an inbox when you want one.");
105
+ } else if (readiness.action_required_by === "extrovert") {
106
+ console.log("Extrovert is handling the next steps. No DNS changes are needed.");
107
+ }
108
+
109
+ // A deliberate recheck verifies the DNS entries now; ordinary reads do not.
110
+ // Use after publishing/fixing the entries, not in a tight polling loop.
111
+ await extrovert.domains.verify("mail.example.com");
112
+
113
+ // Wait at most 45 seconds. A timeout does not stop background setup.
114
+ const waiting = await extrovert.domains.wait("mail.example.com", { timeout_seconds: 45 });
115
+ if (waiting.outcome === "timed_out") {
116
+ console.log(`Resume checking in ${waiting.resume_after_seconds} seconds.`);
117
+ }
118
+
119
+ // Persist next_cursor privately and pass it as after on the next check.
120
+ const updates = await extrovert.domains.events("mail.example.com", { after: "0", limit: 50 });
121
+ for (const event of updates.items) console.log(event.summary);
122
+ ```
123
+
124
+ `waiting_for_dns` means your DNS entries are not confirmed yet; `setting_up` means Extrovert is
125
+ finishing setup. `ready` means mail setup is complete. `action_required` identifies a customer DNS
126
+ repair; `needs_attention` identifies work for Extrovert. Temporary `checking` results are inconclusive,
127
+ not a request to change DNS. Use the returned summary and next action when answering a person.
128
+
129
+ Do not infer readiness from legacy verification or signing fields. Ready inboxes still follow their
130
+ permissions, account limits, and review rules. Setup continues while your agent is disconnected, but
131
+ the agent must stay connected or resume its event/status checks to receive updates.
132
+
133
+ ## The canonical chain: `projects.inboxes.*`
91
134
 
92
135
  Scope lives in your **key**, not in headers. A broad (org-tier) key narrows to one **project** by
93
136
  path; a project/inbox key is already pinned. The canonical, contract-aligned surface is the
@@ -98,13 +141,13 @@ within-project alias):
98
141
  const x = new Extrovert({ apiKey: process.env.EXTROVERT_API_KEY! });
99
142
  const { project_id } = await x.whoami(); // the key's fixed project
100
143
 
101
- // Create / send / list in a project keyed by the opaque inbox_id.
144
+ // Create / send / list in a project: keyed by the opaque inbox_id.
102
145
  const inbox = await x.projects.inboxes.create(project_id!, { username: "ada" });
103
146
  await x.projects.inboxes.send(project_id!, inbox.id, { to: "ops@acme.test", subject: "hi", text: "…",
104
147
  intent: { summary: "…one sentence for the human reviewer…" } }); // queues for review
105
148
 
106
149
  // One list envelope: { object: "list", data, has_more, next_cursor }. The ListPage
107
- // auto-paginates over OPAQUE cursors never thread a cursor by hand.
150
+ // auto-paginates over OPAQUE cursors: never thread a cursor by hand.
108
151
  const page = await x.projects.inboxes.list(project_id!, { limit: 50 });
109
152
  for await (const ib of page) console.log(ib.id); // walks every page
110
153
  const all = await (await x.projects.inboxes.list(project_id!)).collect(); // eager
@@ -125,7 +168,7 @@ to opt into the server's transform shim.
125
168
 
126
169
  ---
127
170
 
128
- ## The OTP flow `waitForEmail`
171
+ ## The OTP flow: `waitForEmail`
129
172
 
130
173
  Agents sign up for things. The high-value, time-boxed task is "wait for the verification email and
131
174
  read the code." Extrovert holds the request open, polls the mailbox server-side, and hands you the extracted code.
@@ -165,7 +208,7 @@ of real warmup mail for years. Need it standalone? Import `extractOtp` / `extrac
165
208
 
166
209
  ## Try it offline (no API key, no network)
167
210
 
168
- Run the whole SDK against built-in, deterministic fixtures no key, no network. Every method
211
+ Run the whole SDK against built-in, deterministic fixtures: no key, no network. Every method
169
212
  works, including a synthesized `waitForEmail` OTP, and the mock models the real review policy,
170
213
  so a send without an `intent` is refused offline exactly as it would be live:
171
214
 
@@ -173,21 +216,25 @@ so a send without an `intent` is refused offline exactly as it would be live:
173
216
  const extrovert = new Extrovert({ transport: "mock" });
174
217
  const inbox = await extrovert.inboxes.create();
175
218
  const { extracted } = await inbox.waitForEmail();
176
- console.log(extracted.otp); // a fixture OTP no network touched
219
+ console.log(extracted.otp); // a fixture OTP: no network touched
177
220
  ```
178
221
 
179
222
  Set `EXTROVERT_API_BASE_URL=mock` to flip every client into offline mode from the environment.
180
223
 
224
+ Offline domain setup remains `waiting_for_dns`: it does not query DNS, run background setup, or
225
+ produce lifecycle events. Recheck does not fabricate a ready result. Use a test HTTP response for
226
+ readiness transitions and resumable event scenarios; only the live service confirms actual setup.
227
+
181
228
  ---
182
229
 
183
- ## Scoped keys redeem an enrollment key
230
+ ## Scoped keys: redeem an enrollment key
184
231
 
185
- The identity model: a human (or org-admin call) **mints** a scoped `pk_enroll_...` enrollment key
232
+ The identity model: a human (or org-admin call) **issues** a scoped `pk_enroll_...` enrollment key
186
233
  that can create up to *N* inboxes and nothing else. An agent **redeems** it for a short-lived,
187
234
  individually-revocable `pk_agent_...` key.
188
235
 
189
236
  ```ts
190
- // The agent is handed only the enrollment key never an org-wide key.
237
+ // The agent is handed only the enrollment key: never an org-wide key.
191
238
  const bootstrap = new Extrovert({ apiKey: process.env.EXTROVERT_ENROLLMENT_KEY! });
192
239
 
193
240
  const { client, enrollment } = await bootstrap.enrolled({
@@ -200,21 +247,21 @@ const { client, enrollment } = await bootstrap.enrolled({
200
247
  console.log(enrollment.agent_id, enrollment.scopes);
201
248
  console.log(enrollment.org_id, enrollment.project_id); // the key's fixed org + project
202
249
 
203
- // `client` is already authenticated with the minted agent key.
250
+ // `client` is already authenticated with the issued agent key.
204
251
  const inbox = await client.inboxes.create();
205
252
 
206
- // The minted key is bound to a fixed org + project visible via whoami, never selectable.
253
+ // The issued key is bound to a fixed org + project: visible via whoami, never selectable.
207
254
  const me = await client.whoami();
208
255
  console.log(me.org_id, me.project_id, me.scopes);
209
256
  ```
210
257
 
211
258
  ---
212
259
 
213
- ## Inbox metadata attach your own key-value data
260
+ ## Inbox metadata: attach your own key-value data
214
261
 
215
262
  Every inbox carries an arbitrary `metadata` object (string / number / boolean values; ≤256 keys,
216
263
  ≤256 chars per key and per string value). Set it at create time, read it on every inbox shape, and
217
- patch it in place with shallow-merge / null-delete semantics no delete+recreate.
264
+ patch it in place with shallow-merge / null-delete semantics: no delete+recreate.
218
265
 
219
266
  ```ts
220
267
  // Set metadata at create time. It is echoed back on the inbox record.
@@ -235,30 +282,57 @@ const updated = await extrovert.inboxes.update(inbox.address, {
235
282
  console.log(updated.metadata); // { tier: 3, vip: true }
236
283
  ```
237
284
 
238
- Metadata is **project-scoped** a key only reads/mutates inboxes in its bound project. Where the
285
+ Metadata is **project-scoped**: a key only reads/mutates inboxes in its bound project. Where the
239
286
  API accepts a `project_id`, it is an **assertion** that must match the key's bound project (a
240
287
  mismatch is a 403), never a selector: the project is always derived from the key. See `whoami` for
241
288
  the fixed `org_id` / `project_id` the key is bound to.
242
289
 
243
290
  ---
244
291
 
245
- ## Receiving mail read, thread, reply
292
+ ## Receiving mail: read, thread, reply
246
293
 
247
294
  ```ts
248
- // List messages and threads in an inbox.
249
- const { data: messages } = await inbox.messages({ direction: "inbound" });
250
- const { data: threads } = await inbox.threads();
251
-
252
- // Reply in-thread (In-Reply-To / References handled server-side from IMAP).
253
- const first = messages[0];
254
- if (first) {
255
- await extrovert.messages.reply(first.id, { text: "On it — thanks." });
295
+ // Find a conversation. Pass next_cursor back unchanged to continue.
296
+ const firstPage = await extrovert.threads.search(inbox.address, {
297
+ q: "deployment",
298
+ limit: 25,
299
+ });
300
+ const nextPage = firstPage.next_cursor
301
+ ? await extrovert.threads.search(inbox.address, {
302
+ q: "deployment",
303
+ limit: 25,
304
+ cursor: firstPage.next_cursor,
305
+ })
306
+ : undefined;
307
+
308
+ // Read the complete oldest-first conversation before acting.
309
+ const summary = firstPage.items[0];
310
+ if (summary) {
311
+ const thread = await extrovert.threads.get(inbox.address, summary.id);
312
+ const authoredText = thread.messages.map((message) =>
313
+ message.extracted_text ?? message.text
314
+ );
315
+
316
+ // Recipients, subject, In-Reply-To, and References are derived server-side.
317
+ await extrovert.threads.reply(inbox.address, {
318
+ thread_id: thread.id,
319
+ expected_last_message_id: thread.last_message_id,
320
+ text: "On it — thanks.",
321
+ intent: { summary: "Acknowledge the deployment request." },
322
+ idempotency_key: "deployment-ack-v1",
323
+ });
256
324
  }
257
325
  ```
258
326
 
327
+ If the thread advances first, `expected_last_message_id` returns a 409: fetch the thread again and
328
+ reconsider the draft. It is an optimistic check at submission, not an atomic lock through delivery.
329
+
330
+ For an inbox-bound style, use `inbox.threads(...)`, `inbox.searchThreads(...)`,
331
+ `inbox.thread(...)`, `inbox.reply(...)`, and `inbox.deleteThread(...)`.
332
+
259
333
  ---
260
334
 
261
- ## Webhooks verified inbound, anywhere
335
+ ## Webhooks: verified inbound, anywhere
262
336
 
263
337
  Register an HMAC-signed, timestamped webhook, then verify deliveries with Web Crypto (works in Node
264
338
  and at the edge, no dependency):
@@ -272,11 +346,11 @@ const webhook = await extrovert.webhooks.register({
272
346
  url: "https://my-agent.example.com/inbound",
273
347
  events: ["message.received"],
274
348
  });
275
- // Store webhook.secret now it is shown once.
349
+ // Store webhook.secret now: it is shown once.
276
350
 
277
351
  // In your handler (Workers / Vercel Edge / Node):
278
352
  export async function POST(req: Request) {
279
- const payload = await req.text(); // raw body do not re-serialize
353
+ const payload = await req.text(); // raw body: do not re-serialize
280
354
  const ok = await verifyWebhookSignature({
281
355
  payload,
282
356
  signature: req.headers.get("x-extrovert-signature")!,
@@ -308,12 +382,12 @@ new Extrovert({
308
382
 
309
383
  | Option | Env | Default |
310
384
  | ------------ | ----------------------- | ------------------------------------ |
311
- | `apiKey` | `EXTROVERT_API_KEY` | (required for `http` transport) |
385
+ | `apiKey` | `EXTROVERT_API_KEY` |: (required for `http` transport) |
312
386
  | `baseUrl` | `EXTROVERT_API_BASE_URL` | `https://api.extrovert.dev` |
313
387
  | `transport` | (`baseUrl=mock`) | `http` |
314
- | `timeoutMs` | — | `30000` |
388
+ | `timeoutMs` |: | `30000` |
315
389
 
316
- Idempotency: pass `client_id` to `inboxes.create()` and `idempotency_key` to `send` / `reply`
390
+ Idempotency: pass `client_id` to `inboxes.create()` and `idempotency_key` to `send` / `reply` :
317
391
  retries won't duplicate. Cursor pagination: list responses carry `next_cursor`; pass it back as
318
392
  `cursor`.
319
393
 
@@ -326,15 +400,15 @@ Every non-2xx response throws a typed error extending `ApiError`. Branch on the
326
400
  ```ts
327
401
  import {
328
402
  ApiError,
329
- AuthenticationError, // 401 key missing / expired / revoked
330
- PermissionError, // 403 scope denied
331
- ForbiddenScopeError, // 403 forbidden_scope out of the key's ceiling / non-org key on the wildcard
332
- BreadthRequiredError, // 400 breadth_required org key on a bare list must pick a project / "-"
333
- NotFoundError, // 404 incl. an out-of-ceiling id (never an existence oracle)
334
- ConflictError, // 409 incl. idempotency_conflict (same key, different body)
335
- ValidationError, // 422 see err.body.error.details
336
- PaymentRequiredError, // 402 x402 test-mode challenge in err.paymentRequired
337
- RateLimitError, // 429 err.retryAfter (seconds)
403
+ AuthenticationError, // 401: key missing / expired / revoked
404
+ PermissionError, // 403: scope denied
405
+ ForbiddenScopeError, // 403 forbidden_scope: out of the key's ceiling / non-org key on the wildcard
406
+ BreadthRequiredError, // 400 breadth_required: org key on a bare list must pick a project / "-"
407
+ NotFoundError, // 404: incl. an out-of-ceiling id (never an existence oracle)
408
+ ConflictError, // 409: incl. idempotency_conflict (same key, different body)
409
+ ValidationError, // 422: see err.body.error.details
410
+ PaymentRequiredError, // 402: x402 test-mode challenge in err.paymentRequired
411
+ RateLimitError, // 429: err.retryAfter (seconds)
338
412
  ConnectionError, // network failure before a response
339
413
  TimeoutError, // request timed out / aborted
340
414
  } from "@extrovert.dev/sdk";
@@ -387,13 +461,24 @@ Maps 1:1 to the Extrovert `/v1` REST contract.
387
461
  | `inbox.send()` | `POST /v1/inboxes/{addr}/send` |
388
462
  | `inbox.messages()` | `GET /v1/inboxes/{addr}/messages` |
389
463
  | `inbox.threads()` | `GET /v1/inboxes/{addr}/threads` |
464
+ | `inbox.searchThreads()` | `GET /v1/inboxes/{addr}/threads/search` |
465
+ | `inbox.thread(thread_id)` | `GET /v1/inboxes/{addr}/threads/{thread_id}` |
466
+ | `inbox.reply({ thread_id, ... })` | `POST /v1/inboxes/{addr}/reply` |
467
+ | `inbox.deleteThread(thread_id)` | `DELETE /v1/inboxes/{addr}/threads/{thread_id}` |
390
468
  | `inbox.waitForEmail()` | `POST /v1/inboxes/{addr}/wait` |
391
469
  | `extrovert.inboxes.update(addr, ...)` | `PATCH /v1/inboxes/{addr}` |
392
470
  | `extrovert.messages.get(id)` | `GET /v1/messages/{id}` |
393
- | `extrovert.messages.reply(id, ...)` | `POST /v1/messages/{id}/reply` |
394
- | `extrovert.threads.get(id)` | `GET /v1/threads/{id}` |
471
+ | `extrovert.threads.list(inbox, ...)` | `GET /v1/inboxes/{inbox}/threads` |
472
+ | `extrovert.threads.search(inbox, ...)` | `GET /v1/inboxes/{inbox}/threads/search` |
473
+ | `extrovert.threads.get(inbox, id)` | `GET /v1/inboxes/{inbox}/threads/{id}` |
474
+ | `extrovert.threads.reply(inbox, ...)` | `POST /v1/inboxes/{inbox}/reply` |
475
+ | `extrovert.threads.delete(inbox, id)` | `DELETE /v1/inboxes/{inbox}/threads/{id}` |
395
476
  | `extrovert.webhooks.register()` | `POST /v1/webhooks` |
396
477
  | `extrovert.domains.onboard(...)` | `POST /v1/domains` |
478
+ | `extrovert.commerce.quoteDomain(...)` | `POST /v1/commerce/domain-quotes` |
479
+ | `extrovert.commerce.requestDomainPurchase(...)` | `POST /v1/commerce/requests/domain-purchases` |
480
+ | `extrovert.commerce.requestPlanChange(...)` | `POST /v1/commerce/requests/plan-changes` |
481
+ | `extrovert.commerce.get(...)` | `GET /v1/commerce/requests/{id}` |
397
482
  | `extrovert.whoami()` | `GET /v1/auth/me` |
398
483
 
399
484
  Helpers: `verifyWebhookSignature`, `parseWebhook`, `extractOtp`, `extractLink`,
@@ -419,30 +504,32 @@ A key carries a subset of these capability scopes (`whoami().scopes`):
419
504
  | ----------------- | ---------------------------------------------------------------------- |
420
505
  | `mailbox:create` | Create inboxes. |
421
506
  | `mailbox:read` | Read inboxes, messages, threads. |
507
+ | `mailbox:credentials` | Export raw IMAP/SMTP credentials on paid plans; never implied by read. |
422
508
  | `mailbox:send` | Send / reply / forward. |
423
509
  | `mailbox:quota` | Change an inbox's effective daily recipient cap (opt-in). |
424
510
  | `mailbox:delete` | Delete inboxes. |
425
511
  | `webhook:write` | Register / manage webhooks. |
426
- | `domain:manage` | The agent-facing domains plane (onboard / verify / offboard). |
427
- | `domain:purchase` | Additionally required (and **opt-in, default-off**) to onboard with `mode: "purchased"` (registrar spend, plan-capped). |
512
+ | `domain:manage` | Onboard / verify / offboard shared or customer-controlled domains. It never buys a domain. |
513
+ | `domain:read` | Read accessible domains, readiness, and domain updates without permission to change setup. |
514
+ | `commerce:request` | Quote and request a domain purchase or plan change, then poll status. It never approves or spends directly. |
428
515
  | `review:act` | The BYO reviewer decision plane. |
429
516
 
430
517
  > The `mailbox:*` scope strings are the live wire contract (the public product term is **inbox**;
431
- > the scope strings are kept verbatim so already-minted keys stay valid).
518
+ > the scope strings are kept verbatim so already-issued keys stay valid).
432
519
 
433
520
  ---
434
521
 
435
- ## Review Loop the open contract
522
+ ## Review Loop: the open contract
436
523
 
437
524
  The **Review Loop** (HITL) adds supervised autonomy: an agent submits a draft `mode:"review"`,
438
525
  a human approves / edits / rejects it, and the loop learns. The stable agent-facing JSON shapes
439
- are published here as a documented, **versioned open contract** an SDK + skill contract, **not**
526
+ are published here as a documented, **versioned open contract**: an SDK + skill contract, **not**
440
527
  a wire protocol (there is no `/v1/contract` endpoint).
441
528
 
442
529
  ```ts
443
530
  import { CONTRACT_VERSION, CONTRACT_MANIFEST } from "@extrovert.dev/sdk";
444
531
 
445
- CONTRACT_VERSION; // "0.1.0-pre.5" provisional, pre-1.0; pin it
532
+ CONTRACT_VERSION; // "0.1.0-pre.7": provisional, pre-1.0; pin it
446
533
  CONTRACT_MANIFEST.stability; // "provisional"
447
534
  CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","Rule","ReviewEvent"]
448
535
  ```
@@ -451,8 +538,8 @@ CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","R
451
538
  `org_id` / `project_id`. `org` rules are house-style inherited by every project in the org;
452
539
  `project` rules are layered on top and outrank broader org rules in the ordered `get_rules`
453
540
  precedence ladder. An agent-plane `rules.save(...)` is **always project-layer** (bound to the key's
454
- project); an agent **cannot author `rule_layer: "org"` rules in v1** that is a console/admin
455
- action. (`scope: "general"` still means a house-style rule *within* the project layer `scope` is
541
+ project); an agent **cannot author `rule_layer: "org"` rules in v1**: that is a console/admin
542
+ action. (`scope: "general"` still means a house-style rule *within* the project layer: `scope` is
456
543
  the category axis, `rule_layer` is the ownership axis.)
457
544
 
458
545
  Full reference: the [agent contract](https://docs.extrovert.dev/review-loop/agent-contract/) docs
@@ -460,21 +547,21 @@ page and the agent skills (`extrovert-send-email`, `extrovert-writing-rules`).
460
547
 
461
548
  ### Contract & versioning
462
549
 
463
- The Review-Loop shapes are an **open, documented contract versioned *with* this SDK** (not a wire
550
+ The Review-Loop shapes are an **open, documented contract: versioned *with* this SDK** (not a wire
464
551
  protocol; there is no `/v1/contract` endpoint). Three guarantees:
465
552
 
466
- - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.5`**, reconciled across the SDK package
553
+ - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.7`**, reconciled across the SDK package
467
554
  version, the MCP server, and the OpenAPI `info.version`. Pin it; pin `CONTRACT_MANIFEST` for the
468
555
  exact shape set you built against.
469
- - **Named, documented types.** The five canonical shapes `ReviewIntent`, `ReviewFeedback`,
470
- `DiffJson` (+`DiffHunk`), `Rule`, `ReviewEvent` plus the full M1–M8 surface (submit/states,
556
+ - **Named, documented types.** The five canonical shapes: `ReviewIntent`, `ReviewFeedback`,
557
+ `DiffJson` (+`DiffHunk`), `Rule`, `ReviewEvent`: plus the full M1–M8 surface (submit/states,
471
558
  chat, categories, graduation + risk dial, reconciliation + pacing, rules + audit, the BYO reviewer
472
559
  decision plane) are exported as named TypeScript types from the `contract` module.
473
560
  `CONTRACT_MANIFEST.shapes` enumerates them all by name.
474
561
  - **Drift-proof.** A conformance/drift test validates the canonical example JSON against **both** the
475
562
  OpenAPI component schemas (Go) and these SDK types, and asserts the version is reconciled across
476
563
  every surface (plus a negative test so the guard isn't a tautology). Rename a field anywhere and
477
- the build breaks the published types can't silently diverge from the wire.
564
+ the build breaks: the published types can't silently diverge from the wire.
478
565
 
479
566
  > **Provisional 0.x.** The contract is open and documented but MAY still evolve additively before
480
567
  > 1.0 (no external users yet). Pin `CONTRACT_VERSION` and `CONTRACT_MANIFEST`.
@@ -483,7 +570,7 @@ protocol; there is no `/v1/contract` endpoint). Three guarantees:
483
570
 
484
571
  ## Examples
485
572
 
486
- Runnable with [`tsx`](https://github.com/privatenumber/tsx) work offline out of the box:
573
+ Runnable with [`tsx`](https://github.com/privatenumber/tsx): work offline out of the box:
487
574
 
488
575
  ```bash
489
576
  EXTROVERT_API_BASE_URL=mock npx tsx examples/mailbox-in-one-call.ts
@@ -494,7 +581,7 @@ EXTROVERT_API_BASE_URL=mock npx tsx examples/wait-for-otp.ts
494
581
 
495
582
  ## Status
496
583
 
497
- > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.5` a
584
+ > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.7`: a
498
585
  > deliberate **prerelease**, pre-1.0, expect additive change. The offline `mock` transport models the
499
586
  > live server closely enough to reproduce a 422 `intent_required` and a queued review, so build and
500
587
  > test against it before you have a key. Install from the `next` tag until a stable release is cut.