@cohortapp/agent-sdk 2.3.2 → 2.4.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 (148) hide show
  1. package/framework-features.json +30 -0
  2. package/lib/backlog.mjs +136 -0
  3. package/lib/cadences.mjs +63 -2
  4. package/lib/cadences.test.mjs +105 -0
  5. package/lib/capability/inventory.mjs +542 -0
  6. package/lib/capability/inventory.test.mjs +232 -0
  7. package/lib/capability/probe.mjs +255 -0
  8. package/lib/channels/contract.mjs +37 -1
  9. package/lib/channels/contract.test.mjs +25 -1
  10. package/lib/claude-bin.mjs +37 -3
  11. package/lib/claude-bin.test.mjs +42 -8
  12. package/lib/execution/disposition.mjs +501 -0
  13. package/lib/execution/disposition.test.mjs +482 -0
  14. package/lib/execution/drive.mjs +352 -0
  15. package/lib/execution/drive.test.mjs +270 -0
  16. package/lib/execution/effects.mjs +340 -0
  17. package/lib/execution/effects.test.mjs +193 -0
  18. package/lib/execution/index.mjs +152 -0
  19. package/lib/execution/intake.mjs +581 -0
  20. package/lib/execution/intake.test.mjs +343 -0
  21. package/lib/execution/journal.mjs +374 -0
  22. package/lib/execution/journal.test.mjs +261 -0
  23. package/lib/execution/match.mjs +331 -0
  24. package/lib/execution/match.test.mjs +235 -0
  25. package/lib/execution/pipeline.mjs +341 -0
  26. package/lib/execution/pipeline.test.mjs +389 -0
  27. package/lib/execution/route.mjs +332 -0
  28. package/lib/execution/route.test.mjs +186 -0
  29. package/lib/execution/surface-policy.mjs +446 -0
  30. package/lib/execution/surface-policy.test.mjs +162 -0
  31. package/lib/goals/admission.mjs +209 -0
  32. package/lib/goals/admission.test.mjs +139 -0
  33. package/lib/goals/classify.mjs +206 -0
  34. package/lib/goals/classify.test.mjs +109 -0
  35. package/lib/goals/collaborate.mjs +415 -0
  36. package/lib/goals/collaborate.test.mjs +324 -0
  37. package/lib/goals/gaps.mjs +111 -0
  38. package/lib/goals/gaps.test.mjs +284 -0
  39. package/lib/goals/loop.mjs +537 -0
  40. package/lib/goals/loop.test.mjs +719 -0
  41. package/lib/identity/persona.mjs +247 -0
  42. package/lib/identity/persona.test.mjs +117 -0
  43. package/lib/kpi.mjs +469 -0
  44. package/lib/kpi.test.mjs +244 -0
  45. package/lib/mandate/audit.mjs +168 -0
  46. package/lib/mandate/audit.test.mjs +195 -0
  47. package/lib/mandate/cache.mjs +162 -0
  48. package/lib/mandate/derive.mjs +317 -0
  49. package/lib/mandate/derive.test.mjs +224 -0
  50. package/lib/mandate/model.mjs +352 -0
  51. package/lib/mandate/model.test.mjs +145 -0
  52. package/lib/mandate/refresh.mjs +187 -0
  53. package/lib/mandate/refresh.test.mjs +293 -0
  54. package/lib/mcp/server.test.mjs +4 -4
  55. package/lib/org/approvals.mjs +14 -2
  56. package/lib/org/client.mjs +58 -22
  57. package/lib/org/client.test.mjs +3 -1
  58. package/lib/org/inbound/directedness.mjs +720 -0
  59. package/lib/org/inbound/directedness.test.mjs +543 -0
  60. package/lib/org/inbound/facts.mjs +501 -0
  61. package/lib/org/inbound/facts.test.mjs +375 -0
  62. package/lib/org/inbound/hydrate.mjs +535 -0
  63. package/lib/org/inbound/hydrate.test.mjs +326 -0
  64. package/lib/org/inbound/index.mjs +233 -0
  65. package/lib/org/inbound/index.test.mjs +324 -0
  66. package/lib/org/inbound/io.mjs +141 -0
  67. package/lib/org/inbound/project.mjs +201 -0
  68. package/lib/org/inbound/project.test.mjs +287 -0
  69. package/lib/org/inbound/surfaces.mjs +257 -0
  70. package/lib/org/knowledge.mjs +10 -1
  71. package/lib/org/knowledge.test.mjs +8 -1
  72. package/lib/org/leases.mjs +5 -0
  73. package/lib/org/mesh.mjs +17 -2
  74. package/lib/org/messaging.mjs +40 -4
  75. package/lib/org/messaging.test.mjs +40 -0
  76. package/lib/org/param-contract.mjs +694 -0
  77. package/lib/org/param-contract.test.mjs +451 -0
  78. package/lib/org/protocol.checksum +1 -1
  79. package/lib/org/protocol.mjs +8 -0
  80. package/lib/org/protocol.test.mjs +5 -1
  81. package/lib/org/push.mjs +1025 -0
  82. package/lib/org/push.test.mjs +690 -0
  83. package/lib/org/tool-surface.mjs +138 -38
  84. package/lib/org/tool-surface.test.mjs +13 -8
  85. package/lib/org/typing.mjs +341 -0
  86. package/lib/org/typing.test.mjs +291 -0
  87. package/lib/plan/compile.mjs +510 -0
  88. package/lib/plan/compile.test.mjs +286 -0
  89. package/lib/plan/emit.mjs +256 -0
  90. package/lib/plan/emit.test.mjs +246 -0
  91. package/lib/plan/explain.mjs +226 -0
  92. package/lib/plan/explain.test.mjs +188 -0
  93. package/lib/plan/schema.mjs +140 -0
  94. package/lib/resource-governor.mjs +47 -1
  95. package/lib/resource-governor.test.mjs +21 -1
  96. package/lib/setup/enroll-from-cohort.mjs +84 -16
  97. package/lib/setup/enroll-from-cohort.test.mjs +43 -1
  98. package/lib/setup/sections/identity.mjs +15 -4
  99. package/lib/setup/sections/identity.test.mjs +94 -0
  100. package/lib/setup/sections/inventory.mjs +178 -0
  101. package/lib/setup/sections/inventory.test.mjs +198 -0
  102. package/lib/setup/sections/mandate.mjs +392 -0
  103. package/lib/setup/sections/mandate.test.mjs +373 -0
  104. package/lib/setup/sections/subagents.mjs +427 -0
  105. package/lib/setup/sections/subagents.test.mjs +429 -0
  106. package/lib/setup/sections/verify.mjs +121 -0
  107. package/lib/setup/sections/verify.test.mjs +175 -0
  108. package/lib/setup/sot.mjs +2 -0
  109. package/lib/subagents/cli.mjs +463 -0
  110. package/lib/subagents/cli.test.mjs +389 -0
  111. package/lib/subagents/client.mjs +373 -0
  112. package/lib/subagents/client.test.mjs +309 -0
  113. package/lib/subagents/gap.mjs +268 -0
  114. package/lib/subagents/gap.test.mjs +234 -0
  115. package/lib/subagents/lock.mjs +296 -0
  116. package/lib/subagents/lock.test.mjs +248 -0
  117. package/lib/subagents/manifest.mjs +224 -0
  118. package/lib/subagents/manifest.test.mjs +175 -0
  119. package/lib/subagents/refs.mjs +274 -0
  120. package/lib/subagents/refs.test.mjs +204 -0
  121. package/lib/subagents/resolve.mjs +455 -0
  122. package/lib/subagents/resolve.test.mjs +422 -0
  123. package/lib/subagents/schema.mjs +467 -0
  124. package/lib/subagents/schema.test.mjs +306 -0
  125. package/package.json +8 -3
  126. package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
  127. package/policies/ai-disclosure.yaml +42 -2
  128. package/scaffold/CLAUDE.md +16 -2
  129. package/schedules/triggers/goal-steward.md +79 -0
  130. package/scripts/ci/conformance-org-api.mjs +792 -0
  131. package/scripts/ci/conformance-org-api.test.mjs +417 -0
  132. package/scripts/daemon/agent-daemon.mjs +36 -4
  133. package/scripts/daemon/cadence-handlers.mjs +145 -1
  134. package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
  135. package/scripts/daemon/inbox-deferral.mjs +45 -2
  136. package/scripts/daemon/inbox-deferral.test.mjs +56 -0
  137. package/scripts/daemon/inbox-wake.mjs +282 -0
  138. package/scripts/daemon/inbox-wake.test.mjs +199 -0
  139. package/scripts/daemon/prompt-builder.mjs +41 -1
  140. package/scripts/daemon/typing-registry.mjs +55 -2
  141. package/scripts/daemon/typing-registry.test.mjs +25 -0
  142. package/scripts/local-triggers/generate-plists.test.mjs +5 -5
  143. package/scripts/setup/gen-subagent-manifest.mjs +95 -0
  144. package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
  145. package/scripts/setup/generate-plan.mjs +108 -0
  146. package/scripts/setup/init-capability-manifest.mjs +70 -0
  147. package/scripts/setup/init-skill-marketplace.mjs +155 -0
  148. package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
@@ -67,6 +67,15 @@ import {
67
67
  approvalWait,
68
68
  fetchSelfProfile,
69
69
  } from "./client.mjs";
70
+ // The hq vocabularies the model must pick from (an out-of-vocabulary enum value
71
+ // is a BAD_REQUEST). Sourced from the wire contract so the tool table and the
72
+ // normaliser can never disagree about what hq accepts.
73
+ import {
74
+ BOARD_COLS,
75
+ TASK_PRIORITIES,
76
+ ESCALATION_SEVERITIES,
77
+ MEMORY_CLASSES,
78
+ } from "./param-contract.mjs";
70
79
 
71
80
  /** Default org server origin when neither env nor config names one. */
72
81
  export const DEFAULT_COHORT_BASE = "https://os.cohortapp.com";
@@ -265,7 +274,7 @@ export const ORG_TOOLS = Object.freeze([
265
274
  title: "Send org message",
266
275
  description:
267
276
  "Post a message into an org channel (messaging.send). Send-gate screened locally; " +
268
- "the server enforces channel ACLs + pairing. A clientMsgId is auto-minted for dedup.",
277
+ "the server enforces channel ACLs + pairing. An idempotencyId is auto-minted for dedup.",
269
278
  input_schema: S(
270
279
  {
271
280
  channelId: str("Channel id (use messaging_open_dm for a member DM)."),
@@ -308,23 +317,46 @@ export const ORG_TOOLS = Object.freeze([
308
317
  {
309
318
  name: "board_complete",
310
319
  title: "Complete work item",
311
- description: "Mark a claimed board item complete (board.complete).",
312
- input_schema: S({ itemId: str("Board item id."), note: str("Completion note (optional).") }, ["itemId"]),
320
+ description:
321
+ "Mark a claimed board item complete (board.complete). The server's schema is strict: " +
322
+ "evidence goes in `proof` (a free-text note is wrapped into proof.note).",
323
+ input_schema: S(
324
+ {
325
+ itemId: str("Board item id."),
326
+ proof: {
327
+ type: "object",
328
+ description: "Evidence of completion (optional) — e.g. {note, url, commit}.",
329
+ },
330
+ },
331
+ ["itemId"],
332
+ ),
313
333
  access: "write",
314
334
  binding: { kind: "rpc", method: "board.complete" },
315
335
  },
316
336
  {
317
337
  name: "task_create",
318
338
  title: "Create task",
319
- description: "Create a task on the org board (board.createTask).",
339
+ description:
340
+ "Create a task on the org board (board.createTask). Due dates are NOT settable at " +
341
+ "create time — create the task, then set dueAt with task_update.",
320
342
  input_schema: S(
321
343
  {
322
344
  title: str("Task title."),
323
- description: str("Task body/description (optional)."),
324
- assignee: str("Member to assign (optional)."),
325
- status: str("Initial status (optional)."),
326
- priority: str("Priority (optional)."),
327
- dueAt: str("Due date ISO 8601 (optional)."),
345
+ detail: str("Task body/description (optional)."),
346
+ assigneeId: str("Member id to assign (optional) — an id, not a name."),
347
+ col: {
348
+ type: "string",
349
+ enum: BOARD_COLS,
350
+ description: "Initial board column (optional; defaults to triage).",
351
+ },
352
+ priority: {
353
+ type: "string",
354
+ enum: TASK_PRIORITIES,
355
+ description: "Priority P0 (highest) .. P4 (optional; defaults to P2).",
356
+ },
357
+ channelId: str("The Space/DM whose board this task belongs to (optional)."),
358
+ workstreamId: str("Workstream/lane id (optional)."),
359
+ reviewerId: str("Reviewer member id (optional)."),
328
360
  },
329
361
  ["title"],
330
362
  ),
@@ -335,16 +367,26 @@ export const ORG_TOOLS = Object.freeze([
335
367
  name: "task_update",
336
368
  title: "Update task",
337
369
  description:
338
- "Update a task's status/fields, or move it into a workstream lane (board.updateTask). Pass workstreamName to assign a task that's sitting in no lane — reuses an existing lane by name or creates it.",
370
+ "Update a task's column/fields, or move it into a workstream lane (board.updateTask). Pass " +
371
+ "workstreamName to assign a task that's sitting in no lane — reuses an existing lane by name " +
372
+ "or creates it. This verb CANNOT reassign a task — use task_assign for that.",
339
373
  input_schema: S(
340
374
  {
341
375
  taskId: str("Task id."),
342
- status: str("New status (optional)."),
376
+ col: {
377
+ type: "string",
378
+ enum: BOARD_COLS,
379
+ description: "Move the task to this board column (optional).",
380
+ },
343
381
  title: str("New title (optional)."),
344
- description: str("New description (optional)."),
345
- assignee: str("New assignee (optional)."),
346
- priority: str("New priority (optional)."),
347
- dueAt: str("New due date ISO 8601 (optional)."),
382
+ detail: str("New description (optional)."),
383
+ priority: {
384
+ type: "string",
385
+ enum: TASK_PRIORITIES,
386
+ description: "New priority P0..P4 (optional).",
387
+ },
388
+ dueAt: str("New due date ISO 8601 (optional; null clears it)."),
389
+ blockedBy: str("Free-text reason this task is waiting (optional)."),
348
390
  workstreamName: str(
349
391
  "Move the task into this workstream lane — reuses an existing lane by name or creates it (optional).",
350
392
  ),
@@ -354,6 +396,20 @@ export const ORG_TOOLS = Object.freeze([
354
396
  access: "write",
355
397
  binding: { kind: "rpc", method: "board.updateTask" },
356
398
  },
399
+ {
400
+ name: "task_assign",
401
+ title: "Assign task",
402
+ description:
403
+ "Set or clear a task's owner (board.assignTask). The assignee must be an ACTIVE member id " +
404
+ "in this org; pass null to unassign. task_update cannot do this — its schema has no " +
405
+ "assignee field.",
406
+ input_schema: S(
407
+ { taskId: str("Task id."), assigneeId: str("Member id to own the task (null clears it).") },
408
+ ["taskId", "assigneeId"],
409
+ ),
410
+ access: "write",
411
+ binding: { kind: "rpc", method: "board.assignTask" },
412
+ },
357
413
  {
358
414
  name: "task_comment",
359
415
  title: "Comment on task",
@@ -374,12 +430,23 @@ export const ORG_TOOLS = Object.freeze([
374
430
  {
375
431
  name: "decision_propose",
376
432
  title: "Propose decision",
377
- description: "Propose a decision into the org registry (decision.propose).",
433
+ description:
434
+ "Propose a decision into the org registry (decision.propose). The rationale is recorded " +
435
+ "in the explain-yourself `why` block — a decision proposed without it is registered with " +
436
+ "no rationale at all.",
378
437
  input_schema: S(
379
438
  {
380
439
  title: str("Decision title."),
381
- rationale: str("Why (optional)."),
382
- scope: str("Scope (optional)."),
440
+ body: str("The decision text (optional)."),
441
+ why: {
442
+ type: "object",
443
+ description:
444
+ "Explain-yourself provenance: {reason, options[], clause, wouldChange}. `reason` is " +
445
+ "the rationale the registry displays.",
446
+ },
447
+ tag: str("Scope/tag for the registry, e.g. \"eng\" (optional)."),
448
+ clause: str("The charter clause this decision rests on (optional)."),
449
+ supersedes: str("Id of the decision this replaces (optional)."),
383
450
  },
384
451
  ["title"],
385
452
  ),
@@ -398,7 +465,7 @@ export const ORG_TOOLS = Object.freeze([
398
465
  name: "decision_comment",
399
466
  title: "Comment on decision",
400
467
  description: "Comment on a decision (decision.comment).",
401
- input_schema: S({ decisionId: str("Decision id."), body: str("Comment body.") }, ["decisionId", "body"]),
468
+ input_schema: S({ decisionId: str("Decision id."), text: str("Comment text.") }, ["decisionId", "text"]),
402
469
  access: "write",
403
470
  binding: { kind: "rpc", method: "decision.comment" },
404
471
  },
@@ -413,7 +480,9 @@ export const ORG_TOOLS = Object.freeze([
413
480
  actionClass: str("Action class, e.g. \"external_comms\"."),
414
481
  payload: { type: "object", description: "The intent payload the approval binds (hash-bound server-side)." },
415
482
  subject: str("Human-readable subject (optional)."),
416
- payload_hash: str("Precomputed sha256 payload hash (optional — pass through from a structured approval_required result)."),
483
+ payloadHash: str("Precomputed sha256 payload hash (64 hex chars) — pass through from a structured approval_required result. Supply this OR payload."),
484
+ itemId: str("Board item this approval blocks (optional — the task flips to `blocked`)."),
485
+ expiresAt: str("ISO 8601 expiry (optional)."),
417
486
  },
418
487
  ["actionClass"],
419
488
  ),
@@ -444,14 +513,19 @@ export const ORG_TOOLS = Object.freeze([
444
513
  {
445
514
  name: "knowledge_append",
446
515
  title: "Append org knowledge",
447
- description: "Append a knowledge episode to the shared plane (knowledge.append).",
516
+ description:
517
+ "Append a knowledge episode to the shared plane (knowledge.append). Enrichment " +
518
+ "(kind/participants/links/refs) is persisted inside `metadata`.",
448
519
  input_schema: S(
449
520
  {
450
- text: str("The fact/episode text."),
451
- kind: str("Kind (optional)."),
452
- group: str("Group hint (server enforces ACL; optional)."),
521
+ body: str("The fact/episode text."),
522
+ group: str("Group hint (server enforces ACL; optional) — \"org\" | \"unit:<id>\" | \"barrier:<cell>\"."),
523
+ metadata: {
524
+ type: "object",
525
+ description: "Structured enrichment stored with the episode (optional) — e.g. {kind, participants, links, refs}.",
526
+ },
453
527
  },
454
- ["text"],
528
+ ["body"],
455
529
  ),
456
530
  access: "write",
457
531
  binding: { kind: "rpc", method: "knowledge.append" },
@@ -459,14 +533,24 @@ export const ORG_TOOLS = Object.freeze([
459
533
  {
460
534
  name: "memory_author",
461
535
  title: "Author org memory",
462
- description: "Write to the org memory plane (memory.author).",
536
+ description:
537
+ "Write a Cortex index-hint entry to the org memory plane (memory.author). `scopes` is the " +
538
+ "ACL field (who may recall this), NOT a tag list. Amend an existing entry with " +
539
+ "{amendOf, note}.",
463
540
  input_schema: S(
464
541
  {
465
- content: str("Memory content."),
466
- kind: str("Memory kind (optional)."),
467
- tags: arr({ type: "string" }, "Tags (optional)."),
542
+ memoryClass: {
543
+ type: "string",
544
+ enum: MEMORY_CLASSES,
545
+ description: "Which memory plane this entry belongs to.",
546
+ },
547
+ title: str("Short title (<=200 chars) — the entry's headline."),
548
+ summary: str("The memory content (optional but expected)."),
549
+ scopes: arr({ type: "string" }, "ACL scopes that may recall this (optional; empty = org-wide)."),
550
+ amendOf: str("Id of the entry this amends (optional)."),
551
+ note: str("Revision note — REQUIRED when amendOf is set."),
468
552
  },
469
- ["content"],
553
+ ["memoryClass", "title"],
470
554
  ),
471
555
  access: "write",
472
556
  binding: { kind: "rpc", method: "memory.author" },
@@ -475,22 +559,31 @@ export const ORG_TOOLS = Object.freeze([
475
559
  {
476
560
  name: "member_profile",
477
561
  title: "Member profile",
478
- description: "One member's full profile (member.get).",
479
- input_schema: S({ memberId: str("Member id or slug.") }, ["memberId"]),
562
+ description: "One member's full profile (member.get). Resolved by SLUG, not by member id.",
563
+ input_schema: S({ slug: str("Member slug (e.g. \"isla-roselli\").") }, ["slug"]),
480
564
  access: "read",
481
565
  binding: { kind: "rpc", method: "member.get" },
482
566
  },
483
567
  {
484
568
  name: "escalation_raise",
485
569
  title: "Raise escalation",
486
- description: "Raise an escalation to the org (escalation.create).",
570
+ description:
571
+ "Raise an escalation to the org (escalation.create). An escalation must reference " +
572
+ "something — supply a taskId or a channelId (or both).",
487
573
  input_schema: S(
488
574
  {
489
- severity: str("Severity, e.g. low|medium|high|critical."),
490
- subject: str("Escalation subject."),
491
- body: str("Details."),
575
+ title: str("Escalation title (<=500 chars)."),
576
+ severity: {
577
+ type: "string",
578
+ enum: ESCALATION_SEVERITIES,
579
+ description: "Severity (optional; defaults to medium).",
580
+ },
581
+ detail: str("Details (optional)."),
582
+ taskId: str("The task being escalated (supply this or channelId)."),
583
+ channelId: str("The channel being escalated (supply this or taskId)."),
584
+ waitingOn: str("Who/what this is blocked on (optional)."),
492
585
  },
493
- ["severity", "subject", "body"],
586
+ ["title"],
494
587
  ),
495
588
  access: "write",
496
589
  binding: { kind: "rpc", method: "escalation.create" },
@@ -2495,7 +2588,14 @@ export async function executeOrgTool(name, input = {}, o = {}) {
2495
2588
  // Business-dedup ids auto-minted where hq expects them. (artifact.*
2496
2589
  // is sideEffecting:false at the dispatcher — the hq artifact service
2497
2590
  // self-manages its writes — so these params ARE the dedup lane.)
2498
- if (name === "messaging_send" && !params.clientMsgId) params.clientMsgId = randomUUID();
2591
+ //
2592
+ // messaging.send's dedup key is `idempotencyId` — hq's
2593
+ // sendMessageSchemaV1 REQUIRES it and never reads `clientMsgId` (that
2594
+ // is the COLUMN it de-dups into, not the wire name). Minting the wrong
2595
+ // name meant every message the model sent through this plane came back
2596
+ // BAD_REQUEST. `call()` would mint it now anyway (param-contract), but
2597
+ // minting here keeps the id available to the caller's audit row.
2598
+ if (name === "messaging_send" && !params.idempotencyId) params.idempotencyId = randomUUID();
2499
2599
  if (name === "email_send" && !params.idempotencyId) params.idempotencyId = randomUUID();
2500
2600
  if (name === "artifact_create" && !params.clientMsgId) params.clientMsgId = randomUUID();
2501
2601
  if (name === "artifact_act" && !params.idempotencyKey) params.idempotencyKey = randomUUID();
@@ -59,11 +59,13 @@ function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200,
59
59
 
60
60
  // ── table shape ────────────────────────────────────────────────────────────
61
61
 
62
- test("table: curated 28 + 5 email + 5 artifact + 69 desk tools, snake_case names, valid access + schemas", () => {
62
+ test("table: curated 29 + 5 email + 5 artifact + 69 desk tools, snake_case names, valid access + schemas", () => {
63
63
  // 2026-08 mobile-parity delta: +5 mail (report_spam/react/move_mailbox/
64
64
  // summarise/ask) and +3 asks (files/calendar/crm) → desk 56 → 64; the
65
65
  // call-working-sessions delta adds the 5 calls-desk tools → 69.
66
- assert.equal(ORG_TOOLS.length, 107, "28 curated + 5 email + 5 artifact + 69 desk");
66
+ // The wire-contract fix adds task_assign: hq's editTaskSchema (board.updateTask)
67
+ // has NO assignee field, so reassignment needs board.assignTask — curated 28 → 29.
68
+ assert.equal(ORG_TOOLS.length, 108, "29 curated + 5 email + 5 artifact + 69 desk");
67
69
  assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 69, "exactly sixty-nine desk tools");
68
70
  assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
69
71
  assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
@@ -88,19 +90,19 @@ test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail
88
90
  test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
89
91
  // The email family landed in the vendored protocol (sync-protocol phase 1).
90
92
  assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
91
- assert.equal(getOrgTools().length, 107);
93
+ assert.equal(getOrgTools().length, 108);
92
94
  const without = getOrgTools({ emailAvailable: false });
93
- assert.equal(without.length, 102);
95
+ assert.equal(without.length, 103);
94
96
  assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
95
97
  });
96
98
 
97
99
  test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
98
100
  assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
99
101
  const without = getOrgTools({ artifactAvailable: false });
100
- assert.equal(without.length, 102);
102
+ assert.equal(without.length, 103);
101
103
  assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
102
104
  const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
103
- assert.equal(neither.length, 28, "all additive families off → the 28 always-on tools");
105
+ assert.equal(neither.length, 29, "all additive families off → the 29 always-on tools");
104
106
  });
105
107
 
106
108
  test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
@@ -203,7 +205,7 @@ test("approval_wait: requires approvalId; dispatches the bounded long-poll", asy
203
205
 
204
206
  // ── executor: send-gate parity (§1.5a) ─────────────────────────────────────
205
207
 
206
- test("messaging_send: screened through the send-gate, clientMsgId auto-minted", async () => {
208
+ test("messaging_send: screened through the send-gate, idempotencyId auto-minted", async () => {
207
209
  const screened = [];
208
210
  const fetchImpl = fakeFetch({ ok: true, result: { messageId: "m1" } });
209
211
  const frame = await executeOrgTool(
@@ -216,7 +218,10 @@ test("messaging_send: screened through the send-gate, clientMsgId auto-minted",
216
218
  assert.equal(screened[0].recipient, "C9");
217
219
  assert.equal(screened[0].text, "hello org");
218
220
  const body = JSON.parse(fetchImpl.calls[0].init.body);
219
- assert.ok(body.clientMsgId && body.clientMsgId.length >= 8, "clientMsgId auto-minted");
221
+ // hq's sendMessageSchemaV1 REQUIRES `idempotencyId`. Minting `clientMsgId`
222
+ // (the column name, not the wire name) 400'd every message this plane sent.
223
+ assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
224
+ assert.equal(body.clientMsgId, undefined, "the column name is NOT the wire name");
220
225
  assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher idempotency key present");
221
226
  });
222
227