@cohortapp/agent-sdk 2.10.0 → 2.11.1

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 (64) 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/channels/inbox-item.mjs +12 -0
  5. package/lib/channels/inbox-item.test.mjs +33 -0
  6. package/lib/execution/disposition.mjs +13 -2
  7. package/lib/execution/disposition.test.mjs +19 -2
  8. package/lib/execution/pipeline.test.mjs +4 -1
  9. package/lib/mcp/server.test.mjs +16 -4
  10. package/lib/org/client.mjs +58 -1
  11. package/lib/org/messaging.mjs +5 -0
  12. package/lib/org/messaging.test.mjs +7 -0
  13. package/lib/org/protocol.checksum +1 -1
  14. package/lib/org/protocol.mjs +98 -0
  15. package/lib/org/protocol.test.mjs +19 -2
  16. package/lib/org/resource-tools.mjs +317 -0
  17. package/lib/org/resource-tools.test.mjs +361 -0
  18. package/lib/org/tool-access.mjs +176 -0
  19. package/lib/org/tool-access.test.mjs +144 -0
  20. package/lib/org/tool-surface.mjs +431 -5
  21. package/lib/org/tool-surface.test.mjs +385 -8
  22. package/lib/org/ui-parity.mjs +196 -3
  23. package/lib/org/ui-parity.test.mjs +126 -7
  24. package/lib/tool-definitions.js +23 -2
  25. package/package.json +2 -2
  26. package/plugins/maestro-skills/.claude-plugin/marketplace.json +1 -1
  27. package/plugins/maestro-skills/plugin.json +4 -0
  28. package/plugins/maestro-skills/skills/venture-deliverables.md +176 -0
  29. package/policies/information-barriers.yaml +34 -7
  30. package/scripts/ci/check-no-residual-identity.mjs +281 -9
  31. package/scripts/ci/check-no-residual-identity.test.mjs +115 -2
  32. package/scripts/cloud-relay/voice/relay-identity.test.mjs +96 -0
  33. package/scripts/cloud-relay/voice/server.mjs +42 -2
  34. package/scripts/cost/track-claude-usage-pricing.test.mjs +183 -0
  35. package/scripts/cost/track-claude-usage.mjs +113 -4
  36. package/scripts/daemon/agent-daemon.mjs +212 -5
  37. package/scripts/daemon/agent-daemon.test.mjs +307 -0
  38. package/scripts/daemon/assurance.mjs +38 -15
  39. package/scripts/daemon/assurance.test.mjs +39 -1
  40. package/scripts/daemon/cadence-handlers.mjs +48 -5
  41. package/scripts/daemon/cadence-handlers.test.mjs +57 -2
  42. package/scripts/daemon/classifier-identity.test.mjs +137 -0
  43. package/scripts/daemon/classifier.mjs +98 -17
  44. package/scripts/daemon/inbox-deferral.mjs +49 -24
  45. package/scripts/daemon/inbox-deferral.test.mjs +39 -1
  46. package/scripts/daemon/prompt-builder-preamble.test.mjs +210 -0
  47. package/scripts/daemon/prompt-builder.mjs +264 -41
  48. package/scripts/daemon/prompt-builder.test.mjs +5 -5
  49. package/scripts/daemon/responder.mjs +9 -0
  50. package/scripts/disclosure_boundaries.py +56 -5
  51. package/scripts/huddle/huddle-prompt.test.mjs +176 -0
  52. package/scripts/huddle/huddle-server.mjs +128 -13
  53. package/scripts/local-triggers/autoupdate.sh +83 -0
  54. package/scripts/local-triggers/generate-plists.sh +9 -0
  55. package/scripts/local-triggers/generate-plists.test.mjs +12 -10
  56. package/scripts/media-generation/brand-clause.test.mjs +135 -0
  57. package/scripts/media-generation/gemini-image-client.mjs +27 -9
  58. package/scripts/media-generation/generate-assets.mjs +102 -7
  59. package/scripts/poller/inbox-scan-poller.mjs +7 -0
  60. package/scripts/poller/utils.mjs +11 -0
  61. package/scripts/pre-draft-context.py +91 -15
  62. package/scripts/spawn-session.sh +36 -6
  63. package/scripts/test-employer-grounding.py +348 -0
  64. package/scripts/validate_outbound.py +190 -26
@@ -0,0 +1,317 @@
1
+ /**
2
+ * lib/org/resource-tools.mjs — the `resource_*` desk tools, GENERATED.
3
+ *
4
+ * THE FAMILY. `OrgResource` is a venture's own deliverable catalogue: one
5
+ * org-scoped card per artifact (deck, one-pager, film, site, data room…),
6
+ * keyed `(orgId, slug)`, written by the FTLAB portal, by Genesis, by the
7
+ * artifact ingest, or by hand, and rendered to humans at `/resources`. Until
8
+ * this wave it was reachable from NO agent code path in either plane — a
9
+ * venture's own AI colleagues could not list, add, correct, reorder or remove
10
+ * a single one of the venture's deliverables. Eight protocol methods now
11
+ * exist; these are their curated tools.
12
+ *
13
+ * WHY THIS FILE IS A GENERATOR AND NOT A TABLE. hq's responder desks expose
14
+ * 239 ops across 11 families; maestro's curated table exposed 69 desk tools
15
+ * across 9 — files 29 vs 7, crm 44 vs 8. That divergence has one cause: the two
16
+ * tool tables are hand-written twice, so they drift the moment one side moves
17
+ * and nobody hand-copies the delta. This family refuses to be able to drift the
18
+ * same way. The tool NAME, the ACCESS TIER, the BINDING and the DESK TAG are
19
+ * all DERIVED from the vendored protocol declaration — the same declaration
20
+ * hq's handlers are registered from — and the only thing a human writes here is
21
+ * PRESENTATION: the title, the description, and the input schema. Add a ninth
22
+ * `resource.*` method to `lib/org/protocol.mjs` and sync it, and a ninth tool
23
+ * appears on the next import with no edit to this file; `resource-tools.test.mjs`
24
+ * then goes red until someone gives it a real description. The capability is
25
+ * never dark and the gap is never quiet — which is the whole thesis of the
26
+ * wave that shipped the family.
27
+ *
28
+ * ACCESS. `expectedAccessFor` derives the tier from the method's protocol
29
+ * SCOPE, never from a literal typed here: `resource.list` / `resource.get` ride
30
+ * `org.read` → `access:"read"` (every tier's lookup set, including
31
+ * least-privilege "default"); the six writes ride `org.write` →
32
+ * `access:"write"` (ceo + leadership). NONE is `access:"admin"` — that is the
33
+ * CEO-only escape-hatch tier, and a venture's colleagues are default- and
34
+ * leadership-tier agents, so parking their own deliverables there would ship
35
+ * this family exactly as dark as it was before. See lib/org/tool-access.mjs.
36
+ *
37
+ * NO `outbound` FLAG ANYWHERE IN THIS BLOCK. A card is org-internal metadata;
38
+ * nothing here puts free text a human reads onto an outbound channel, and the
39
+ * send-gate lane stays exactly messaging_send / email_send / email_draft_send /
40
+ * org_call_share_step.
41
+ *
42
+ * THE FILES BRIDGE IS A POINTER, NOT A SECOND DRIVE. A card returns its own
43
+ * columns plus a `fileId`, and NOTHING derived from the `WorkspaceFile` — no
44
+ * bytes, name, mime, size, version or share list. The catalogue is org-wide
45
+ * under `org.read` while the drive is seat-scoped by `driveVisibilityWhere`, so
46
+ * a resource tool that re-served file content would be a one-call bypass of the
47
+ * share filter. Bytes are read with `files_get` / `files_doc_read` /
48
+ * `org_rpc files.exportRequest`. `resource_attach_file` is the one seam, and hq
49
+ * resolves the id through the drive's own `getFileOrThrow` (org pin +
50
+ * `canSeeDriveFile`, NOT_FOUND either way), so it can neither cite another
51
+ * tenant's file nor act as an existence oracle for one this seat cannot see.
52
+ *
53
+ * Node builtins only. ESM.
54
+ *
55
+ * @module lib/org/resource-tools
56
+ */
57
+
58
+ "use strict";
59
+
60
+ import { METHODS } from "./protocol.mjs";
61
+ import { expectedAccessFor } from "./tool-access.mjs";
62
+
63
+ /** The protocol family these tools are generated from. */
64
+ export const RESOURCE_FAMILY = "resource";
65
+
66
+ /** The desk tag every generated entry carries (gates on DESK_PROBES.resource). */
67
+ export const RESOURCE_DESK = "resource";
68
+
69
+ // Schema helpers — same shapes tool-surface.mjs builds its entries with.
70
+ const S = (properties, required = []) => ({ type: "object", properties, required });
71
+ const str = (description) => ({ type: "string", description });
72
+ const num = (description) => ({ type: "number", description });
73
+ const bool = (description) => ({ type: "boolean", description });
74
+ const obj = (description) => ({ type: "object", description });
75
+ const arr = (items, description) => ({ type: "array", items, description });
76
+
77
+ /**
78
+ * PRESENTATION ONLY, keyed by protocol method. No `name`, no `access`, no
79
+ * `binding`, no `desk` — those four are derived, and a literal here would be
80
+ * the hand-copy this file exists to prevent.
81
+ *
82
+ * Each description carries what the schema cannot: the semantics an agent gets
83
+ * wrong on the first try (total-vs-partial reorder, shallow-merge metadata,
84
+ * what survives a delete) and the refusals it will otherwise learn by
85
+ * BAD_REQUEST.
86
+ * @type {Readonly<Record<string, {title: string, description: string, input_schema: object}>>}
87
+ */
88
+ export const RESOURCE_PRESENTATION = Object.freeze({
89
+ "resource.list": {
90
+ title: "List venture deliverables",
91
+ description:
92
+ "This venture's deliverable catalogue (resource.list) — the same cards, in the same " +
93
+ "order, that humans see at /resources: sortOrder asc, then createdAt asc. Each card is " +
94
+ "{id, slug, kind, title, description, url, previewImageUrl, source, fileId, metadata, " +
95
+ "sortOrder, createdAt, updatedAt}. `source` says who put it there — \"ftlab-portal\" " +
96
+ "(pushed by the venture portal and re-upserted on every push), \"genesis\", or " +
97
+ "\"manual\" (created through this desk). `fileId` non-null means the artifact is also " +
98
+ "filed in the workspace drive: read it with files_get / files_doc_read, NOT from here — " +
99
+ "this desk returns the card's own columns and nothing about the file itself. Filters: " +
100
+ "`kind`, `source`, free-text `q` (matches title OR description, case-insensitive), " +
101
+ "`hasFile` (true = only drive-backed cards, false = only link-only cards, omit = both). " +
102
+ "No cursor — raise `limit` (max 200) if the catalogue is long.",
103
+ input_schema: S({
104
+ kind: str("Filter by card kind, e.g. \"deck\", \"one-pager\", \"film\", \"site\" (≤64 chars)."),
105
+ source: str("Filter by provenance: \"ftlab-portal\" | \"genesis\" | \"manual\" (≤64 chars)."),
106
+ q: str("Free text; matches title OR description, case-insensitive (≤200 chars)."),
107
+ hasFile: bool("true = only cards with a drive fileId; false = only link-only cards; omit = both."),
108
+ limit: num("Max cards 1..200 (default 100)."),
109
+ }),
110
+ },
111
+ "resource.get": {
112
+ title: "Get one deliverable",
113
+ description:
114
+ "One catalogue card by `slug` (the stable org-scoped natural key the portal, the " +
115
+ "/resources page and every tool here use) or by `resourceId` — pass EXACTLY ONE; " +
116
+ "neither or both is a BAD_REQUEST. Returns the card's own columns only. A card in " +
117
+ "another workspace answers NOT_FOUND identically to one that never existed, so this is " +
118
+ "not an existence oracle — do not read NOT_FOUND as \"it belongs to someone else\".",
119
+ input_schema: S({
120
+ slug: str("Card slug, e.g. \"deck-seed-round\" (preferred — stable across re-pushes)."),
121
+ resourceId: str("Card id (cuid). Alternative to slug; never pass both."),
122
+ }),
123
+ },
124
+ "resource.create": {
125
+ title: "Add a deliverable",
126
+ description:
127
+ "Add a card to the venture's catalogue (resource.create). `url` OR `fileId` must be " +
128
+ "present — a card pointing nowhere is the exact state this desk exists to end. Omit " +
129
+ "`slug` and the server derives one from kind + title (and suffixes -2, -3 on collision) " +
130
+ "using the SAME transform the portal uses, so a portal push and an agent create agree. " +
131
+ "`source` is NOT an input: the server stamps \"manual\", so an agent cannot relabel its " +
132
+ "own card as portal-provenance. This is NOT an upsert — an existing slug answers " +
133
+ "CONFLICT rather than overwriting a portal row someone guessed the slug of; correct an " +
134
+ "existing card with resource_update. `metadata` is free JSON (object only, ≤16KB) for " +
135
+ "provenance such as sha256/runId/documentVersion — NEVER put a credential in it; only " +
136
+ "its KEY NAMES ride the event chain.",
137
+ input_schema: S(
138
+ {
139
+ kind: str("Card kind, e.g. \"deck\" | \"one-pager\" | \"film\" | \"site\" (≤64 chars)."),
140
+ title: str("Human title as it appears at /resources (≤200 chars)."),
141
+ url: str("Public https URL (≤2048) or an in-app path like \"/files/abc123\". Required unless fileId is given."),
142
+ fileId: str("Workspace drive file id to cite. Required unless url is given; must be a file this seat can already see."),
143
+ slug: str("Optional explicit slug: lowercase a-z0-9 words joined by single hyphens, ≤64. Omit to let the server derive it."),
144
+ description: str("One or two sentences a human reads under the title (≤2000 chars)."),
145
+ previewImageUrl: str("Thumbnail URL (≤2048)."),
146
+ metadata: obj("Free-JSON provenance object, ≤16KB serialized. No secrets."),
147
+ sortOrder: num("Position 0..10000 (ties break on createdAt asc). Omit to append."),
148
+ },
149
+ ["kind", "title"],
150
+ ),
151
+ },
152
+ "resource.update": {
153
+ title: "Correct a deliverable",
154
+ description:
155
+ "Edit an existing card (resource.update), addressed by EXACTLY ONE of `slug` or " +
156
+ "`resourceId`. At least one mutable field is required, else BAD_REQUEST. `metadata` is a " +
157
+ "SHALLOW MERGE over what is already there — {...prior, ...patch} — and a key you pass as " +
158
+ "null is DELETED. It is deliberately not a whole-object replace: the artifact ingest " +
159
+ "writes sha256/runId/documentVersion/byteState/driveUrl into metadata, and a replace " +
160
+ "would let a one-field edit destroy the delivery provenance. Three fields are NOT " +
161
+ "editable here, each on purpose: `slug` (it is the ref AND the portal's re-sync key — " +
162
+ "renaming it would orphan the upsert and duplicate the card on the next push), `fileId` " +
163
+ "(re-pointing a card at a different drive row is a separately-audited act — use " +
164
+ "resource_attach_file), and `source` (provenance is server-owned).",
165
+ input_schema: S({
166
+ slug: str("Card slug to edit (preferred). Never pass both slug and resourceId."),
167
+ resourceId: str("Card id (cuid). Alternative to slug."),
168
+ kind: str("New kind (≤64 chars)."),
169
+ title: str("New title (≤200 chars)."),
170
+ description: str("New description (≤2000 chars); null clears it."),
171
+ url: str("New https URL (≤2048) or in-app path."),
172
+ previewImageUrl: str("New thumbnail URL (≤2048); null clears it."),
173
+ metadata: obj("Metadata PATCH — shallow-merged; a key set to null is removed. ≤16KB serialized."),
174
+ sortOrder: num("New position 0..10000. This is the PARTIAL nudge; resource_reorder is the total re-sort."),
175
+ }),
176
+ },
177
+ "resource.delete": {
178
+ title: "Remove a deliverable",
179
+ description:
180
+ "Remove a card from the catalogue (resource.delete), addressed by EXACTLY ONE of `slug` " +
181
+ "or `resourceId`. THE CARD ONLY: the WorkspaceFile it cited is NOT deleted and not even " +
182
+ "soft-deleted — the drive owns its own 30-day recovery lane — which is why the reply " +
183
+ "carries `fileRetained: true`. Relay that truth; do not tell a human the file is gone. " +
184
+ "The card's Cortex SOURCES entry goes with it, so a removed deliverable stops being " +
185
+ "discoverable. CAVEAT worth saying out loud: deleting a card whose `source` is " +
186
+ "\"ftlab-portal\" is not permanent — the next portal push re-upserts it on its natural " +
187
+ "key. If a portal card is wrong, fix it upstream or correct it with resource_update.",
188
+ input_schema: S({
189
+ slug: str("Card slug to remove (preferred). Never pass both slug and resourceId."),
190
+ resourceId: str("Card id (cuid). Alternative to slug."),
191
+ }),
192
+ },
193
+ "resource.reorder": {
194
+ title: "Re-sort the catalogue",
195
+ description:
196
+ "Set the whole catalogue's order (resource.reorder) — this is what changes what a human " +
197
+ "sees FIRST at /resources. TOTAL, not partial: `slugs` must name EVERY card in this " +
198
+ "workspace exactly once, and each gets sortOrder = its index. Anything else is a " +
199
+ "CONFLICT that names the missing, unknown and duplicated slugs back to you — which is " +
200
+ "the feature, not the friction: a card created by the portal while you were composing " +
201
+ "the list surfaces as a refusal instead of silently scrambling the order. So list first " +
202
+ "(resource_list), reorder from THAT list, and re-list if it refuses. To nudge one card " +
203
+ "without touching the rest, use resource_update { sortOrder } instead. Returns the full " +
204
+ "catalogue in its new order — no re-list needed.",
205
+ input_schema: S(
206
+ {
207
+ slugs: arr(
208
+ { type: "string" },
209
+ "Every slug in the workspace, exactly once, in the order you want (1..500).",
210
+ ),
211
+ },
212
+ ["slugs"],
213
+ ),
214
+ },
215
+ "resource.attachFile": {
216
+ title: "Cite a drive file on a card",
217
+ description:
218
+ "Point a catalogue card at a workspace drive file (resource.attachFile), addressed by " +
219
+ "EXACTLY ONE of `slug` or `resourceId`. This is a LINK write, not a byte write: it sets " +
220
+ "fileId and merges {driveUrl:\"/files/<fileId>\"} into metadata. hq resolves the file " +
221
+ "through the drive's own permission check, so you may only cite a file this seat can " +
222
+ "already see, and a file you cannot see answers NOT_FOUND exactly like one that does not " +
223
+ "exist. `setUrl` defaults to FALSE and should usually stay there: a deck, one-pager or " +
224
+ "film keeps its public URL because the venture's own site and outreach link that object " +
225
+ "— pass setUrl:true only when the card's destination really should become the in-app " +
226
+ "file. Attaching does not copy, move or re-share the file.",
227
+ input_schema: S(
228
+ {
229
+ fileId: str("Workspace drive file id to cite."),
230
+ slug: str("Card slug (preferred). Never pass both slug and resourceId."),
231
+ resourceId: str("Card id (cuid). Alternative to slug."),
232
+ setUrl: bool("Also rewrite the card's url to /files/<fileId>. Default false — an external URL is usually load-bearing."),
233
+ },
234
+ ["fileId"],
235
+ ),
236
+ },
237
+ "resource.detachFile": {
238
+ title: "Stop citing a drive file",
239
+ description:
240
+ "Clear a card's drive citation (resource.detachFile), addressed by EXACTLY ONE of `slug` " +
241
+ "or `resourceId`: fileId → null and the `driveUrl` key drops out of metadata. The " +
242
+ "WorkspaceFile is untouched. The card's `url` is left exactly as it was UNLESS you pass " +
243
+ "a replacement `url`. One guard: if the card's only destination IS the file being " +
244
+ "detached (its url is /files/<fileId>), `url` is REQUIRED and the call answers CONFLICT " +
245
+ "without it — otherwise this verb would manufacture the dead card the whole family " +
246
+ "exists to prevent.",
247
+ input_schema: S({
248
+ slug: str("Card slug (preferred). Never pass both slug and resourceId."),
249
+ resourceId: str("Card id (cuid). Alternative to slug."),
250
+ url: str("Replacement destination — required when the card's url is the /files/<fileId> being detached."),
251
+ }),
252
+ },
253
+ });
254
+
255
+ /**
256
+ * `resource.attachFile` → `resource_attach_file`. Dots become underscores and
257
+ * camelCase humps split, so the tool name is a pure function of the protocol
258
+ * name and cannot be typed differently from it.
259
+ * @param {string} method
260
+ * @returns {string}
261
+ */
262
+ export function toolNameForMethod(method) {
263
+ return String(method)
264
+ .replace(/\./g, "_")
265
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
266
+ .toLowerCase();
267
+ }
268
+
269
+ /**
270
+ * Build the curated `resource_*` entries from the VENDORED protocol table.
271
+ *
272
+ * Order follows the protocol declaration, so the tool block reads in the order
273
+ * the family was designed in. Returns `[]` when the vendored protocol carries
274
+ * no `resource` family at all — an older protocol degrades cleanly, exactly
275
+ * like the desk gates.
276
+ *
277
+ * A family method with no presentation entry still gets a tool, built from the
278
+ * descriptor: an under-described capability is a documentation bug, but a
279
+ * MISSING one is the failure this whole wave was called to fix, so drift
280
+ * surfaces as a red test (resource-tools.test.mjs asserts the presentation
281
+ * table covers the family exactly) and never as a dark verb.
282
+ *
283
+ * @param {Record<string, {family?:string, scope?:string, sideEffecting?:boolean}>} [methods]
284
+ * @returns {Array<object>}
285
+ */
286
+ export function buildResourceTools(methods = METHODS) {
287
+ const out = [];
288
+ for (const [method, def] of Object.entries(methods || {})) {
289
+ if (!def || def.family !== RESOURCE_FAMILY) continue;
290
+ // Scope is the authority (it is what hq gates on). The `??` arm only fires
291
+ // for a method the REAL vendored table does not carry — i.e. a synthetic
292
+ // table under test — and mirrors sideEffecting so such a row still lands on
293
+ // the correct side of the read/write line rather than defaulting open.
294
+ const access = expectedAccessFor(method) ?? (def.sideEffecting ? "write" : "read");
295
+ const p = RESOURCE_PRESENTATION[method];
296
+ out.push({
297
+ name: toolNameForMethod(method),
298
+ title: p ? p.title : method,
299
+ description: p
300
+ ? p.description
301
+ : `Undocumented ${RESOURCE_FAMILY} verb (${method}) — generated from the protocol ` +
302
+ `declaration so the capability is reachable; add a presentation entry in ` +
303
+ `lib/org/resource-tools.mjs.`,
304
+ input_schema: p ? p.input_schema : S({}),
305
+ access,
306
+ desk: RESOURCE_DESK,
307
+ binding: { kind: "rpc", method },
308
+ });
309
+ }
310
+ return out;
311
+ }
312
+
313
+ /**
314
+ * The generated block, spliced into `ORG_TOOLS` by lib/org/tool-surface.mjs.
315
+ * @type {ReadonlyArray<object>}
316
+ */
317
+ export const RESOURCE_TOOLS = Object.freeze(buildResourceTools());