@cohortapp/agent-sdk 2.9.1 → 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 (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 +4 -0
  5. package/lib/comms/send-gate.mjs +23 -1
  6. package/lib/comms/send-gate.test.mjs +24 -0
  7. package/lib/mcp/server.test.mjs +16 -4
  8. package/lib/model-router/economics.mjs +53 -1
  9. package/lib/model-router/economics.test.mjs +76 -0
  10. package/lib/model-router/resolve.mjs +57 -4
  11. package/lib/model-router.mjs +95 -8
  12. package/lib/model-router.test.mjs +305 -5
  13. package/lib/org/client.mjs +58 -1
  14. package/lib/org/inbound/project.mjs +9 -5
  15. package/lib/org/messaging.mjs +6 -1
  16. package/lib/org/protocol.checksum +1 -1
  17. package/lib/org/protocol.mjs +176 -3
  18. package/lib/org/protocol.test.mjs +31 -2
  19. package/lib/org/resource-tools.mjs +317 -0
  20. package/lib/org/resource-tools.test.mjs +361 -0
  21. package/lib/org/tool-access.mjs +176 -0
  22. package/lib/org/tool-access.test.mjs +144 -0
  23. package/lib/org/tool-surface.mjs +431 -5
  24. package/lib/org/tool-surface.test.mjs +385 -8
  25. package/lib/org/ui-parity.mjs +196 -3
  26. package/lib/org/ui-parity.test.mjs +126 -7
  27. package/lib/tool-definitions.js +23 -2
  28. package/package.json +2 -2
  29. package/plugins/maestro-skills/.claude-plugin/marketplace.json +1 -1
  30. package/plugins/maestro-skills/plugin.json +4 -0
  31. package/plugins/maestro-skills/skills/venture-deliverables.md +176 -0
  32. package/policies/information-barriers.yaml +34 -7
  33. package/scripts/ci/check-no-residual-identity.mjs +281 -9
  34. package/scripts/ci/check-no-residual-identity.test.mjs +115 -2
  35. package/scripts/cloud-relay/voice/relay-identity.test.mjs +96 -0
  36. package/scripts/cloud-relay/voice/server.mjs +42 -2
  37. package/scripts/cost/track-claude-usage-pricing.test.mjs +183 -0
  38. package/scripts/cost/track-claude-usage.mjs +113 -4
  39. package/scripts/daemon/agent-daemon.mjs +150 -3
  40. package/scripts/daemon/agent-daemon.test.mjs +190 -0
  41. package/scripts/daemon/assurance.mjs +50 -16
  42. package/scripts/daemon/assurance.test.mjs +39 -1
  43. package/scripts/daemon/classifier-identity.test.mjs +137 -0
  44. package/scripts/daemon/classifier.mjs +98 -17
  45. package/scripts/daemon/deliver.mjs +457 -33
  46. package/scripts/daemon/deliver.test.mjs +564 -0
  47. package/scripts/daemon/prompt-builder-preamble.test.mjs +210 -0
  48. package/scripts/daemon/prompt-builder.mjs +264 -41
  49. package/scripts/daemon/prompt-builder.test.mjs +5 -5
  50. package/scripts/daemon/responder-history.test.mjs +18 -2
  51. package/scripts/daemon/responder.mjs +7 -1
  52. package/scripts/disclosure_boundaries.py +56 -5
  53. package/scripts/huddle/huddle-prompt.test.mjs +176 -0
  54. package/scripts/huddle/huddle-server.mjs +128 -13
  55. package/scripts/local-triggers/autoupdate.sh +83 -0
  56. package/scripts/local-triggers/generate-plists.sh +9 -0
  57. package/scripts/local-triggers/generate-plists.test.mjs +12 -10
  58. package/scripts/media-generation/brand-clause.test.mjs +135 -0
  59. package/scripts/media-generation/gemini-image-client.mjs +27 -9
  60. package/scripts/media-generation/generate-assets.mjs +102 -7
  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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * ui-parity.test.mjs — Agent UI-parity helpers (full human-action mirror).
3
3
  *
4
- * The 359 wrappers in ui-parity.mjs are each a one-liner over call() from
4
+ * The 360 wrappers in ui-parity.mjs are each a one-liner over call() from
5
5
  * client.mjs, so client.test.mjs already proves the transport contract (headers,
6
6
  * idempotency, fail-open, frame normalisation). Here we just prove a representative
7
7
  * slice across families ROUTES to the correct method name, POSTs the params body,
@@ -326,7 +326,7 @@ test("a human-gated directory verb surfaces the server's refusal verbatim, never
326
326
  assert.match(frame.error.message, /human/);
327
327
  });
328
328
 
329
- test("every desk protocol method has exactly one ui-parity wrapper (196 desks / 359 total)", async () => {
329
+ test("every desk protocol method has exactly one ui-parity wrapper (196 desks / 373 total)", async () => {
330
330
  const fs = await import("node:fs");
331
331
  const path = await import("node:path");
332
332
  const url = await import("node:url");
@@ -340,7 +340,15 @@ test("every desk protocol method has exactly one ui-parity wrapper (196 desks /
340
340
  // NONE of calling's 22 — so the huddle control, the calendar Join, host
341
341
  // mute/lock/remove, hand-raising and in-call chat had no wrapper at all.
342
342
  // +9 messaging +22 calling → 328 → 359.
343
- assert.equal(called.length, 359, "one call() site per wrapper");
343
+ // 2026-08 voice delta: messaging.synthesizeVoiceNote — the agent-plane twin
344
+ // of the composer's hold-to-record button, which the messaging family had no
345
+ // wrapper for at all → 360.
346
+ // 2026-08 mandate delta: the whole accountability spine (11 methods) was
347
+ // protocol-declared and wrapped nowhere, so `lib/mandate/refresh.mjs` reached
348
+ // it by spelling the method name out as a string constant of its own → 371.
349
+ // 2026-08 preference delta: hq built `preference.*` FOR this plane (its own
350
+ // handler docblock says so) and the SDK wrapped neither half → 373.
351
+ assert.equal(called.length, 373, "one call() site per wrapper");
344
352
  assert.equal(new Set(called).size, called.length, "no duplicate method bindings");
345
353
  // The two conversation families are now WHOLE, which is what makes the
346
354
  // module docblock's claim true rather than aspirational.
@@ -348,8 +356,11 @@ test("every desk protocol method has exactly one ui-parity wrapper (196 desks /
348
356
  const famMethods = Object.entries((await import("./protocol.mjs")).METHODS)
349
357
  .filter(([, d]) => d.family === fam)
350
358
  .map(([n]) => n);
351
- const unwrapped = famMethods.filter((m) => !new Set(called).has(m));
352
- assert.deepEqual(unwrapped, [], `every ${fam}.* method is wrapped`);
359
+ // Whole MINUS the declared daemon-internal exceptions (the same ledger the
360
+ // sibling test enforces) — e.g. messaging.electResponder is the daemon's
361
+ // responder-election transport, not a human-app verb.
362
+ const unwrapped = famMethods.filter((m) => !new Set(called).has(m) && !DELIBERATELY_UNWRAPPED.has(m));
363
+ assert.deepEqual(unwrapped, [], `every ${fam}.* method is wrapped or declared daemon-internal`);
353
364
  }
354
365
  const p = await import("./protocol.mjs");
355
366
  const deskFams = new Set(["books", "calendar", "crm", "directory", "files"]);
@@ -398,6 +409,11 @@ const DELIBERATELY_UNWRAPPED = new Map([
398
409
  ["integration.listAgentTools", "runtime toolset plane (daemon poll)"],
399
410
  ["integration.invokeTool", "runtime toolset plane (executor)"],
400
411
  ["meetings.record", "knowledge plane writer, not the desk verb"],
412
+ // The responder-election plane: the daemon asks hq who — if anyone — should
413
+ // answer an undirected channel message. Server-decided and daemon-driven
414
+ // (scripts/daemon/agent-daemon.mjs, via lib/org/client.electResponder); a human
415
+ // never elects a responder, so there is no human-app verb to wrap.
416
+ ["messaging.electResponder", "responder-election plane — daemon-driven (client.electResponder)"],
401
417
  ]);
402
418
 
403
419
  test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning exception list", async () => {
@@ -415,7 +431,8 @@ test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning ex
415
431
  "escalation", "annotation", "contact", "file", "memory", "compact",
416
432
  "notification", "decision", "messaging", "calling", "board", "invitation",
417
433
  "user", "settings", "billing", "org", "integration", "email", "artifact",
418
- "books", "calendar", "crm", "directory", "files",
434
+ "books", "calendar", "crm", "directory", "files", "meetings", "mandate",
435
+ "preference",
419
436
  ];
420
437
  const undeclared = [];
421
438
  for (const name of Object.keys(p.METHODS)) {
@@ -437,5 +454,107 @@ test("the claimed families are wrapped WHOLE, minus a declared, self-cleaning ex
437
454
  assert.deepEqual(notMethods, [], "these are not protocol methods — delete their exception lines");
438
455
 
439
456
  // And the docblock's own numbers stay honest.
440
- assert.equal(wrapped.size, 359, "the docblock's wrapper count");
457
+ assert.equal(wrapped.size, 373, "the docblock's wrapper count");
458
+ });
459
+
460
+ /**
461
+ * THE FAMILIES THIS MODULE DOES NOT CLAIM, and who carries each instead.
462
+ *
463
+ * The guard above is airtight INSIDE `CLAIMED` and blind at its edge: it walks
464
+ * the protocol table and `continue`s on any family not in the list, so a NEWLY
465
+ * DECLARED family lands wrapped by nobody and fails no test. That is not
466
+ * hypothetical — genesis, subagent and mandate all arrived dark in one
467
+ * fortnight and were found by a census, not by CI.
468
+ *
469
+ * So every family in `METHODS` must now appear either in `CLAIMED` or here,
470
+ * with the surface that actually carries it. Two entries deliberately record a
471
+ * REFUSAL rather than a home, and two record a genuine hole; that is the point
472
+ * of a ledger — the honest answer is allowed, silence is not.
473
+ */
474
+ const UNCLAIMED_FAMILIES = new Map([
475
+ // The org spine: named wrappers live in client.mjs beside the transport they
476
+ // use, and predate this module. Moving them would break every importer.
477
+ ["approval", "client.mjs approvalRequest/Resolve/Get/List + lib/org/approvals.mjs; curated approval_request/approval_wait"],
478
+ ["branding", "client.mjs (said in this module's own docblock); curated design_* tools"],
479
+ ["contacts", "client.mjs contactsUpsert — the knowledge plane's contact writer"],
480
+ ["cost", "client.mjs costReport + lib/org/cost-sync.mjs"],
481
+ ["credential", "client.mjs credentialPut/Lease/List/Revoke + lib/org/keys.mjs — a secret broker, not a pane"],
482
+ ["governance", "client.mjs governanceStatus/setDecisionRights — the §0.4 control plane"],
483
+ ["handoff", "client.mjs + lib/org/handoff.mjs — governance-gated delegation"],
484
+ ["knowledge", "client.mjs + lib/org/knowledge.mjs; curated knowledge_search/knowledge_append"],
485
+ ["lease", "client.mjs + lib/org/leases.mjs — the work kernel's lock primitive"],
486
+ ["policy", "client.mjs policyPublish + lib/org/policy.mjs"],
487
+ ["presence", "client.mjs presenceBeat + lib/org/registry.mjs — the daemon's own heartbeat"],
488
+ ["registry", "client.mjs register/heartbeat + lib/org/registry.mjs — enrolment, not a click"],
489
+ ["pairing", "client.mjs pairRequest — PRE-AUTH, deliberately off the authenticated call() path"],
490
+ ["agent", "agent.wait is the long-poll TRANSPORT rung (lib/org/push.mjs DEFAULT_WAIT_PATH), not a surface"],
491
+ // The registry family has a home with state a thin wrapper would silently
492
+ // lose. Reads reach a session through the curated subagent_* tools.
493
+ ["subagent", "lib/subagents/client.mjs — owns the offline outbox + lock file; curated subagent_list/get/resolve for the reads"],
494
+ // The venture-deliverable catalogue (2026-08). Its maestro surface is the
495
+ // CURATED table, not a thin one-liner per method: the eight `resource_*` desk
496
+ // tools are GENERATED from hq's own desk declaration and parity-tested
497
+ // against it, because hand-copying a desk table is precisely what produced
498
+ // the 239-ops-vs-69-tools drift this wave exists to stop. A hand-written
499
+ // wrapper here would be a third copy of the same eight names.
500
+ ["resource", "lib/org/tool-surface.mjs — curated resource_* desk tools, GENERATED from hq's desk declaration (never hand-copied) and parity-tested against it"],
501
+ // REFUSALS, not gaps. hq's `requireGenesisSeat` throws UNAUTHORIZED on any
502
+ // actor whose kind is not "human" (src/server/methods/genesis/_shared.ts),
503
+ // and architecture.* imports that same gate. A Genesis run belongs to
504
+ // (orgId, startedById) — one workspace AND one person — and an api-key
505
+ // principal is neither. Wrappers here would be dead code that always 401s,
506
+ // which is a worse surface than an honest absence.
507
+ ["genesis", "hq refuses every non-human actor (requireGenesisSeat) — unreachable from a bearer key by design"],
508
+ ["architecture", "same gate as genesis (it imports requireGenesisSeat) — unreachable from a bearer key by design"],
509
+ // HOLES, recorded as holes. Both are admin-scope writes with no SDK caller
510
+ // today; neither is reachable below the CEO tier even through org_rpc.
511
+ ["admin", "GAP — the deactivate/reactivate kill switch has no wrapper and no tool; admin scope, operator console only"],
512
+ ["hierarchy", "GAP — hierarchy.set (admin scope) has no wrapper; reporting edges are set in hq's own org chart today"],
513
+ ]);
514
+
515
+ test("every protocol family is CLAIMED or has a written line saying who carries it", async () => {
516
+ const p = await import("./protocol.mjs");
517
+
518
+ // Kept in step with the guard above by construction: read the same list.
519
+ const CLAIMED = new Set([
520
+ "charter", "member", "team", "channel", "persona", "profile", "sop",
521
+ "escalation", "annotation", "contact", "file", "memory", "compact",
522
+ "notification", "decision", "messaging", "calling", "board", "invitation",
523
+ "user", "settings", "billing", "org", "integration", "email", "artifact",
524
+ "books", "calendar", "crm", "directory", "files", "meetings", "mandate",
525
+ "preference",
526
+ ]);
527
+
528
+ const families = new Set(Object.values(p.METHODS).map((d) => d.family));
529
+ const undeclared = [...families]
530
+ .filter((f) => !CLAIMED.has(f) && !UNCLAIMED_FAMILIES.has(f))
531
+ .sort();
532
+ // If this fails, a family was added to protocol.mjs and nothing on this plane
533
+ // noticed. Wrap it (and add it to CLAIMED), or add an UNCLAIMED_FAMILIES line
534
+ // naming the surface that carries it. "It's reachable via org_rpc" is not an
535
+ // answer: org_rpc is access:"admin" and does not exist below the CEO tier.
536
+ assert.deepEqual(undeclared, [], "every protocol family is claimed or declared");
537
+
538
+ // Self-cleaning: a declared family that has since been claimed, or that is no
539
+ // longer a protocol family at all, must lose its line rather than rot.
540
+ const nowClaimed = [...UNCLAIMED_FAMILIES.keys()].filter((f) => CLAIMED.has(f));
541
+ assert.deepEqual(nowClaimed, [], "these are claimed now — delete their ledger lines");
542
+ const gone = [...UNCLAIMED_FAMILIES.keys()].filter((f) => !families.has(f));
543
+ assert.deepEqual(gone, [], "these are not protocol families — delete their ledger lines");
544
+
545
+ // The two CLAIMED lists must stay identical, or one guard silently narrows.
546
+ const src = (await import("node:fs")).readFileSync(
547
+ (await import("node:path")).join(
548
+ (await import("node:path")).dirname(
549
+ (await import("node:url")).fileURLToPath(import.meta.url),
550
+ ),
551
+ "ui-parity.test.mjs",
552
+ ),
553
+ "utf8",
554
+ );
555
+ const blocks = [...src.matchAll(/const CLAIMED = (?:new Set\()?\[([\s\S]*?)\]/g)].map((m) =>
556
+ [...m[1].matchAll(/"([a-z]+)"/g)].map((x) => x[1]).sort().join(","),
557
+ );
558
+ assert.equal(blocks.length, 2, "both guards declare a CLAIMED list");
559
+ assert.equal(blocks[0], blocks[1], "the two CLAIMED lists must not drift apart");
441
560
  });
@@ -30,6 +30,11 @@
30
30
  */
31
31
 
32
32
  import { getOrgTools, loadIntegrationToolsFromDisk } from "./org/tool-surface.mjs";
33
+ // The tool-side twin of normalizeAccessLevel below. Exact string equality used to
34
+ // drop any entry whose `access` was missing or mis-cased out of EVERY tier —
35
+ // see lib/org/tool-access.mjs for why that is the same footgun, on the other
36
+ // side of the same comparison.
37
+ import { partitionByAccess } from "./org/tool-access.mjs";
33
38
 
34
39
  // ============================================================================
35
40
  // 1. COMMUNICATION TOOLS
@@ -573,8 +578,24 @@ const generateReport = {
573
578
  const ORG_TOOL_DEFS = getOrgTools({}).filter((t) => !t.integration);
574
579
  const asClaudeTool = ({ name, description, input_schema }) => ({ name, description, input_schema });
575
580
  const orgTools = ORG_TOOL_DEFS.map(asClaudeTool);
576
- const orgToolsByAccess = (access) =>
577
- ORG_TOOL_DEFS.filter((t) => t.access === access).map(asClaudeTool);
581
+
582
+ // EXHAUSTIVE partition — every curated tool lands in exactly one tier.
583
+ //
584
+ // This replaces three independent `filter(t => t.access === x)` passes, which
585
+ // silently agreed to drop any entry they all disagreed with: a tool whose
586
+ // `access` was absent or mis-cased ("Read", " write ") matched no bucket and
587
+ // therefore reached NO native access level — not ceo, not leadership, not
588
+ // voice, not default — while the MCP plane kept publishing it, because
589
+ // lib/mcp/server.mjs reads `access` only to set `readOnlyHint`. A capability
590
+ // could be fully built, fully tested and invisible on this plane alone, with
591
+ // nothing anywhere saying so. That is the same silent UNDER-privilege footgun
592
+ // `normalizeAccessLevel` (below) was written to kill on the CALLER's side of
593
+ // the very same string comparison, left unfixed on the TOOL's side.
594
+ // partitionByAccess normalises casing and sends a genuinely unrecognised tier
595
+ // to the most-restricted bucket with a loud warning: fail closed, fail noisy,
596
+ // never fail invisible.
597
+ const ORG_TOOLS_BY_TIER = partitionByAccess(ORG_TOOL_DEFS);
598
+ const orgToolsByAccess = (access) => (ORG_TOOLS_BY_TIER[access] || []).map(asClaudeTool);
578
599
 
579
600
  /**
580
601
  * The workspace's GRANTED integration tools (SP5), read FRESH from the shared
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cohortapp/agent-sdk",
3
- "version": "2.9.1",
4
- "description": "Cohort Agent SDK \u2014 autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
3
+ "version": "2.11.0",
4
+ "description": "Cohort Agent SDK — autonomous AI colleague runtime. Deploy senior AI colleagues on dedicated Mac minis, wired to the Cohort operating surface.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agent-sdk": "./bin/maestro.mjs",
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "maestro-skills",
12
12
  "source": "./",
13
- "description": "Maestro operational skills — executive briefings, communications, hiring, strategy, org-desk workflows (mail, files, calendar, CRM, books, directory, design) and Cohort call working sessions."
13
+ "description": "Maestro operational skills — executive briefings, communications, hiring, strategy, org-desk workflows (mail, files, calendar, CRM, books, directory, design, venture deliverables) and Cohort call working sessions."
14
14
  }
15
15
  ]
16
16
  }
@@ -131,6 +131,10 @@
131
131
  "name": "brand-steward",
132
132
  "description": "Work the org's Design section — read the brand foundation and voice, rewrite copy in that voice, render and export templates and the brand kit, regenerate stale imagery within a budget, and propose reviewable foundation changes. Use before writing anything the workspace publishes, to produce a branded asset, or when imagery/templates have gone stale."
133
133
  },
134
+ {
135
+ "name": "venture-deliverables",
136
+ "description": "Work the venture's deliverable catalogue — the cards a founder actually sends an investor (deck, one-pager, film, site, data room), their order, their provenance, and their link to the workspace drive. Use to answer \"what have we produced\", to file or correct a deliverable after producing one, to re-sort what an outsider sees first, or to point a card at the file you just wrote."
137
+ },
134
138
  {
135
139
  "name": "call-working-sessions",
136
140
  "description": "Turn a Cohort call into a working session — present a doc/sheet/channel/board/charter on the call stage, step through it (scroll/highlight/type), live-edit docs while talking, read what participants see on screen, and watch reactions and raised hands. Use when you are a participant in a live Cohort call and want to show rather than tell, walk people through a document, or ground \"this chart / that number\" talk in what is actually on screen."
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: venture-deliverables
3
+ description: Work the venture's deliverable catalogue — the cards a founder actually sends an investor (deck, one-pager, film, site, data room), their order, their provenance, and their link to the workspace drive. Use to answer "what have we produced", to file or correct a deliverable after producing one, to re-sort what an outsider sees first, or to point a card at the file you just wrote.
4
+ ---
5
+
6
+ # Venture Deliverables
7
+
8
+ `/resources` is the venture's own catalogue: one card per artifact the venture
9
+ has produced — deck, one-pager, film, site, data room, brand kit. It is not a
10
+ folder and not an inbox. It is the short list a founder sends an investor, and
11
+ the order of it is the order they are asked to look in.
12
+
13
+ Until this family shipped, no agent could see it. Colleagues could write a deck
14
+ and had no way to say the venture *had* one. That is the gap these eight tools
15
+ close: `resource_list` / `resource_get` (every seat, including least-privilege
16
+ ones), and `resource_create` / `resource_update` / `resource_delete` /
17
+ `resource_reorder` / `resource_attach_file` / `resource_detach_file` (an
18
+ EDITOR-equivalent seat). All of it is `executeOrgTool` — fail-open frames, never
19
+ a throw.
20
+
21
+ ## Read it before you claim anything about it
22
+
23
+ ```bash
24
+ node --input-type=module -e '
25
+ import { executeOrgTool } from "./lib/org/tool-surface.mjs";
26
+ const res = await executeOrgTool("resource_list", { limit: 200 });
27
+ for (const r of res.ok ? res.result.resources : [])
28
+ console.log(r.sortOrder, r.slug, `[${r.kind}]`, r.source, r.fileId ?? "-", r.url);
29
+ '
30
+ ```
31
+
32
+ The order you get back is byte-identical to the order a human sees at
33
+ `/resources` — same `sortOrder asc, createdAt asc`. Quote it as-is; a
34
+ re-derivation can only disagree with the page.
35
+
36
+ ## Provenance decides whether your edit survives
37
+
38
+ `source` is the single most load-bearing field on a card, and no schema can tell
39
+ you why:
40
+
41
+ - **`ftlab-portal`** — pushed from the venture portal and **re-upserted on its
42
+ natural key on every push**. Your delete comes back. Your retitle is
43
+ overwritten. If a portal card is wrong, the fix is upstream in the portal, not
44
+ here; correcting it here buys you the time until the next push and no longer.
45
+ - **`genesis`** — laid down when the venture was stood up.
46
+ - **`manual`** — created through this desk. These are the ones that are actually
47
+ yours to own.
48
+
49
+ You cannot set `source` yourself. `resource_create` always stamps `manual`, so a
50
+ card an agent made can never masquerade as portal-provenance. And `create` is
51
+ **not** an upsert: an existing slug answers `CONFLICT` rather than quietly
52
+ overwriting a portal row whose slug you guessed. Correct with `resource_update`.
53
+
54
+ ## Filing a deliverable you just produced
55
+
56
+ Two steps, in this order — the card is the claim, the file is the object.
57
+
58
+ ```bash
59
+ node --input-type=module -e '
60
+ import { executeOrgTool } from "./lib/org/tool-surface.mjs";
61
+ const made = await executeOrgTool("resource_create", {
62
+ kind: "one-pager",
63
+ title: "Seed one-pager — Aug 2026",
64
+ description: "Two-page summary for warm intros. Supersedes the June version.",
65
+ url: "https://acme.vc/one-pager.pdf",
66
+ });
67
+ console.log(made.ok ? made.result.resource.slug : made.error);
68
+ '
69
+ ```
70
+
71
+ Then, if the artifact is also filed in the workspace drive (the artifact ingest
72
+ does this automatically for portal-crossed artifacts), cite it:
73
+ `resource_attach_file { slug, fileId }`. Leave `setUrl` at its default `false` —
74
+ a deck, one-pager or film keeps its public URL because the venture's own site
75
+ and outreach link that object, and rewriting it to `/files/<id>` breaks those
76
+ links. Pass `setUrl:true` only when the in-app file genuinely *is* the
77
+ destination.
78
+
79
+ Write the `description` like it will be searched, because it will be: hq indexes
80
+ each card into the org's shared recall with the URL inside the summary text. A
81
+ card described as "deck" is findable by nobody; "Seed deck, 14 slides, used in
82
+ the Sept partner meetings" is.
83
+
84
+ ## The catalogue is an index, not a drive
85
+
86
+ A card **cites** a file; a `WorkspaceFile` **is** one. `resource_*` returns the
87
+ card's own columns and a `fileId` — never bytes, name, size, version or share
88
+ list. That is a share boundary, not tidiness: the catalogue is org-wide, the
89
+ drive is seat-scoped, so a resource tool that re-served file content would be a
90
+ one-call way around the drive's own share filter.
91
+
92
+ So: read the card here, read the **object** with `files_get` / `files_doc_read` /
93
+ `org_rpc files.exportRequest`. If the drive answers `NOT_FOUND` on a `fileId` a
94
+ card carries, that is the honest answer — either the file was swept or your seat
95
+ is not shared on it, and those two are deliberately indistinguishable. Ask for
96
+ the share; do not route around it, and do not report "the deliverable is
97
+ missing" when what you hit was an ACL.
98
+
99
+ ## Re-sorting is total, and the refusal is the feature
100
+
101
+ `resource_reorder` takes **every** slug in the workspace, exactly once. Anything
102
+ else is a `CONFLICT` that names the missing / unknown / duplicated slugs back to
103
+ you.
104
+
105
+ ```bash
106
+ node --input-type=module -e '
107
+ import { executeOrgTool } from "./lib/org/tool-surface.mjs";
108
+ const cur = await executeOrgTool("resource_list", { limit: 200 });
109
+ const slugs = cur.result.resources.map((r) => r.slug);
110
+ const first = "film-founder-story";
111
+ const res = await executeOrgTool("resource_reorder", {
112
+ slugs: [first, ...slugs.filter((s) => s !== first)],
113
+ });
114
+ console.log(res.ok ? res.result.resources.map((r) => r.slug) : res.error.message);
115
+ '
116
+ ```
117
+
118
+ List first, reorder from *that* list, and if it refuses, re-list rather than
119
+ retrying: a `CONFLICT` means the catalogue changed under you — usually a portal
120
+ push — and blind retry would scramble somebody's order. To nudge one card
121
+ without touching the rest, use `resource_update { sortOrder }` instead.
122
+
123
+ ## Corrections without collateral damage
124
+
125
+ `resource_update` **shallow-merges** `metadata` (`{...prior, ...patch}`), and a
126
+ key you send as `null` is deleted. This matters more than it looks: the artifact
127
+ ingest writes `sha256`, `runId`, `documentVersion`, `byteState` and `driveUrl`
128
+ into that object, and if it were a whole-object replace, fixing a typo in a
129
+ title would erase the delivery provenance of the artifact. Patch the keys you
130
+ mean and leave the rest alone.
131
+
132
+ Three fields are deliberately not editable, and it is worth knowing why before
133
+ you go looking for them:
134
+
135
+ - **`slug`** — it is both the ref and the portal's re-sync key. Renaming it would
136
+ orphan the upsert and produce a duplicate card on the next push.
137
+ - **`fileId`** — re-pointing a card at a different drive row is its own audited
138
+ act: `resource_attach_file` / `resource_detach_file`.
139
+ - **`source`** — provenance is the server's to assign.
140
+
141
+ ## Removing one
142
+
143
+ `resource_delete` removes **the card only**. The `WorkspaceFile` it cited is not
144
+ deleted, not even soft-deleted — the drive keeps its own 30-day recovery lane —
145
+ which is why the reply carries `fileRetained: true`. Say that out loud when you
146
+ report a deletion: "removed the card; the file is still in the drive" is the
147
+ truth, and "deleted the deck" is not.
148
+
149
+ `resource_detach_file` refuses with `CONFLICT` if the card's only destination is
150
+ the file you are detaching, unless you supply a replacement `url`. A card that
151
+ points nowhere is precisely the state this desk exists to prevent.
152
+
153
+ ## Rules of the desk
154
+
155
+ - A card is a **claim the venture made this thing**. Never create one for an
156
+ artifact that does not exist yet, and never invent a `url` — if there is no
157
+ destination, there is no card.
158
+ - `metadata` is free JSON with no secret column behind it. Never put a token,
159
+ key or signed URL in it. Only its *key names* ride the event chain; the values
160
+ are yours to keep honest.
161
+ - A `FORBIDDEN` frame is your seat's scope, not a bug. Reads ride `org.read`
162
+ (every seat has it); the six writes ride `org.write` (EDITOR-equivalent). Ask
163
+ for the seat rather than working around it.
164
+ - Cite cards to humans by **slug + title**, and say the `source` when you are
165
+ about to change something a portal push will undo.
166
+ - Every write appends exactly one chain event, so a human can see what you did.
167
+ Write like that is true.
168
+
169
+ ## Notes
170
+
171
+ - These tools register only when the vendored protocol carries the `resource`
172
+ family. `org_describe { family: "resource" }` lists the eight methods and their
173
+ scopes offline, with no credential.
174
+ - The human surface for the same rows is `/resources`. If what you report and
175
+ what that page shows ever differ, the page is right and you have a bug —
176
+ `resource_list` is deliberately the same query in the same order.
@@ -5,10 +5,23 @@
5
5
  # ─────────────────────────────────────────────
6
6
  # Domain Taxonomy
7
7
  # Used in provenance context_domain and profile boundaries
8
+ #
9
+ # THESE DESCRIPTIONS ARE EXECUTABLE, NOT DOCUMENTATION.
10
+ # scripts/daemon/context-compiler.mjs#loadDomainDescriptions regex-parses each
11
+ # `description:` string into the compiled prompt, and #buildDomainKeywords
12
+ # turns the same strings into the live keyword matcher that assigns a domain
13
+ # to an item. Every 4+-character word here is therefore a classification
14
+ # keyword on every deployment.
15
+ #
16
+ # They must stay COMPANY-NEUTRAL. They previously named a specific (fictional)
17
+ # employer and a specific (fictional) joint-venture counterparty, so those two
18
+ # invented proper nouns were live classification keywords for every customer —
19
+ # matching nothing real, while the neutral words that should have matched were
20
+ # diluted. Describe the SHAPE of the information, never the name of a party.
8
21
  # ─────────────────────────────────────────────
9
22
  domains:
10
23
  internal-legal:
11
- description: "Northwind's own legal strategy, GC advice, privilege-protected"
24
+ description: "The company's own legal strategy, general counsel advice, privilege-protected"
12
25
  default_sensitivity: high
13
26
  financial:
14
27
  description: "Cap table, runway, fundraising, financial planning"
@@ -23,19 +36,19 @@ domains:
23
36
  description: "Approach to regulators, gap assessments, remediation"
24
37
  default_sensitivity: high
25
38
  strategic-initiative:
26
- description: "the strategic initiative M&A targets, deal terms, pipeline"
39
+ description: "Strategic initiative M&A targets, deal terms, pipeline"
27
40
  default_sensitivity: critical
28
41
  board-governance:
29
42
  description: "Board materials, resolutions, governance"
30
43
  default_sensitivity: high
31
44
  partner-operations:
32
- description: "PartnerCo JV operational matters (shared domain)"
45
+ description: "Partner or joint-venture operational matters (shared domain)"
33
46
  default_sensitivity: low
34
47
  partner-commercial:
35
- description: "PartnerCo commercial / revenue matters (shared domain)"
48
+ description: "Partner or joint-venture commercial and revenue matters (shared domain)"
36
49
  default_sensitivity: medium
37
50
  partner-internal:
38
- description: "Northwind-side-only PartnerCo matters (negotiation position, internal JV strategy)"
51
+ description: "Company-side-only partner matters (negotiation position, internal joint-venture strategy)"
39
52
  default_sensitivity: high
40
53
  investor-relations:
41
54
  description: "Investor communications, fundraising pipeline"
@@ -106,8 +119,22 @@ assessment:
106
119
  medium: strip_and_send
107
120
  low: warn_and_send
108
121
  recent_context_hours: 48
109
- unrestricted_recipients:
110
- - alex-chen
122
+
123
+ # DEFAULT DENY. A slug listed here is granted blanket clearance across EVERY
124
+ # information domain — scripts/disclosure_boundaries.py#_build_clearance_summary
125
+ # renders "<name> has unrestricted access — all information domains are
126
+ # permitted" for them, and the assessment path treats them as unrestricted.
127
+ #
128
+ # It shipped populated with a fictional example recipient, which handed a
129
+ # person who exists in no customer's org a standing all-domains clearance on
130
+ # every deployment. Empty is the only safe shipped value, and it costs nothing:
131
+ # disclosure_boundaries.py already default-denies an undeclared domain.
132
+ #
133
+ # WIZARD STEP: `/init-maestro` may offer to populate this from
134
+ # config/agent.json `principal` — but ONLY after the operator explicitly
135
+ # confirms that person should bypass every barrier. Never populate it
136
+ # automatically, and never infer it from a contact list.
137
+ unrestricted_recipients: []
111
138
 
112
139
  # ─────────────────────────────────────────────
113
140
  # Audit Configuration