@rubytech/create-maxy-code 0.1.458 → 0.1.460

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 (218) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -3
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +79 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
  9. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  10. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  12. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
  13. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  14. package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
  15. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  16. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
  17. package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
  18. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  21. package/payload/platform/lib/storage-broker/dist/index.js +2 -0
  22. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  23. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
  27. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  31. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
  32. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
  33. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  34. package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
  35. package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
  36. package/payload/platform/lib/storage-broker/src/index.ts +2 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  51. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  52. package/payload/platform/plugins/cloudflare/references/api.md +1 -1
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
  65. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
  66. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
  67. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
  68. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
  69. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
  70. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
  71. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
  72. package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
  73. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
  74. package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
  75. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
  76. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  77. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
  78. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
  92. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
  93. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
  104. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
  106. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
  107. package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
  108. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  109. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
  110. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
  111. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
  112. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
  113. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  114. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  115. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  117. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  118. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  119. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  120. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  121. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
  122. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  123. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
  124. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  125. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
  126. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  127. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
  128. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  129. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
  130. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
  131. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
  132. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
  133. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
  134. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  135. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
  136. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  137. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
  138. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  139. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
  140. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  141. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  142. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  143. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  144. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  145. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  146. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
  147. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  148. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
  149. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  150. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
  151. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  152. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  153. package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
  154. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
  155. package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
  156. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
  157. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
  158. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
  159. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
  160. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
  161. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
  162. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
  163. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
  164. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
  165. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
  166. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
  167. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
  168. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
  169. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  170. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
  171. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
  172. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
  173. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
  174. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  175. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  176. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  177. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  178. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  179. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  180. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  181. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  182. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  183. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
  184. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  185. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  186. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  187. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  188. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  189. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  190. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  191. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  192. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  193. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  194. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  195. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
  196. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
  197. package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
  198. package/payload/platform/scripts/logs-read.sh +63 -9
  199. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  200. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  201. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
  202. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  203. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  204. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  205. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  206. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  207. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  208. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  209. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
  210. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  211. package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
  212. package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
  213. package/payload/server/maxy-edge.js +1 -1
  214. package/payload/server/server.js +412 -37
  215. package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
  216. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  217. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  218. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,7 @@ import { readFileSync } from "node:fs";
33
33
  import { join, dirname } from "node:path";
34
34
  import { fileURLToPath } from "node:url";
35
35
  import { readdirSync } from "node:fs";
36
+ import { spawn } from "node:child_process";
36
37
 
37
38
  const __dirname = dirname(fileURLToPath(import.meta.url));
38
39
  const TOOLS_SRC = join(__dirname, "../src/tools");
@@ -410,6 +411,339 @@ await check(
410
411
  },
411
412
  );
412
413
 
414
+ // ---------------------------------------------------------------------------
415
+ // 12. Boot identity resolution (Task 1699). The server resolves the operator
416
+ // from USER_ID — the variable the PTY spawn stamps and MCP children
417
+ // inherit. ADMIN_USER_ID was never emitted by the platform, so every boot
418
+ // logged userId=null and four of five tools refused.
419
+ //
420
+ // Spawning the built server is the only faithful test: userId is read at
421
+ // module scope, and the boot line is the contract the operator diagnoses
422
+ // from. An empty USER_ID (silent-compaction / recorder / public spawns
423
+ // legitimately stamp empty) must resolve to null, not "" — an empty string
424
+ // is truthy enough to reach voiceRetrieveConditioning's `userId` and would
425
+ // become a profile key.
426
+ // ---------------------------------------------------------------------------
427
+ console.log("\n-- boot identity --");
428
+
429
+ const DIST_ENTRY = join(__dirname, "../dist/index.js");
430
+
431
+ function bootUserId(env) {
432
+ return new Promise((resolve, reject) => {
433
+ const child = spawn(process.execPath, [DIST_ENTRY], {
434
+ env: { PATH: process.env.PATH, ACCOUNT_ID: "acct-smoke", ...env },
435
+ stdio: ["pipe", "pipe", "pipe"],
436
+ });
437
+ let stderr = "";
438
+ const timer = setTimeout(() => {
439
+ child.kill("SIGKILL");
440
+ reject(new Error(`no boot line within 10s; stderr=${stderr.slice(0, 300)}`));
441
+ }, 10_000);
442
+ child.stderr.on("data", (chunk) => {
443
+ stderr += chunk.toString();
444
+ const m = stderr.match(/\[voice-mirror\] boot accountId=(\S+) userId=(\S+)/);
445
+ if (m) {
446
+ clearTimeout(timer);
447
+ child.kill("SIGKILL");
448
+ resolve({ accountId: m[1], userId: m[2] });
449
+ }
450
+ });
451
+ child.on("error", (err) => {
452
+ clearTimeout(timer);
453
+ reject(err);
454
+ });
455
+ // A child that dies before printing the boot line would otherwise sit here
456
+ // until the timeout and report "no boot line", hiding the real cause.
457
+ child.on("exit", (code, signal) => {
458
+ if (signal === "SIGKILL") return; // our own kill, after a successful match
459
+ clearTimeout(timer);
460
+ reject(new Error(`exited code=${code} before boot line; stderr=${stderr.slice(0, 300)}`));
461
+ });
462
+ });
463
+ }
464
+
465
+ await check("boot resolves userId from USER_ID (the var the platform emits)", async () => {
466
+ const { accountId, userId } = await bootUserId({ USER_ID: "436fdf2d-cfa7-4836-9ffa-77d91cef15f9" });
467
+ assert.equal(accountId, "acct-smoke");
468
+ assert.equal(userId, "436fdf2d-cfa7-4836-9ffa-77d91cef15f9");
469
+ });
470
+
471
+ await check("boot reports userId=null when USER_ID is absent", async () => {
472
+ const { userId } = await bootUserId({});
473
+ assert.equal(userId, "null");
474
+ });
475
+
476
+ await check("boot reports userId=null when USER_ID is empty (not \"\")", async () => {
477
+ const { userId } = await bootUserId({ USER_ID: "" });
478
+ assert.equal(userId, "null");
479
+ });
480
+
481
+ await check("ADMIN_USER_ID is inert — it is not an identity source", async () => {
482
+ const { userId } = await bootUserId({ ADMIN_USER_ID: "should-be-ignored" });
483
+ assert.equal(userId, "null");
484
+ });
485
+
486
+ // ---------------------------------------------------------------------------
487
+ // 12. Identity gate (Task 1701).
488
+ // A userId / voiceAuthor that names no :AdminUser on the calling account
489
+ // must never become a profile key or an author stamp. The reserved org
490
+ // sentinel legitimately names no :AdminUser and must bypass (Tasks 676, 975).
491
+ // ---------------------------------------------------------------------------
492
+ console.log("\n-- identity gate --");
493
+
494
+ const adminUserMod = await import("../dist/lib/admin-user.js");
495
+
496
+ check("unknownAdminUserMessage names the tool, the rejected key and the account", () => {
497
+ const msg = adminUserMod.unknownAdminUserMessage(
498
+ "voice-tag-content",
499
+ "rob-mead",
500
+ "68b50910",
501
+ );
502
+ assert.ok(msg.includes("rob-mead"), `names the rejected key: ${msg}`);
503
+ assert.ok(msg.includes("68b50910"), `names the account: ${msg}`);
504
+ assert.ok(msg.includes("voice-tag-content"), `names the tool: ${msg}`);
505
+ });
506
+
507
+ check("refusal wording does not invite the caller to guess another userId", () => {
508
+ // The prior text told callers to supply userId explicitly. An agent with no
509
+ // way to learn a real id supplied a plausible-looking name instead, which is
510
+ // how `rob-mead` reached a live client account.
511
+ const msg = adminUserMod.unknownAdminUserMessage(
512
+ "voice-tag-content",
513
+ "rob-mead",
514
+ "68b50910",
515
+ );
516
+ assert.ok(
517
+ !/supply\s+\S*\s*userId|pass\s+\S*\s*userId\s+explicitly|provide\s+a\s+userId/i.test(
518
+ msg,
519
+ ),
520
+ `must not invite the caller to supply a userId: ${msg}`,
521
+ );
522
+ });
523
+
524
+ check("VoiceIdentityError carries the rejected key and account", () => {
525
+ const err = new adminUserMod.VoiceIdentityError(
526
+ "voice-tag-content",
527
+ "rob-mead",
528
+ "68b50910",
529
+ );
530
+ assert.ok(err instanceof Error);
531
+ assert.equal(err.name, "VoiceIdentityError");
532
+ assert.equal(err.userId, "rob-mead");
533
+ assert.equal(err.accountId, "68b50910");
534
+ });
535
+
536
+ await check("ORG_USER_ID bypasses the gate without opening a session", async () => {
537
+ // With NEO4J_URI unset, getDriver() throws. Returning normally proves the
538
+ // org sentinel short-circuits before any session is opened, so an org
539
+ // distil/retrieve still works on a spawn with no personal identity.
540
+ const saved = process.env.NEO4J_URI;
541
+ delete process.env.NEO4J_URI;
542
+ try {
543
+ await adminUserMod.assertAdminUserOnAccount(
544
+ "voice-distil-profile",
545
+ corpusMod.ORG_USER_ID,
546
+ "acct-1",
547
+ );
548
+ } finally {
549
+ if (saved !== undefined) process.env.NEO4J_URI = saved;
550
+ }
551
+ });
552
+
553
+ await check("a non-org key is looked up, never passed through (fail-closed)", async () => {
554
+ // The converse of the bypass: a real key must reach the lookup. With
555
+ // NEO4J_URI unset that surfaces as a driver throw. A gate that returned
556
+ // normally here would be failing open on every unverified key.
557
+ const saved = process.env.NEO4J_URI;
558
+ delete process.env.NEO4J_URI;
559
+ try {
560
+ await assert.rejects(
561
+ () =>
562
+ adminUserMod.assertAdminUserOnAccount(
563
+ "voice-tag-content",
564
+ "rob-mead",
565
+ "68b50910",
566
+ ),
567
+ /NEO4J_URI unset/,
568
+ "must attempt the lookup rather than silently accepting the key",
569
+ );
570
+ } finally {
571
+ if (saved !== undefined) process.env.NEO4J_URI = saved;
572
+ }
573
+ });
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // 13. Identity-gate wiring (Task 1701).
577
+ // The gate's own behaviour is covered above against the helper. These are
578
+ // structural invariants on the call sites: a live Neo4j is needed to tell
579
+ // "gate refused" from "write failed" at runtime (both surface as the same
580
+ // driver error), so ordering and presence are asserted on the source —
581
+ // the same idiom as the Cypher forbidden-pattern gate above.
582
+ // ---------------------------------------------------------------------------
583
+ console.log("\n-- identity-gate wiring --");
584
+
585
+ const tagSrc = readFileSync(join(TOOLS_SRC, "voice-tag-content.ts"), "utf-8");
586
+
587
+ check("voice-tag-content gates the resolved voiceAuthor", () => {
588
+ assert.ok(
589
+ /assertAdminUserOnAccount\(/.test(tagSrc),
590
+ "voice-tag-content must gate voiceAuthor via assertAdminUserOnAccount",
591
+ );
592
+ });
593
+
594
+ check("voice-tag-content gates before it stamps voiceAuthor", () => {
595
+ // The stamp is the write this task exists to stop. A gate ordered after it
596
+ // would refuse only once the dead data was already on the node.
597
+ const gateAt = tagSrc.indexOf("assertAdminUserOnAccount(");
598
+ const writeAt = tagSrc.indexOf("n.voiceAuthor = $voiceAuthor");
599
+ assert.ok(gateAt > -1, "gate present");
600
+ assert.ok(writeAt > -1, "voiceAuthor write present");
601
+ assert.ok(gateAt < writeAt, "identity gate must run before the voiceAuthor write");
602
+ });
603
+
604
+ await check("voice-tag-content: empty nodeIds stays a no-op (no identity lookup)", async () => {
605
+ // An empty tag has nothing to attribute, so it must not reach the driver.
606
+ // Guards the ordering of the early return against the new gate.
607
+ const saved = process.env.NEO4J_URI;
608
+ delete process.env.NEO4J_URI;
609
+ try {
610
+ const result = await tagMod.voiceTagContent({
611
+ nodeIds: [],
612
+ mode: "human-only",
613
+ accountId: "acct-1",
614
+ format: "text",
615
+ voiceAuthor: "rob-mead",
616
+ });
617
+ assert.equal(result.updated, 0);
618
+ assert.equal(result.skipped, 0);
619
+ } finally {
620
+ if (saved !== undefined) process.env.NEO4J_URI = saved;
621
+ }
622
+ });
623
+
624
+ const distilSrc = readFileSync(join(TOOLS_SRC, "voice-distil-profile.ts"), "utf-8");
625
+
626
+ check("voice-distil-profile gates the profile key at its single entry point", () => {
627
+ // Gating the exported entry rather than distilForFormat covers sample, amend,
628
+ // amend-write and multi-format enumeration with one lookup, instead of one
629
+ // per format.
630
+ const entryAt = distilSrc.indexOf("export async function voiceDistilProfile");
631
+ assert.ok(entryAt > -1, "exported entry present");
632
+ assert.ok(
633
+ /assertAdminUserOnAccount\(/.test(distilSrc.slice(entryAt)),
634
+ "the exported entry must gate the profile key",
635
+ );
636
+ });
637
+
638
+ check("voice-distil-profile keeps both structural write anchors", () => {
639
+ // The anchor MATCH binds `anchor` for MERGE (anchor)-[:HAS_VOICE_PROFILE]->(p).
640
+ // The entry gate is additive; removing these would drop the edge.
641
+ const anchors = distilSrc.match(
642
+ /MATCH \(anchor:AdminUser \{accountId: \$accountId, userId: \$profileUserId\}\)/g,
643
+ );
644
+ assert.equal(anchors?.length, 2, "both write-path anchors retained");
645
+ });
646
+
647
+ const retrieveSrc = readFileSync(
648
+ join(TOOLS_SRC, "voice-retrieve-conditioning.ts"),
649
+ "utf-8",
650
+ );
651
+
652
+ check("voice-retrieve-conditioning does NOT gate identity (it writes nothing)", () => {
653
+ // Deliberately ungated. Retrieve mutates nothing, so it sits outside the
654
+ // "validate before any write keyed on it" rule, and gating it broke the
655
+ // personal->org fallback below. Invented keys are stopped at
656
+ // voice-tag-content, which is the only path that puts one in the graph.
657
+ assert.ok(
658
+ !/assertAdminUserOnAccount\(/.test(retrieveSrc),
659
+ "retrieve must not gate identity: it regresses the personal->org fallback",
660
+ );
661
+ });
662
+
663
+ check("voice-retrieve-conditioning keeps the personal->org fallback", () => {
664
+ // The regression guard. A personal lookup with no data must borrow the org
665
+ // (house) voice, not degrade to the default register. A house operator
666
+ // scoped into a client sub-account has no :AdminUser on that account, so
667
+ // this fallback is the only thing that puts the client's house card into
668
+ // their drafts.
669
+ assert.ok(
670
+ /const org = await orgLookup\(\);/.test(retrieveSrc),
671
+ "personal miss must fall back to orgLookup()",
672
+ );
673
+ assert.ok(
674
+ /status = hasData\(org\) \? "fallback-org" : "no-data";/.test(retrieveSrc),
675
+ "personal miss must report fallback-org when the org voice resolves",
676
+ );
677
+ });
678
+
679
+ check("voice-retrieve-conditioning never sets isError", () => {
680
+ // Match the property assignment, not the word: prose about isError is fine,
681
+ // setting it is not.
682
+ assert.ok(
683
+ !/isError\s*[:=]/.test(retrieveSrc),
684
+ "retrieve degrades via status, never isError",
685
+ );
686
+ });
687
+
688
+ // ---------------------------------------------------------------------------
689
+ // 14. One refusal wording across every gate (Task 1701).
690
+ // record-feedback and ingest already enforced the rule via an inline
691
+ // MATCH (a:AdminUser {accountId, userId}). That match is structural — it
692
+ // binds `a` for the AUTHORED edge — so it stays exactly where it is and
693
+ // only the wording is shared.
694
+ // ---------------------------------------------------------------------------
695
+ console.log("\n-- shared refusal wording --");
696
+
697
+ for (const file of [
698
+ "voice-record-feedback.ts",
699
+ "voice-ingest-session-text.ts",
700
+ "voice-distil-profile.ts",
701
+ ]) {
702
+ const src = readFileSync(join(TOOLS_SRC, file), "utf-8");
703
+ check(`${file} refuses with the shared wording`, () => {
704
+ assert.ok(
705
+ /unknownAdminUserMessage\(/.test(src),
706
+ `${file} must use the shared refusal wording`,
707
+ );
708
+ });
709
+ check(`${file} drops the un-actionable onboarding wording`, () => {
710
+ // "Onboarding must promote the operator to :AdminUser" names a remedy the
711
+ // caller cannot perform and never names the account.
712
+ assert.ok(
713
+ !/Onboarding must promote the operator/.test(src),
714
+ `${file} still carries the old onboarding wording`,
715
+ );
716
+ });
717
+ }
718
+
719
+ check("record-feedback keeps its structural AdminUser match (binds the AUTHORED edge)", () => {
720
+ const fb = readFileSync(join(TOOLS_SRC, "voice-record-feedback.ts"), "utf-8");
721
+ assert.ok(
722
+ /MATCH \(a:AdminUser \{accountId: \$accountId, userId: \$userId\}\)/.test(fb),
723
+ "structural match removed — the AUTHORED edge would be dropped",
724
+ );
725
+ assert.ok(/CREATE \(a\)-\[:AUTHORED\]->\(e\)/.test(fb), "AUTHORED edge retained");
726
+ });
727
+
728
+ check("ingest keeps its structural AdminUser match (binds the AUTHORED edge)", () => {
729
+ const ing = readFileSync(join(TOOLS_SRC, "voice-ingest-session-text.ts"), "utf-8");
730
+ assert.ok(
731
+ /MATCH \(a:AdminUser \{accountId: \$accountId, userId: \$userId\}\)/.test(ing),
732
+ "structural match removed — the AUTHORED edge would be dropped",
733
+ );
734
+ assert.ok(/MERGE \(a\)-\[:AUTHORED\]->\(n\)/.test(ing), "AUTHORED edge retained");
735
+ });
736
+
737
+ check("distil keeps the :LocalBusiness wording for the org branch", () => {
738
+ // An org distil failing on a missing business node is a different failure
739
+ // from an unknown personal key; it must not collapse into the shared wording.
740
+ const src = readFileSync(join(TOOLS_SRC, "voice-distil-profile.ts"), "utf-8");
741
+ assert.ok(
742
+ /:LocalBusiness \{accountId='\$\{accountId\}'\} not found/.test(src),
743
+ "org branch must keep its own :LocalBusiness message",
744
+ );
745
+ });
746
+
413
747
  // ---------------------------------------------------------------------------
414
748
  // Final summary.
415
749
  // ---------------------------------------------------------------------------
@@ -201,6 +201,18 @@ Which voice a draft uses is the `scope` on the retrieval brief: `"my voice"` →
201
201
 
202
202
  **Single-operator accounts are unchanged.** With author omitted at tag time, the sole operator is the implicit author, so their personal corpus equals the account-wide corpus and personal distillation reproduces the prior single profile. Legacy content carries no `voiceAuthor`; it appears only in the account-wide org walk until re-tagged, and personal drafts fall back to the org voice in the meantime.
203
203
 
204
+ ## Identity is never guessed
205
+
206
+ Every `userId` and every `author` is an `:AdminUser` id that already exists on the account being operated. It is never a person's name, and never a slug built from one. `rob-mead` is not an identity.
207
+
208
+ A value naming no `:AdminUser` on the account is refused, and nothing is written. This is not a formality. A profile keyed on an admin who does not exist cannot be read back by any seat that resolves identity properly, so it becomes dead data that looks like a real operator profile. A `voiceAuthor` stamped the same way attributes writing to somebody who does not exist.
209
+
210
+ The tools resolve identity from the spawn environment on their own. Pass `author:` only to attribute content to a different operator on a multi-operator account, and only with that operator's real `:AdminUser` id.
211
+
212
+ If a tool refuses, the id is wrong. Do not retry with a different guess. The account's real admin identity is the operator's to supply.
213
+
214
+ `__org__` is exempt, because the house voice anchors on the business rather than on a person.
215
+
204
216
  ## Observability
205
217
 
206
218
  | Tag | When |
@@ -213,6 +225,7 @@ Which voice a draft uses is the `scope` on the retrieval brief: `"my voice"` →
213
225
  | `[voice-retrieve] scope=<scope> format=<format> status=<ok\|fallback-org\|no-data\|error> styleCardBytes=<n> exemplarCount=<k> tokenBudget=<n>` | Each retrieval. `status=fallback-org` proves the personal→org fallback fired. |
214
226
  | `[voice-record-feedback] scope=<scope> userId=<id\|__org__> format=<format> intent="<haiku-summary>" diffBytes=<n>` | Each feedback write. |
215
227
  | `[voice-ingest-session-text] sessionId=<id> adminUser=<id> turns=<n> bytes=<n> skipped=<m>` | On-demand session-text capture. |
228
+ | `[voice-identity] refuse tool=<tool> reason=unknown-admin-user userId=<key> accountId=<account>` | A key naming no `:AdminUser` on the account was refused. Names the rejected key and the account, so an invented key is visible in the logs instead of only in the graph. |
216
229
 
217
230
  **Confirms working:** `[voice-retrieve] exemplarCount≥1` precedes every drafting-skill prompt assembly when the calling skill has not opted out. `[voice-distil]` appears at least once per 30-day window per active format per operator. `[voice-ingest-session-text] turns≥1` appears when the operator asks to capture a session's voice.
218
231
 
@@ -87,6 +87,52 @@ write_parent_with_error() {
87
87
  } > "$f"
88
88
  }
89
89
 
90
+ # Write a parent JSONL carrying BOTH harness rejections and the classes that
91
+ # must NOT be treated as rejections. A rejection is a call the CLI refused to
92
+ # dispatch, so the tool never ran; everything else here either ran and failed or
93
+ # is a different fault. Both InputValidationError sub-classes are covered:
94
+ # unparseable JSON (carries __unparsedToolInput) and schema-valid JSON that
95
+ # failed validation (no such marker, and the majority class in practice).
96
+ write_parent_with_rejections() {
97
+ local cfg="$1" uuid="$2"
98
+ local f="$cfg/projects/-fake-cwd/${uuid}.jsonl"
99
+ {
100
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:00.000Z","uuid":"r0","message":{"role":"user","content":"send it"}}'
101
+ # 2 x rejection sub-class A (unparseable JSON) on ONE tool -> census x2
102
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:01.000Z","uuid":"r1","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_R1","name":"outlook-mail-send","input":{"__unparsedToolInput":"{\"to\": bare@x.co}"}}]}}'
103
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:02.000Z","uuid":"r2","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_R1","is_error":true,"content":"<tool_use_error>InputValidationError: outlook-mail-send was called with input that could not be parsed as JSON.\nYou sent (first 200 of 670 bytes): {\"to\": bare@x.co} REJECT_A_ONE</tool_use_error>"}]}}'
104
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:03.000Z","uuid":"r3","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_R2","name":"outlook-mail-send","input":{"__unparsedToolInput":"{\"to\": bare@x.co}"}}]}}'
105
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:04.000Z","uuid":"r4","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_R2","is_error":true,"content":"<tool_use_error>InputValidationError: outlook-mail-send was called with input that could not be parsed as JSON.\nYou sent (first 200 of 670 bytes): {\"to\": bare@x.co} REJECT_A_TWO</tool_use_error>"}]}}'
106
+ # rejection sub-class B: JSON parsed fine, schema validation failed
107
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:05.000Z","uuid":"r5","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_R3","name":"Read","input":{"offset":"x"}}]}}'
108
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:06.000Z","uuid":"r6","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_R3","is_error":true,"content":"<tool_use_error>InputValidationError: Read failed due to the following issue: The parameter `offset` type is expected as `number` REJECT_B_ONE</tool_use_error>"}]}}'
109
+ # NOT a rejection: unknown tool (different fault, has its own PostToolUse hook)
110
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:07.000Z","uuid":"r7","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_N1","name":"work-create","input":{}}]}}'
111
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:08.000Z","uuid":"r8","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_N1","is_error":true,"content":"<tool_use_error>Error: No such tool available: work-create NOTREJ_NOTOOL</tool_use_error>"}]}}'
112
+ # NOT a rejection: MCP server-side validation (the call DID reach the server)
113
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:09.000Z","uuid":"r9","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_N2","name":"outlook-mail-fetch-body","input":{}}]}}'
114
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:10.000Z","uuid":"rA","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_N2","is_error":true,"content":"MCP error -32602: Input validation error: id: Required NOTREJ_32602"}]}}'
115
+ # NOT a rejection: built-in tool that RAN and failed its precondition
116
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T15:59:11.000Z","uuid":"rB","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_N3","name":"Edit","input":{}}]}}'
117
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T15:59:12.000Z","uuid":"rC","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_N3","is_error":true,"content":"<tool_use_error>File has not been read yet. Read it first before writing to it. NOTREJ_UNREAD</tool_use_error>"}]}}'
118
+ } > "$f"
119
+ }
120
+
121
+ # Write a subagent transcript whose ONLY error is a harness rejection. Used to
122
+ # prove --scan-subagent-errors does not report a rejection-only subagent as an
123
+ # error-carrying one: the subagent did not fail, a call was never dispatched.
124
+ write_subagent_rejection_only() {
125
+ local cfg="$1" parent_uuid="$2" agent="$3" atype="$4"
126
+ local d="$cfg/projects/-fake-cwd/${parent_uuid}/subagents"
127
+ mkdir -p "$d"
128
+ {
129
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T16:00:00.000Z","uuid":"j1","message":{"role":"user","content":"read it"}}'
130
+ printf '%s\n' '{"type":"assistant","timestamp":"2026-07-16T16:00:01.000Z","uuid":"j2","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_SR","name":"Read","input":{"__unparsedToolInput":"{\"file_path\": /x}"}}]}}'
131
+ printf '%s\n' '{"type":"user","timestamp":"2026-07-16T16:00:02.000Z","uuid":"j3","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_SR","is_error":true,"content":"<tool_use_error>InputValidationError: Read was called with input that could not be parsed as JSON.\nYou sent (first 200 of 30 bytes): {\"file_path\": /x} SUBREJ_ONLY</tool_use_error>"}]}}'
132
+ } > "$d/${agent}.jsonl"
133
+ printf '{"agentType":"%s","description":"read a file"}\n' "$atype" > "$d/${agent}.meta.json"
134
+ }
135
+
90
136
  # Write a CLEAN subagent transcript (no errors) + its meta.
91
137
  write_subagent_clean() {
92
138
  local cfg="$1" parent_uuid="$2" agent="$3" atype="$4"
@@ -248,6 +294,75 @@ case_parent_error_not_subagent() {
248
294
  return 0
249
295
  }
250
296
 
297
+ # --- Case J: harness rejection is distinguished from a tool that RAN and failed ---
298
+ # The 1692 defect: a call the CLI refused to dispatch rendered identically to a
299
+ # tool that ran and returned a failure, so a rejection loop read as "the tool is
300
+ # broken" while the tool was healthy. Only InputValidationError may be relabelled.
301
+ case_rejection_classified_and_counted() {
302
+ local root="/tmp/maxy-1692-J-$$"; local out
303
+ out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
304
+ write_parent_with_rejections "$cfg" "$UUID_A"
305
+ write_pidfile "$cfg" 4255 "$UUID_A" "$BRIDGE"
306
+
307
+ invoke "$script" "$cfg" "session_${BRIDGE}"
308
+ cleanup_tree "$root"
309
+
310
+ [[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
311
+
312
+ # Both InputValidationError sub-classes are rejections (neither dispatched).
313
+ [[ "$OUT" == *"REJECTED"*"REJECT_A_ONE"* ]] || { echo " sub-class A (unparseable) not flagged REJECTED"; return 1; }
314
+ [[ "$OUT" == *"REJECTED"*"REJECT_B_ONE"* ]] || { echo " sub-class B (schema-invalid) not flagged REJECTED"; return 1; }
315
+
316
+ # The exclusions MUST keep the plain tool-error rendering. Widening the
317
+ # predicate to the bare <tool_use_error> wrapper is the regression this pins.
318
+ local l
319
+ for l in NOTREJ_NOTOOL NOTREJ_32602 NOTREJ_UNREAD; do
320
+ grep -q "tool error.*$l" <<<"$OUT" || { echo " $l lost its '‼ tool error' rendering"; return 1; }
321
+ grep -q "REJECTED.*$l" <<<"$OUT" && { echo " $l wrongly flagged REJECTED"; return 1; }
322
+ done
323
+
324
+ # Repetition is the diagnostic fact: 2 identical calls on one tool, counted.
325
+ [[ "$OUT" == *"rejections: 3"* ]] || { echo " census missing or miscounted (want 3)"; return 1; }
326
+ [[ "$OUT" == *"outlook-mail-send x2"* ]] || { echo " census does not count the repeat per tool"; return 1; }
327
+ return 0
328
+ }
329
+
330
+ # --- Case K: a clean session emits NO census line (non-regression) ---
331
+ case_no_rejections_no_census() {
332
+ local root="/tmp/maxy-1692-K-$$"; local out
333
+ out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
334
+ write_parent_with_error "$cfg" "$UUID_A"
335
+ write_pidfile "$cfg" 4256 "$UUID_A" "$BRIDGE"
336
+
337
+ invoke "$script" "$cfg" "session_${BRIDGE}"
338
+ cleanup_tree "$root"
339
+
340
+ [[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC"; return 1; }
341
+ [[ "$OUT" != *"rejections:"* ]] || { echo " census line emitted for a session with no rejections"; return 1; }
342
+ [[ "$OUT" != *"REJECTED"* ]] || { echo " genuine tool error wrongly flagged REJECTED"; return 1; }
343
+ return 0
344
+ }
345
+
346
+ # --- Case L: --scan-subagent-errors does not report a rejection-only subagent ---
347
+ # A rejection is not a subagent failure: the subagent did not fail, a call was
348
+ # never dispatched. Counting it re-creates the 1692 conflation one mode over.
349
+ case_scan_ignores_rejection_only_subagent() {
350
+ local root="/tmp/maxy-1692-L-$$"; local out
351
+ out=$(setup_tree "$root"); local script="${out% *}"; local cfg="${out#* }"
352
+ write_parent "$cfg" "$UUID_A"
353
+ write_subagent_rejection_only "$cfg" "$UUID_A" "agent-aREJ00001" "content-producer"
354
+ write_subagent_failed "$cfg" "$UUID_A" "agent-aFAIL0001" "pdf-renderer"
355
+
356
+ invoke "$script" "$cfg" --scan-subagent-errors
357
+ cleanup_tree "$root"
358
+
359
+ [[ $RC -eq 0 ]] || { echo " expected exit 0, got $RC; err=$ERR"; return 1; }
360
+ [[ "$OUT" == *"aFAIL0001"* ]] || { echo " genuinely failed subagent missing from scan"; return 1; }
361
+ [[ "$OUT" != *"SUBREJ_ONLY"* ]] || { echo " rejection-only subagent reported as error-carrying"; return 1; }
362
+ [[ "$OUT" != *"aREJ00001"* ]] || { echo " rejection-only subagent listed by scan"; return 1; }
363
+ return 0
364
+ }
365
+
251
366
  # --- Case I: content-grep resolves a unique body match (last-resort tier) ---
252
367
  case_content_grep_single() {
253
368
  local root="/tmp/maxy-585-I-$$"; local out
@@ -299,6 +414,9 @@ run_case "parent's own tool error not mislabelled SUBAGENT ERROR" case_pare
299
414
  run_case "content-grep resolves a unique body match" case_content_grep_single
300
415
  run_case "content-grep refuses an ambiguous (multi) body match" case_content_grep_ambiguous_refuses
301
416
  run_case "unresolvable key → exit 1" case_unresolvable_miss
417
+ run_case "harness rejection classified + counted, exclusions intact" case_rejection_classified_and_counted
418
+ run_case "no rejections → no census line" case_no_rejections_no_census
419
+ run_case "scan ignores a rejection-only subagent" case_scan_ignores_rejection_only_subagent
302
420
 
303
421
  echo ""
304
422
  echo "================================================"
@@ -627,7 +627,7 @@ tail_mode() {
627
627
  # non-interactive SSH PATH. argv[1] selects the mode (timeline|scan); the
628
628
  # program is fed via -c so logs-read.sh stays a single self-contained file.
629
629
  JSONL_PY=$(cat <<'PY'
630
- import sys, os, re, json, glob
630
+ import sys, os, re, json, glob, collections
631
631
 
632
632
  MODE = sys.argv[1] if len(sys.argv) > 1 else ''
633
633
 
@@ -776,7 +776,40 @@ def subagent_dir(projects_root, uuid):
776
776
  # failure (the task's target, flagged 'SUBAGENT ERROR' and counted) from the
777
777
  # parent session's own tool error (shown plainly, never counted as a subagent
778
778
  # error). Returns the count of SUBAGENT errors found (0 for the parent).
779
- def collect_events(path, source, events, is_sub):
779
+ # A harness rejection is a call that NEVER REACHED its tool: the CLI refused the
780
+ # model's arguments, so no MCP dispatch happened and no plugin line exists
781
+ # anywhere (not in server.log, not in mcp-<server>-<sessionId>.log, not in
782
+ # mcp-lifeline). Rendering it as a plain tool error reads as "the tool is broken"
783
+ # when the tool was never called -- the confusion that produced a false
784
+ # customer-facing fault report (task 1692).
785
+ #
786
+ # InputValidationError has two sub-classes and BOTH are rejections, because
787
+ # neither dispatches. The label says 'never dispatched', not 'malformed', because
788
+ # only the first is malformed:
789
+ # A. "...could not be parsed as JSON" -- args unparseable; the tool_use block
790
+ # carries __unparsedToolInput. 52 of 170 measured 2026-07-16.
791
+ # B. "...failed due to the following issue(s)" -- the JSON parsed fine and
792
+ # violated the schema (wrong type, missing required). No __unparsedToolInput.
793
+ # 118 of 170, so the majority; calling it 'malformed' would be wrong.
794
+ #
795
+ # This is the ONLY class relabelled. Deliberately NOT covered, all of which keep
796
+ # rendering as '‼ tool error':
797
+ # 'Error: No such tool available:' / 'Unknown skill:' -- different fault, and
798
+ # the missing-tool case already has a PostToolUse hook (mcp-tool-missing.sh)
799
+ # 'MCP error -32602' -- reached the server; its SDK rejected it
800
+ # every unwrapped is_error payload -- the tool ran and returned a failure
801
+ #
802
+ # The <tool_use_error> wrapper alone is NOT a rejection discriminator: measured
803
+ # 2026-07-16 across 733 SiteDesk transcripts it wraps mostly built-in tools that
804
+ # RAN and failed ('File has not been read yet' x60, 'File has been modified
805
+ # since read' x16). Match the error class, not the wrapper.
806
+ def is_rejection(text):
807
+ if not isinstance(text, str):
808
+ return False
809
+ return text.lstrip().startswith('<tool_use_error>InputValidationError:')
810
+
811
+
812
+ def collect_events(path, source, events, is_sub, rejects=None):
780
813
  rows = load_jsonl(path)
781
814
  id2tool = {}
782
815
  for r in rows:
@@ -812,10 +845,18 @@ def collect_events(path, source, events, is_sub):
812
845
  for b in content:
813
846
  if isinstance(b, dict) and b.get('type') == 'tool_result' and b.get('is_error'):
814
847
  tool = id2tool.get(b.get('tool_use_id'))
815
- flag = '‼ SUBAGENT ERROR' if is_sub else '‼ tool error'
816
- events.append((ts, source, 'error', '%s tool=%s error="%s"' % (flag, tool or '?', clip(result_text(b, r), 200))))
817
- if is_sub:
818
- n_err += 1
848
+ body = result_text(b, r)
849
+ if is_rejection(body):
850
+ # Not a tool failure: the call never ran. Never
851
+ # counted as a subagent error for the same reason.
852
+ flag = '‼ REJECTED (never dispatched)'
853
+ if rejects is not None:
854
+ rejects[tool or '?'] += 1
855
+ else:
856
+ flag = '‼ SUBAGENT ERROR' if is_sub else '‼ tool error'
857
+ if is_sub:
858
+ n_err += 1
859
+ events.append((ts, source, 'error', '%s tool=%s error="%s"' % (flag, tool or '?', clip(body, 200))))
819
860
  elif isinstance(content, str) and content.strip():
820
861
  events.append((ts, source, 'text', 'user: ' + clip(content, 160)))
821
862
  return n_err
@@ -826,18 +867,25 @@ def timeline(projects_root, sessions_dir, key):
826
867
  sys.stderr.write('-- trailer: key=%s resolved=no reason=%s\n' % (key, via))
827
868
  return 1
828
869
  events = []
870
+ rejects = collections.Counter()
829
871
  if parent_path and os.path.exists(parent_path):
830
- collect_events(parent_path, 'parent', events, False)
872
+ collect_events(parent_path, 'parent', events, False, rejects)
831
873
  sdir = subagent_dir(projects_root, uuid)
832
874
  sub_count = err_count = 0
833
875
  if sdir:
834
876
  for af in sorted(glob.glob(os.path.join(sdir, 'agent-*.jsonl'))):
835
877
  sub_count += 1
836
- err_count += collect_events(af, agent_type(af) or os.path.basename(af)[:-6], events, True)
878
+ err_count += collect_events(af, agent_type(af) or os.path.basename(af)[:-6], events, True, rejects)
837
879
  events.sort(key=lambda e: e[0])
838
880
  print('# session %s (resolved via %s)' % (uuid, via))
839
881
  print('# parent: %s' % (parent_path or '(no parent transcript on disk)'))
840
882
  print('# subagents: %d subagent-errors: %d' % (sub_count, err_count))
883
+ # Repetition is the diagnostic fact: one fat-fingered call is a typo, N
884
+ # identical ones against a single tool is a wedged loop. Counting it here
885
+ # makes that one greppable line instead of N unrelated ones.
886
+ if rejects:
887
+ top = ' '.join('%s x%d' % (t, n) for t, n in rejects.most_common())
888
+ print('# rejections: %d (%s)' % (sum(rejects.values()), top))
841
889
  print('')
842
890
  for ts, source, _kind, text in events:
843
891
  print('%s [%s] %s' % (ts or ('-' * 24), source, text))
@@ -871,7 +919,13 @@ def scan(projects_root, limit):
871
919
  if isinstance(content, list):
872
920
  for b in content:
873
921
  if isinstance(b, dict) and b.get('type') == 'tool_result' and b.get('is_error'):
874
- errs.append((id2tool.get(b.get('tool_use_id')), result_text(b, r)))
922
+ body = result_text(b, r)
923
+ # A rejection is not a subagent failure: the subagent did
924
+ # not fail, a call was never dispatched. Counting it here
925
+ # would re-create the conflation the timeline just fixed.
926
+ if is_rejection(body):
927
+ continue
928
+ errs.append((id2tool.get(b.get('tool_use_id')), body))
875
929
  if errs:
876
930
  parent_uuid = os.path.basename(os.path.dirname(os.path.dirname(af)))
877
931
  findings.append((af, agent_type(af), parent_uuid, errs))