@cohortapp/agent-sdk 2.10.0 → 2.11.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.
Files changed (50) hide show
  1. package/.claude/commands/init-maestro.md +16 -9
  2. package/docs/guides/mac-mini.md +11 -1
  3. package/docs/runbooks/cohort-cutover.md +16 -0
  4. package/lib/mcp/server.test.mjs +16 -4
  5. package/lib/org/client.mjs +58 -1
  6. package/lib/org/protocol.checksum +1 -1
  7. package/lib/org/protocol.mjs +98 -0
  8. package/lib/org/protocol.test.mjs +19 -2
  9. package/lib/org/resource-tools.mjs +317 -0
  10. package/lib/org/resource-tools.test.mjs +361 -0
  11. package/lib/org/tool-access.mjs +176 -0
  12. package/lib/org/tool-access.test.mjs +144 -0
  13. package/lib/org/tool-surface.mjs +431 -5
  14. package/lib/org/tool-surface.test.mjs +385 -8
  15. package/lib/org/ui-parity.mjs +196 -3
  16. package/lib/org/ui-parity.test.mjs +126 -7
  17. package/lib/tool-definitions.js +23 -2
  18. package/package.json +2 -2
  19. package/plugins/maestro-skills/.claude-plugin/marketplace.json +1 -1
  20. package/plugins/maestro-skills/plugin.json +4 -0
  21. package/plugins/maestro-skills/skills/venture-deliverables.md +176 -0
  22. package/policies/information-barriers.yaml +34 -7
  23. package/scripts/ci/check-no-residual-identity.mjs +281 -9
  24. package/scripts/ci/check-no-residual-identity.test.mjs +115 -2
  25. package/scripts/cloud-relay/voice/relay-identity.test.mjs +96 -0
  26. package/scripts/cloud-relay/voice/server.mjs +42 -2
  27. package/scripts/cost/track-claude-usage-pricing.test.mjs +183 -0
  28. package/scripts/cost/track-claude-usage.mjs +113 -4
  29. package/scripts/daemon/agent-daemon.mjs +150 -3
  30. package/scripts/daemon/agent-daemon.test.mjs +190 -0
  31. package/scripts/daemon/assurance.mjs +38 -15
  32. package/scripts/daemon/assurance.test.mjs +39 -1
  33. package/scripts/daemon/classifier-identity.test.mjs +137 -0
  34. package/scripts/daemon/classifier.mjs +98 -17
  35. package/scripts/daemon/prompt-builder-preamble.test.mjs +210 -0
  36. package/scripts/daemon/prompt-builder.mjs +264 -41
  37. package/scripts/daemon/prompt-builder.test.mjs +5 -5
  38. package/scripts/disclosure_boundaries.py +56 -5
  39. package/scripts/huddle/huddle-prompt.test.mjs +176 -0
  40. package/scripts/huddle/huddle-server.mjs +128 -13
  41. package/scripts/local-triggers/autoupdate.sh +83 -0
  42. package/scripts/local-triggers/generate-plists.sh +9 -0
  43. package/scripts/local-triggers/generate-plists.test.mjs +12 -10
  44. package/scripts/media-generation/brand-clause.test.mjs +135 -0
  45. package/scripts/media-generation/gemini-image-client.mjs +27 -9
  46. package/scripts/media-generation/generate-assets.mjs +102 -7
  47. package/scripts/pre-draft-context.py +91 -15
  48. package/scripts/spawn-session.sh +36 -6
  49. package/scripts/test-employer-grounding.py +348 -0
  50. package/scripts/validate_outbound.py +190 -26
@@ -396,13 +396,20 @@ Do NOT modify these sections (keep them exactly as they are, except for agent na
396
396
 
397
397
  h. **`scripts/daemon/responder.mjs` `FALLBACK_PREAMBLE`** — System prompt that introduces the agent to Claude. Identity intro line must reference the new agent. Preserve the operational rules.
398
398
 
399
- i. **`scripts/daemon/prompt-builder.mjs` `FALLBACK_PREAMBLE`** — Same treatment.
400
-
401
- j. **`scripts/daemon/classifier.mjs` `SYSTEM_PROMPT`** — Identity intro line. Preserve everything else.
402
-
403
- k. **`scripts/huddle/huddle-server.mjs` `HUDDLE_SYSTEM_PROMPT`** — Voice agent identity line.
404
-
405
- l. **`scripts/spawn-session.sh`** — Sub-session bootstrap prompt that names the agent.
399
+ > **NO LONGER A MANUAL STEP — these four now read config and must NOT be hand-edited.**
400
+ > `scripts/daemon/prompt-builder.mjs` (`buildFallbackPreamble`), `scripts/daemon/classifier.mjs`
401
+ > (`renderClassifierIdentityLine`), `scripts/huddle/huddle-server.mjs`
402
+ > (`renderHuddleIdentityLine` + `renderHuddleCompanyBlock`) and `scripts/spawn-session.sh`
403
+ > (sources `config/agent.env`) all render identity, employer and — for the preamble — AUTHORITY
404
+ > from `config/agent.json`, `config/company.json` and `policies/action-classification.yaml`,
405
+ > omitting any clause whose field is unset or `UNCONFIGURED`. Editing the literals back in
406
+ > re-creates the fabrication. If one of them prints the wrong company, fix the CONFIG.
407
+ >
408
+ > (Previously items i, j, k and l. Item k asked only for the huddle "Voice agent identity
409
+ > line" and never mentioned the `ABOUT <COMPANY>:` block six lines below it — which described
410
+ > a second, different fictional company and was spoken aloud on auto-joined huddles. That
411
+ > block is now `renderHuddleCompanyBlock(config/company.json)` and emits NOTHING when the
412
+ > company config is empty.)
406
413
 
407
414
  m. **`scripts/continuous-monitor.sh`** — Channel monitor agent prompt.
408
415
 
@@ -412,9 +419,9 @@ Do NOT modify these sections (keep them exactly as they are, except for agent na
412
419
 
413
420
  p. **`scripts/rag-indexer.py`, `scripts/user-context-search.py`** — Author docstring at the top. Set to the new agent's full name.
414
421
 
415
- q. **`scripts/validate-outbound.py`** — Test/regex references to the placeholder agent name "Robin Hayes" or `lookup_entity("Robin Hayes")`. Replace with the new agent's full name. Leave the generic third-party pronoun regex (around line 1007) UNCHANGED — it's a detector, not an identity reference.
422
+ q. **`scripts/validate_outbound.py`** — ONLY the embedded `--self-test` harness at the bottom, whose fixtures look up example people in the entity index. Everything on the production path (the employer entity-id set, the possessive-claim regex, the AI-self-disclosure business exceptions) now resolves from `config/company.json` at load and must NOT be hand-edited. Leave the generic third-party pronoun regex UNCHANGED — it's a detector, not an identity reference.
416
423
 
417
- **Verification step (identity AND company):** After rewrites, run `grep -rniE "robin|alex|jordan|northwind" scripts/ config/ agents/ policies/ 2>&1` and confirm NO placeholder identity remains — neither the placeholder persona (Robin / Alex) NOR the placeholder company ("Northwind"). Everything must be the new agent + the company from `config/company.json`. Report any remaining matches you cannot safely auto-resolve so the main agent can decide.
424
+ **Verification step (identity AND company):** After rewrites, run `node scripts/ci/check-no-residual-identity.mjs` — it now FAILS on fictional placeholder identity anywhere under `scripts/`, `lib/`, `policies/`, `agents/`, `bin/`, `services/` and friends (tests, fixtures, `docs/`, `scaffold/` and `*.example` are exempt by construction, and the remaining known-dirty files are printed from `PLACEHOLDER_PENDING`). Then run `grep -rniE "robin|alex|jordan|northwind" scripts/ config/ agents/ policies/ 2>&1` for anything the guard's patterns do not cover, and confirm NO placeholder identity remains — neither the placeholder persona (Robin / Alex) NOR the placeholder company ("Northwind"). Everything must be the new agent + the company from `config/company.json`. Report any remaining matches you cannot safely auto-resolve so the main agent can decide.
418
425
 
419
426
  ### Sub-agent 5: Update agent definitions
420
427
 
@@ -48,10 +48,20 @@ Two lanes — pick one:
48
48
  pair it to the member, then:
49
49
 
50
50
  ```bash
51
- export COHORT_API_KEY=nlk_… COHORT_ORG_ID=<org-slug> COHORT_AGENT_ID=<member-slug>
51
+ export COHORT_API_KEY=nlk_… COHORT_ORG_ID=<org-ID> COHORT_AGENT_ID=<member-slug>
52
52
  cohort setup
53
53
  ```
54
54
 
55
+ > **`COHORT_ORG_ID` is the org's ID, not its slug** — this line said
56
+ > `<org-slug>` and that would 401 every call. The value rides as `x-org-id`
57
+ > (`lib/org/client.mjs#baseHeaders`) and hq compares it with strict equality
58
+ > against the key's own `Org.id`
59
+ > (`src/server/auth/org-api-key.ts:163` — no slug fallback, no normalisation).
60
+ > For Adaptic the two differ: the ID is `org_default_adaptic`, the slug is
61
+ > `adaptic`. `COHORT_AGENT_ID` **is** a slug (`A001`, `A038`) — the two
62
+ > variables sitting side by side take different kinds of identifier, which is
63
+ > exactly why this was easy to get wrong.
64
+
55
65
  Pull-enrollment populates `config/agent.json` from the member profile and
56
66
  writes `config/org.yaml` (`org.cohort.{enabled,base,orgId,token}`).
57
67
 
@@ -55,6 +55,22 @@ machine's `config/org.yaml` pins `server.url` (e.g.
55
55
  service changes the `*.up.railway.app` host and strands the fleet. Move the
56
56
  fleet to a stable custom domain first (§2), then rename freely.
57
57
 
58
+ > ⚠︎ **2026-08-16 — this warning appears to have come true, and it is unresolved.**
59
+ > `https://neolith.up.railway.app` is now UNROUTED: Railway's edge returns
60
+ > `x-railway-fallback: true` with a 404 on `/`, `/v1` and `/v1/ops`, meaning no
61
+ > service is attached to that hostname. Contrast the app, whose rename DID
62
+ > retain its old host — `cohort-app` still answers on
63
+ > `neolith-app-production.up.railway.app`. So the hq-side rename was safe and
64
+ > this one was not.
65
+ >
66
+ > Any fleet machine still pinning that URL is talking to nothing, and
67
+ > `DEPLOYMENT.md` §5.1 was handing the dead host to every new agent as its setup
68
+ > template until this was found. The replacement host was NOT determinable from
69
+ > either repo, from `cohort-os` (the org server does not live there), or from the
70
+ > app's env — a separate Railway project `neolith-production` still exists in the
71
+ > workspace and is the first place to look. Establish the real host, fix §5.1,
72
+ > and sweep every `config/org.yaml` before assuming the fleet is healthy.
73
+
58
74
  ---
59
75
 
60
76
  ## 2. Custom domain for the org server
@@ -154,19 +154,31 @@ test("tools/list: full active surface (email tools present — family is vendore
154
154
  // engage_colleagues is the judged version of that; work_track puts the ask on
155
155
  // the board and moves it. The line above is the real invariant — this one just
156
156
  // pins the number so a silent table change is visible in review.
157
- assert.equal(tools.length, 122, "email + artifact + desk families vendored → 122 tools");
157
+ // 122 → 133: the voice delta (messaging_send_voice_note) plus the mandate /
158
+ // sub-agent-registry / preference delta — ten curated tools for three
159
+ // protocol families that were declared, implemented on hq, and curated
160
+ // NOWHERE, so this transport served them only through the admin-tier
161
+ // `org_rpc` hatch. One table, two transports: they appear here for free.
162
+ // 133 → 141: the venture-deliverable desk. OrgResource — a venture's own
163
+ // catalogue of the artifacts it has produced — was reachable from NO agent
164
+ // code path in either plane, so the venture's own colleagues could not list,
165
+ // add, correct, reorder or remove one of their deliverables. Unlike every
166
+ // other desk block, these eight are GENERATED from the vendored protocol
167
+ // declaration (lib/org/resource-tools.mjs), so this transport gets them, and
168
+ // every future resource method, without a second hand-written table.
169
+ assert.equal(tools.length, 141, "email + artifact + desk families vendored → 141 tools");
158
170
  const names = tools.map((t) => t.name);
159
- for (const expected of ["org_whoami", "org_describe", "org_rpc", "org_read", "messaging_send", "task_assign", "board_ready", "email_send", "email_inbox", "artifact_create", "artifact_act", "artifact_catalog", "email_mailboxes", "email_draft_send", "files_list", "calendar_find_a_time", "crm_list_deals", "books_reports", "meetings_recap_file"]) {
171
+ for (const expected of ["org_whoami", "org_describe", "org_rpc", "org_read", "messaging_send", "task_assign", "board_ready", "email_send", "email_inbox", "artifact_create", "artifact_act", "artifact_catalog", "email_mailboxes", "email_draft_send", "files_list", "calendar_find_a_time", "crm_list_deals", "books_reports", "meetings_recap_file", "resource_list", "resource_attach_file"]) {
160
172
  assert.ok(names.includes(expected), `${expected} listed`);
161
173
  }
162
174
  assert.ok(tools.every((t) => t.inputSchema && t.inputSchema.type === "object"));
163
175
  h.server.stop();
164
176
  });
165
177
 
166
- test("tools/list honours the email gate (emailAvailable:false → 117 tools)", async () => {
178
+ test("tools/list honours the email gate (emailAvailable:false → 136 tools)", async () => {
167
179
  const h = harness({ emailAvailable: false });
168
180
  const r = await h.request("tools/list", {});
169
- assert.equal(r.result.tools.length, 117, "the 5 own-mailbox email tools drop out");
181
+ assert.equal(r.result.tools.length, 136, "the 5 own-mailbox email tools drop out");
170
182
  const names = new Set(r.result.tools.map((t) => t.name));
171
183
  // The five own-mailbox tools (email:true) are gated out …
172
184
  for (const gated of ["email_send", "email_inbox", "email_message", "email_thread", "email_mark_read"]) {
@@ -109,9 +109,18 @@ export function isEnabled(cfg) {
109
109
  * Distil the org client config from an agent config object. Resolves:
110
110
  * - base: the server origin for the /v1 binding (explicit `org.cohort.base`,
111
111
  * else derived from `apiUrl` [+ `orgId` path] for back-compat).
112
- * - orgId: the org slug — config `org.cohort.orgId` else COHORT_ORG_ID env
112
+ * - orgId: the org's ID — config `org.cohort.orgId` else COHORT_ORG_ID env
113
113
  * (hq's documented var). Used by the legacy directory-path derivation
114
114
  * and, when known, sent as the `x-org-id` pin header (defence-in-depth).
115
+ *
116
+ * NOT THE SLUG, though this said "the org slug" until 2026-08-21 and
117
+ * `docs/guides/mac-mini.md` told operators to export one. hq compares
118
+ * the pin with STRICT EQUALITY against the key's `Org.id`
119
+ * (`src/server/auth/org-api-key.ts:163`) — no slug fallback — so a
120
+ * slug here 401s every call. The two are not interchangeable: the
121
+ * ID has the form `org_default_<slug>` (e.g. slug `acme` → ID
122
+ * `org_default_acme`), never the bare slug. Note `COHORT_AGENT_ID`
123
+ * beside it genuinely IS a slug.
115
124
  * - token: bearer credential — the RAW OrgApiKey. Resolution order:
116
125
  * config `token` → COHORT_API_TOKEN → COHORT_TOKEN → COHORT_API_KEY.
117
126
  * hq's auth doc names the agent var COHORT_API_KEY; the older
@@ -1314,6 +1323,54 @@ export function messagingSend(params, o = {}) {
1314
1323
  return call("messaging.send", params || {}, o);
1315
1324
  }
1316
1325
 
1326
+ /**
1327
+ * Ask hq who — if anyone — should respond to a message in a channel
1328
+ * (messaging.electResponder). This is the SHOULD-I-RESPOND decision, moved off
1329
+ * the daemon and onto hq's `routeResponders`, which for an undirected group
1330
+ * message elects exactly ONE responder or NONE, and already fails safe (no model
1331
+ * → quiet). hq derives the roster + trigger server-side from just
1332
+ * `{messageId, channelId}`, org-scoped, computes the verdict ONCE per messageId
1333
+ * and caches it in a durable ledger event, so concurrent daemons read the same
1334
+ * election (the loser of a race reads the winner).
1335
+ *
1336
+ * Read-style: agent-tier `messaging.read` scope (any paired seat may ask),
1337
+ * `sideEffecting:false` — no client idempotency key, and the server owns the
1338
+ * write-once ledger append. Returns a res frame whose `result` is
1339
+ * `{ election, mode, responders:[{slug,dimension}], deferredToHuman }`.
1340
+ *
1341
+ * FAIL-OPEN AT THE TRANSPORT ONLY: an unreachable/disabled server yields an
1342
+ * error frame, never a throw. The CALLER owns the fail-SAFE inversion — for an
1343
+ * ambient channel, "no verdict / not me" means STAY SILENT, never respond-to-all.
1344
+ *
1345
+ * @param {{messageId:string, channelId:string}} params
1346
+ * @param {object} [o] { base, token, orgId?, fetchImpl? }
1347
+ */
1348
+ export function electResponder(params, o = {}) {
1349
+ return call("messaging.electResponder", params || {}, o);
1350
+ }
1351
+
1352
+ /**
1353
+ * Record a voice note in THIS seat's own designed voice
1354
+ * (messaging.synthesizeVoiceNote) and return the READY `{fileId, durationMs,
1355
+ * sizeBytes}` to attach.
1356
+ *
1357
+ * This is the RECORD half only. Posting is an ordinary `messagingSend` with
1358
+ * `attachments:[{fileId}]` — hq deliberately keeps the two apart because
1359
+ * synthesis is a provider round-trip that cannot ride its transactional send
1360
+ * lane, and because a spoken message must be an ordinary message with a file on
1361
+ * it rather than a second send path with its own governance.
1362
+ *
1363
+ * Refusals come back as error frames naming the cause (no synthesis key on the
1364
+ * deployment, this seat has no designed voice, the hourly recording cap). Relay
1365
+ * the cause — never claim a voice note was sent when the frame was not ok.
1366
+ *
1367
+ * `messaging_send_voice_note` (tool-surface) composes both halves into one verb.
1368
+ * @param {{text:string}} params
1369
+ */
1370
+ export function messagingSynthesizeVoiceNote(params, o = {}) {
1371
+ return call("messaging.synthesizeVoiceNote", params || {}, o);
1372
+ }
1373
+
1317
1374
  /** React to an org message (messaging.react). */
1318
1375
  export function messagingReact(params, o = {}) {
1319
1376
  return call("messaging.react", params || {}, o);
@@ -1 +1 @@
1
- 9ecd8993a6a9aceac04da479c6626a3b3852378b81bfd2b7590d2eee2099c9a9
1
+ b2cbf124c99800a0ad1b19d7aea6a88c3c72f07b53838eb44ef6f82d888fda43
@@ -95,6 +95,15 @@ export const FAMILIES = Object.freeze([
95
95
  // because the same surface outlives the run — the Org › Architecture view
96
96
  // reads this state for a workspace that has long since committed.
97
97
  "architecture",
98
+ // RESOURCE (2026-08): the venture-deliverable catalogue — `OrgResource`, the
99
+ // workspace's own index of what it has to show for itself (site, deck,
100
+ // one-pager, memo, brand book, film, brand assets, repo). The table has been
101
+ // written by the portal and rendered by `/resources` since the three-tier
102
+ // workforce migration and was reachable from NO agent code path at all: zero
103
+ // hits across hq's methods/, mcp/ and llm-responder/ trees, so the venture's
104
+ // own colleagues could not list, add, correct, reorder or remove one of
105
+ // their own deliverables. This family is that surface.
106
+ "resource",
98
107
  ]);
99
108
 
100
109
  /**
@@ -311,7 +320,35 @@ export const METHODS = Object.freeze({
311
320
  // the transactional lane. Still messaging.write scope: faking "X is
312
321
  // composing" in a room is a social write a read-only key must not have. ---
313
322
  "messaging.typing": { family: "messaging", scope: "messaging.write", sideEffecting: false },
323
+ // --- messaging.synthesizeVoiceNote (2026-08): a colleague SPEAKS when asked
324
+ // to. hq could already synthesise an agent's voice note, but only as a
325
+ // reflex — it answered a HUMAN's spoken note in kind and nothing else —
326
+ // so an agent asked in TEXT ("send me a voicenote with this update")
327
+ // truthfully answered that it could not. This is the verb that makes the
328
+ // ability reachable: it records the acting seat's OWN designed voice and
329
+ // returns the READY fileId, which `messaging.send` then attaches like any
330
+ // other file (same ACL, same outbound gate, same redacted audit row — no
331
+ // parallel send path). Split in two deliberately: synthesis is a provider
332
+ // round-trip with a 20s timeout and cannot ride hq's 5s transactional
333
+ // lane, so this half is sideEffecting:false and self-audits its own
334
+ // `files`/`media.voice.synthesized` chain row, exactly as `books.ask`
335
+ // does for its model call. messaging.write scope, not read: putting words
336
+ // in a named colleague's mouth is a social write of the first order. ---
337
+ "messaging.synthesizeVoiceNote": { family: "messaging", scope: "messaging.write", sideEffecting: false },
314
338
  "messaging.channels": { family: "messaging", scope: "messaging.read", sideEffecting: false },
339
+ // --- messaging.electResponder (2026-08): the SERVER-SIDE "should anyone answer,
340
+ // and who?" election for a multi-participant channel. A fleet daemon calls it
341
+ // with just {messageId, channelId}; hq assembles the RouteInput server-side
342
+ // (roster/trigger/channel, org from the KEY) and runs the SAME response
343
+ // planner the in-process responder uses, returning ONE verdict every seat
344
+ // converges on — replacing each daemon's local, room-blind directed_at_agent
345
+ // gate. messaging.read scope (every paired seat may ask; in DEFAULT_AGENT_
346
+ // SCOPES). Declared sideEffecting:false though it appends: the verdict is a
347
+ // write-once ledger event ("responder"/"responder.election", keyed on
348
+ // messageId) and a CACHE HIT must return with ZERO appends — which the
349
+ // side-effecting exactly-one-append contract forbids — so it self-audits its
350
+ // own chain row like messaging.synthesizeVoiceNote and books.ask do. ---
351
+ "messaging.electResponder": { family: "messaging", scope: "messaging.read", sideEffecting: false },
315
352
  // --- calling (SP3): call lifecycle in the human app. NOT reserved-admin. ---
316
353
  "calling.start": { family: "calling", scope: "calling.write", sideEffecting: true },
317
354
  "calling.join": { family: "calling", scope: "calling.write", sideEffecting: true },
@@ -1008,6 +1045,67 @@ export const METHODS = Object.freeze({
1008
1045
  "architecture.status": { family: "architecture", scope: "org.read", sideEffecting: false },
1009
1046
  "architecture.accept": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
1010
1047
  "architecture.approve": { family: "architecture", scope: "org.write", sideEffecting: true, idempotent: true },
1048
+ // --- resource (2026-08): THE VENTURE-DELIVERABLE CATALOGUE. `OrgResource` is
1049
+ // the org-scoped index of a venture's own artifacts — one row per
1050
+ // deliverable, keyed `(orgId, slug)`, provisioned by the portal
1051
+ // (`source:"ftlab-portal"`), by Genesis, by the artifact ingest, or by
1052
+ // hand. Eight methods: list/get (reads) and create/update/delete/reorder
1053
+ // + attachFile/detachFile (writes).
1054
+ //
1055
+ // SCOPES REUSE `org.read` / `org.write` rather than minting a
1056
+ // `resource.*` pair — the subagent/genesis precedent, and here it is also
1057
+ // the whole ACCESS-LEVEL point. A catalogue card is org-structural
1058
+ // authored content exactly like a persona, an SOP or an annotation, which
1059
+ // is what `org.write` already names; `org.read` is in hq's VIEWER tier
1060
+ // (src/server/auth/role-scopes.ts) and both are in DEFAULT_AGENT_SCOPES.
1061
+ // So on the day these descriptors land, every seat that exists can READ
1062
+ // its venture's catalogue and every EDITOR seat can maintain it, with no
1063
+ // key reminted and no tier table edited. A private scope pair would have
1064
+ // to be threaded through SCOPES, DEFAULT_AGENT_SCOPES and the VIEWER tier
1065
+ // before ONE existing credential could reach the family — which is the
1066
+ // same unreachable-on-arrival failure this declaration exists to end.
1067
+ // (`org.write` is deliberately NOT in hq's HUMAN_DEFAULT_SCOPES: the
1068
+ // human mobile bearer is intentionally narrower than an agent, and the
1069
+ // founder's own catalogue editing runs through the web app's server
1070
+ // actions under `requireOrgContext()`, never this bearer.)
1071
+ //
1072
+ // THE FILES BRIDGE IS A POINTER, NOT A SECOND DRIVE. The artifact ingest
1073
+ // files a venture's artifacts as real `WorkspaceFile` rows and stamps
1074
+ // `OrgResource.fileId`. `resource.*` returns that id and the card's own
1075
+ // columns and NOTHING derived from the drive row — no bytes, no content,
1076
+ // no name, size, version or share list. That is a share boundary, not
1077
+ // tidiness: the catalogue is ORG-WIDE under `org.read` while the drive is
1078
+ // SEAT-scoped by `driveVisibilityWhere` (hq methods/files/list.ts), so a
1079
+ // `resource.*` that re-served file content would be a one-call bypass of
1080
+ // the share filter — the exact defect that handler's docblock warns
1081
+ // about. A card is a citation; `files.get` / `files.docRead` /
1082
+ // `files.exportRequest` are the retrieval path and already exist.
1083
+ // `resource.attachFile` is the ONE place the families touch, and it
1084
+ // resolves the file through the drive's own `getFileOrThrow` (org pin +
1085
+ // `canSeeDriveFile`, NOT_FOUND either way) so it can neither cite another
1086
+ // tenant's file nor act as an existence oracle for one this seat cannot
1087
+ // see. `files.uploadRegister`'s `.strict()` schema is untouched.
1088
+ //
1089
+ // TENANCY, twice over. Slug-addressed rows go through the compound unique
1090
+ // `orgId_slug` — the tenant is PART OF THE KEY, so another org's card is
1091
+ // unaddressable. Id-addressed rows go through `ctx.scopeWhere({ id })` on
1092
+ // findFirst/updateMany/deleteMany, never `findUnique({ where: { id } })`,
1093
+ // which would cross tenants.
1094
+ //
1095
+ // Writes are `sideEffecting:true` and append exactly ONE chain event each
1096
+ // under `family:"resource"`; `idempotent:true` is opt-in at the caller.
1097
+ // `resource.delete` removes the CARD only — the `WorkspaceFile` keeps its
1098
+ // own 30-day soft-delete lane, which is why `fileId` is a plain String
1099
+ // and not a relation (schema.prisma: a cascade would lose the record that
1100
+ // the venture ever had a deck). ---
1101
+ "resource.list": { family: "resource", scope: "org.read", sideEffecting: false },
1102
+ "resource.get": { family: "resource", scope: "org.read", sideEffecting: false },
1103
+ "resource.create": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1104
+ "resource.update": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1105
+ "resource.delete": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1106
+ "resource.reorder": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1107
+ "resource.attachFile": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1108
+ "resource.detachFile": { family: "resource", scope: "org.write", sideEffecting: true, idempotent: true },
1011
1109
  });
1012
1110
 
1013
1111
  /**
@@ -139,7 +139,11 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
139
139
  // and architecture (the four tier review decisions and the one whole-
140
140
  // architecture approval, which outlive the run as the Org › Architecture
141
141
  // view) → 53.
142
- assert.equal(FAMILIES.length, 53, "family count");
142
+ // The 2026-08 venture-deliverable pass adds 1: resource (the OrgResource
143
+ // catalogue — a venture's own deliverables). The table predates every agent
144
+ // plane and was reachable from none of them, so a venture's colleagues could
145
+ // not list, add, correct, reorder or remove one of their own artifacts → 54.
146
+ assert.equal(FAMILIES.length, 54, "family count");
143
147
  // 357 = the mesh-protocol + agent UI-parity + calling + branding + email
144
148
  // methods, plus the Live Integrations surface: integration.toolsetVersion +
145
149
  // integration.listAgentTools (SP1) and integration.invokeTool (SP5, execute a
@@ -188,7 +192,20 @@ test("protocol: frozen family + method counts (additive evolution guard)", () =>
188
192
  // had shipped WITHOUT descriptors, which made all 13 unreachable — dispatch
189
193
  // rejects unknown methods and scopeForMethod resolves them to `admin`, which
190
194
  // no principal holds → 474.
191
- assert.equal(Object.keys(METHODS).length, 474, "method count");
195
+ // The 2026-08 voice-capability pass adds 1: messaging.synthesizeVoiceNote —
196
+ // hq could always synthesise a colleague's voice note but only as a REFLEX
197
+ // (answering a human's spoken note in kind), so an agent asked in text for a
198
+ // voice note truthfully answered that it could not. This is the verb that
199
+ // makes it askable on every plane; the POST stays plain messaging.send → 475.
200
+ // The 2026-08 venture-deliverable pass adds 8: resource.list/get (reads) and
201
+ // resource.create/update/delete/reorder/attachFile/detachFile (writes). Both
202
+ // reads ride `org.read` and every write `org.write`, so the family is
203
+ // reachable by the VIEWER and EDITOR tiers that already exist rather than by
204
+ // a scope nobody has been minted. attachFile/detachFile are the ONLY bridge
205
+ // to `files.*`: a card carries a fileId POINTER and the drive keeps its own
206
+ // seat-scoped read surface → 483. Then +1: messaging.electResponder (the
207
+ // fleet's server-side respond-election) → 484.
208
+ assert.equal(Object.keys(METHODS).length, 484, "method count");
192
209
  });
193
210
 
194
211
  test("protocol SP3: messaging + calling families/methods/scopes", async () => {