@extrovert.dev/sdk 0.1.0-pre.6 → 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.6` 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`,
@@ -424,26 +509,27 @@ A key carries a subset of these capability scopes (`whoami().scopes`):
424
509
  | `mailbox:quota` | Change an inbox's effective daily recipient cap (opt-in). |
425
510
  | `mailbox:delete` | Delete inboxes. |
426
511
  | `webhook:write` | Register / manage webhooks. |
427
- | `domain:manage` | The agent-facing domains plane (onboard / verify / offboard). |
428
- | `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. |
429
515
  | `review:act` | The BYO reviewer decision plane. |
430
516
 
431
517
  > The `mailbox:*` scope strings are the live wire contract (the public product term is **inbox**;
432
- > 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).
433
519
 
434
520
  ---
435
521
 
436
- ## Review Loop the open contract
522
+ ## Review Loop: the open contract
437
523
 
438
524
  The **Review Loop** (HITL) adds supervised autonomy: an agent submits a draft `mode:"review"`,
439
525
  a human approves / edits / rejects it, and the loop learns. The stable agent-facing JSON shapes
440
- 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**
441
527
  a wire protocol (there is no `/v1/contract` endpoint).
442
528
 
443
529
  ```ts
444
530
  import { CONTRACT_VERSION, CONTRACT_MANIFEST } from "@extrovert.dev/sdk";
445
531
 
446
- CONTRACT_VERSION; // "0.1.0-pre.6" provisional, pre-1.0; pin it
532
+ CONTRACT_VERSION; // "0.1.0-pre.7": provisional, pre-1.0; pin it
447
533
  CONTRACT_MANIFEST.stability; // "provisional"
448
534
  CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","Rule","ReviewEvent"]
449
535
  ```
@@ -452,8 +538,8 @@ CONTRACT_MANIFEST.core_shapes; // ["ReviewIntent","ReviewFeedback","DiffJson","R
452
538
  `org_id` / `project_id`. `org` rules are house-style inherited by every project in the org;
453
539
  `project` rules are layered on top and outrank broader org rules in the ordered `get_rules`
454
540
  precedence ladder. An agent-plane `rules.save(...)` is **always project-layer** (bound to the key's
455
- project); an agent **cannot author `rule_layer: "org"` rules in v1** that is a console/admin
456
- 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
457
543
  the category axis, `rule_layer` is the ownership axis.)
458
544
 
459
545
  Full reference: the [agent contract](https://docs.extrovert.dev/review-loop/agent-contract/) docs
@@ -461,21 +547,21 @@ page and the agent skills (`extrovert-send-email`, `extrovert-writing-rules`).
461
547
 
462
548
  ### Contract & versioning
463
549
 
464
- 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
465
551
  protocol; there is no `/v1/contract` endpoint). Three guarantees:
466
552
 
467
- - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.6`**, reconciled across the SDK package
553
+ - **One version, everywhere.** `CONTRACT_VERSION` is **`0.1.0-pre.7`**, reconciled across the SDK package
468
554
  version, the MCP server, and the OpenAPI `info.version`. Pin it; pin `CONTRACT_MANIFEST` for the
469
555
  exact shape set you built against.
470
- - **Named, documented types.** The five canonical shapes `ReviewIntent`, `ReviewFeedback`,
471
- `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,
472
558
  chat, categories, graduation + risk dial, reconciliation + pacing, rules + audit, the BYO reviewer
473
559
  decision plane) are exported as named TypeScript types from the `contract` module.
474
560
  `CONTRACT_MANIFEST.shapes` enumerates them all by name.
475
561
  - **Drift-proof.** A conformance/drift test validates the canonical example JSON against **both** the
476
562
  OpenAPI component schemas (Go) and these SDK types, and asserts the version is reconciled across
477
563
  every surface (plus a negative test so the guard isn't a tautology). Rename a field anywhere and
478
- 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.
479
565
 
480
566
  > **Provisional 0.x.** The contract is open and documented but MAY still evolve additively before
481
567
  > 1.0 (no external users yet). Pin `CONTRACT_VERSION` and `CONTRACT_MANIFEST`.
@@ -484,7 +570,7 @@ protocol; there is no `/v1/contract` endpoint). Three guarantees:
484
570
 
485
571
  ## Examples
486
572
 
487
- 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:
488
574
 
489
575
  ```bash
490
576
  EXTROVERT_API_BASE_URL=mock npx tsx examples/mailbox-in-one-call.ts
@@ -495,7 +581,7 @@ EXTROVERT_API_BASE_URL=mock npx tsx examples/wait-for-otp.ts
495
581
 
496
582
  ## Status
497
583
 
498
- > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.6` a
584
+ > **Note.** This source SDK tracks the `/v1` contract at `CONTRACT_VERSION` `0.1.0-pre.7`: a
499
585
  > deliberate **prerelease**, pre-1.0, expect additive change. The offline `mock` transport models the
500
586
  > live server closely enough to reproduce a 422 `intent_required` and a queued review, so build and
501
587
  > test against it before you have a key. Install from the `next` tag until a stable release is cut.