@vellumai/assistant 0.11.4 → 0.11.5-staging.2

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 (367) hide show
  1. package/AGENTS.md +2 -2
  2. package/ARCHITECTURE.md +29 -4
  3. package/README.md +1 -1
  4. package/docs/architecture/memory.md +17 -5
  5. package/docs/architecture/security.md +96 -152
  6. package/docs/guardian-request-flow.md +13 -2
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  9. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  10. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  15. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  16. package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
  17. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
  19. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  21. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/openapi.yaml +193 -3
  24. package/package.json +1 -1
  25. package/src/__tests__/anthropic-provider.test.ts +10 -3
  26. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  27. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  28. package/src/__tests__/channel-approval-routes.test.ts +4 -0
  29. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
  30. package/src/__tests__/channel-reply-delivery.test.ts +26 -13
  31. package/src/__tests__/checker.test.ts +20 -314
  32. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  33. package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
  34. package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
  35. package/src/__tests__/config-loader-backfill.test.ts +19 -10
  36. package/src/__tests__/container-cpu-sampler.test.ts +153 -0
  37. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  38. package/src/__tests__/conversation-attachments.test.ts +0 -1
  39. package/src/__tests__/conversation-error.test.ts +17 -0
  40. package/src/__tests__/conversation-lifecycle.test.ts +20 -26
  41. package/src/__tests__/conversation-pairing.test.ts +186 -0
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
  43. package/src/__tests__/credential-broker-server-use.test.ts +24 -5
  44. package/src/__tests__/credential-routes.test.ts +22 -3
  45. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  46. package/src/__tests__/daemon-credential-client.test.ts +88 -0
  47. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  48. package/src/__tests__/dm-backfill.test.ts +63 -0
  49. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  50. package/src/__tests__/edit-propagation.test.ts +107 -4
  51. package/src/__tests__/events-client-registration.test.ts +28 -0
  52. package/src/__tests__/file-write-tool.test.ts +4 -2
  53. package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
  54. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
  55. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  56. package/src/__tests__/host-proxy-interface.test.ts +11 -1
  57. package/src/__tests__/host-shell-tool.test.ts +23 -4
  58. package/src/__tests__/image-conversion.test.ts +143 -1
  59. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  60. package/src/__tests__/input-repairs.test.ts +207 -0
  61. package/src/__tests__/live-workspace-guard.test.ts +75 -0
  62. package/src/__tests__/managed-profile-guard.test.ts +4 -2
  63. package/src/__tests__/mcp-abort-signal.test.ts +1 -1
  64. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  65. package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
  66. package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
  67. package/src/__tests__/mtime-cache.test.ts +2 -0
  68. package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
  69. package/src/__tests__/openai-provider.test.ts +22 -0
  70. package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
  71. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  72. package/src/__tests__/platform.test.ts +16 -1
  73. package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
  74. package/src/__tests__/plugin-api-shim.test.ts +5 -0
  75. package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
  76. package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
  77. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  78. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  79. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  80. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  81. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
  82. package/src/__tests__/reaction-persistence.test.ts +200 -7
  83. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  84. package/src/__tests__/resource-pressure-guard.test.ts +428 -0
  85. package/src/__tests__/resource-pressure-routes.test.ts +113 -0
  86. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  87. package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
  88. package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
  89. package/src/__tests__/skill-tool-factory.test.ts +161 -0
  90. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  91. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  92. package/src/__tests__/tool-executor.test.ts +63 -38
  93. package/src/__tests__/tool-policy.test.ts +97 -1
  94. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
  95. package/src/__tests__/user-plugin-loader.test.ts +2 -0
  96. package/src/__tests__/validate-input.test.ts +243 -7
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
  99. package/src/__tests__/voice-session-bridge.test.ts +42 -0
  100. package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
  101. package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
  102. package/src/acp/acp-claude-oauth.ts +38 -21
  103. package/src/acp/prepare-agent-env.ts +146 -55
  104. package/src/agent/loop-tool-dedup.test.ts +161 -0
  105. package/src/agent/loop.ts +68 -22
  106. package/src/api/constants/app-tools.ts +34 -0
  107. package/src/api/events/resource-pressure-status-changed.ts +53 -0
  108. package/src/api/index.ts +19 -0
  109. package/src/api/responses/resource-pressure-status.ts +25 -0
  110. package/src/approvals/guardian-channel-delivery.ts +70 -6
  111. package/src/approvals/guardian-request-resolvers.ts +32 -66
  112. package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
  113. package/src/calls/voice-session-bridge.ts +60 -6
  114. package/src/channels/__tests__/message-audience.test.ts +49 -0
  115. package/src/channels/__tests__/types.test.ts +17 -3
  116. package/src/channels/message-audience.ts +40 -0
  117. package/src/channels/types.ts +16 -12
  118. package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
  119. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
  120. package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
  121. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  122. package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
  123. package/src/cli/commands/__tests__/keys.test.ts +89 -8
  124. package/src/cli/commands/__tests__/plugins.test.ts +57 -1
  125. package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
  126. package/src/cli/commands/channel-verification-sessions.ts +11 -21
  127. package/src/cli/commands/conversations.help.ts +34 -0
  128. package/src/cli/commands/conversations.ts +125 -0
  129. package/src/cli/commands/inference-providers.ts +9 -4
  130. package/src/cli/commands/inference.help.ts +9 -4
  131. package/src/cli/commands/keys.help.ts +13 -1
  132. package/src/cli/commands/keys.ts +26 -15
  133. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  134. package/src/cli/commands/memory/index.help.ts +41 -27
  135. package/src/cli/commands/memory/index.ts +2 -0
  136. package/src/cli/commands/memory/memory-v2.ts +58 -54
  137. package/src/cli/commands/memory/memory-validate.ts +18 -0
  138. package/src/cli/commands/monitoring.ts +1 -0
  139. package/src/cli/commands/plugins.help.ts +28 -31
  140. package/src/cli/commands/plugins.ts +10 -0
  141. package/src/cli/commands/skills.help.ts +13 -16
  142. package/src/cli/commands/trust.ts +3 -14
  143. package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
  144. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
  145. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
  146. package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
  147. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
  148. package/src/cli/lib/bundled-marketplace.json +13 -0
  149. package/src/cli/lib/daemon-credential-client.ts +25 -3
  150. package/src/cli/lib/install-from-github.ts +35 -24
  151. package/src/cli/lib/merge-plugin-tree.ts +22 -4
  152. package/src/cli/lib/plugin-surfaces.ts +27 -0
  153. package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
  154. package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
  155. package/src/config/default-profile-catalog.ts +5 -6
  156. package/src/config/feature-flag-registry.json +32 -32
  157. package/src/config/webhook-routing.ts +8 -0
  158. package/src/context/compactor.ts +31 -2
  159. package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
  160. package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
  161. package/src/daemon/conversation-error.ts +13 -0
  162. package/src/daemon/conversation-messaging.ts +22 -2
  163. package/src/daemon/conversation-process.ts +11 -0
  164. package/src/daemon/conversation-store.ts +10 -0
  165. package/src/daemon/conversation-surfaces.ts +21 -8
  166. package/src/daemon/conversation.ts +12 -10
  167. package/src/daemon/lifecycle.ts +6 -2
  168. package/src/daemon/message-types/conversations.ts +5 -7
  169. package/src/daemon/provider-rejection-log-fields.ts +124 -0
  170. package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
  171. package/src/daemon/resource-pressure-guard.ts +387 -0
  172. package/src/daemon/shutdown-handlers.ts +2 -0
  173. package/src/daemon/startup-error.ts +16 -0
  174. package/src/daemon/trust-context.ts +14 -10
  175. package/src/daemon/unsendable-image-notice.ts +76 -0
  176. package/src/hooks/registry.ts +65 -51
  177. package/src/ipc/__tests__/socket-path.test.ts +15 -7
  178. package/src/ipc/gateway-client.test.ts +1 -1
  179. package/src/ipc/gateway-client.ts +14 -21
  180. package/src/ipc/socket-cleanup.ts +2 -11
  181. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
  182. package/src/messaging/provider-message-metadata.ts +128 -0
  183. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
  184. package/src/messaging/providers/channel-transport.ts +90 -13
  185. package/src/messaging/providers/discord/send.ts +16 -0
  186. package/src/messaging/providers/discord/transport.ts +11 -1
  187. package/src/messaging/providers/index.ts +109 -16
  188. package/src/messaging/providers/slack/message-metadata.ts +45 -0
  189. package/src/messaging/providers/slack/render-transcript.ts +1 -4
  190. package/src/messaging/providers/slack/send.test.ts +6 -9
  191. package/src/messaging/providers/slack/send.ts +40 -28
  192. package/src/messaging/providers/slack/transport.ts +22 -26
  193. package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
  194. package/src/messaging/providers/telegram-bot/transport.ts +3 -6
  195. package/src/messaging/read-provider-metadata.test.ts +157 -0
  196. package/src/messaging/read-provider-metadata.ts +51 -0
  197. package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
  198. package/src/notifications/__tests__/edit-notification.test.ts +174 -2
  199. package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
  200. package/src/notifications/adapters/macos.ts +55 -0
  201. package/src/notifications/adapters/slack.ts +10 -5
  202. package/src/notifications/assistant-reply-producer.ts +37 -4
  203. package/src/notifications/conversation-pairing.ts +111 -21
  204. package/src/notifications/edit-notification.ts +38 -8
  205. package/src/notifications/emit-signal.ts +12 -14
  206. package/src/notifications/home-feed-side-effect.ts +218 -15
  207. package/src/notifications/types.ts +5 -0
  208. package/src/permissions/AGENTS.md +16 -0
  209. package/src/permissions/checker.test.ts +75 -122
  210. package/src/permissions/checker.ts +44 -560
  211. package/src/permissions/confirmation-guardian-request.test.ts +28 -3
  212. package/src/permissions/confirmation-guardian-request.ts +5 -1
  213. package/src/persistence/conversation-crud.ts +23 -20
  214. package/src/persistence/conversation-queries.ts +14 -1
  215. package/src/persistence/conversation-types.ts +22 -0
  216. package/src/persistence/delivery-crud.ts +122 -15
  217. package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
  218. package/src/plugin-api/conversation-turn.ts +27 -0
  219. package/src/plugin-api/credential-scope.test.ts +15 -0
  220. package/src/plugin-api/credential-scope.ts +14 -0
  221. package/src/plugin-api/index.ts +19 -1
  222. package/src/plugin-api/resolve-credential.ts +15 -10
  223. package/src/plugin-api/store-credential.ts +148 -0
  224. package/src/plugin-api/system-card.ts +36 -0
  225. package/src/plugin-api/vision-support.test.ts +82 -0
  226. package/src/plugin-api/vision-support.ts +14 -2
  227. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
  228. package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
  229. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
  230. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
  231. package/src/plugins/defaults/image-recovery/detect.ts +24 -1
  232. package/src/plugins/defaults/main.ts +15 -0
  233. package/src/plugins/defaults/memory/AGENTS.md +11 -3
  234. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  235. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
  236. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  237. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  238. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
  239. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  240. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  241. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  242. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  243. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  244. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  245. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
  246. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
  247. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  248. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  249. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  250. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  251. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  252. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  253. package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
  254. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
  255. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  256. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  257. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  258. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  259. package/src/plugins/mtime-cache.ts +1 -1
  260. package/src/plugins/pipeline.ts +14 -7
  261. package/src/plugins/plugin-execution-context.ts +35 -11
  262. package/src/plugins/plugin-tree-walk.ts +63 -4
  263. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  264. package/src/providers/anthropic/client.ts +332 -241
  265. package/src/providers/connection-resolution.ts +2 -1
  266. package/src/providers/content-blocks.ts +22 -0
  267. package/src/providers/gemini/client.ts +5 -2
  268. package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
  269. package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
  270. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
  271. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
  272. package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
  273. package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
  274. package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
  275. package/src/providers/inference/adapter-factory.ts +33 -2
  276. package/src/providers/inference/auth.ts +13 -2
  277. package/src/providers/inference/credential-usage.ts +37 -0
  278. package/src/providers/inference/missing-credential-guard.ts +110 -0
  279. package/src/providers/inference/resolve-auth.ts +7 -7
  280. package/src/providers/media-resolve.ts +176 -15
  281. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
  282. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  283. package/src/providers/openai/chat-completions-provider.ts +322 -42
  284. package/src/routes/route-host-protocol.ts +7 -0
  285. package/src/routes/worker.ts +31 -10
  286. package/src/runtime/AGENTS.md +35 -0
  287. package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
  288. package/src/runtime/__tests__/web-presence.test.ts +234 -0
  289. package/src/runtime/assistant-event-hub.ts +38 -0
  290. package/src/runtime/channel-reply-delivery.ts +54 -31
  291. package/src/runtime/channel-retry-sweep.ts +6 -4
  292. package/src/runtime/effective-capabilities.test.ts +19 -0
  293. package/src/runtime/effective-capabilities.ts +25 -0
  294. package/src/runtime/guardian-reply-router.ts +6 -2
  295. package/src/runtime/http-errors.ts +1 -0
  296. package/src/runtime/http-server.ts +20 -5
  297. package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
  298. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
  299. package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
  300. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
  301. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
  302. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
  303. package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
  304. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  305. package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
  306. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  307. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
  308. package/src/runtime/routes/approval-routes.ts +37 -4
  309. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
  310. package/src/runtime/routes/canned-message-complete.ts +68 -29
  311. package/src/runtime/routes/client-routes.ts +113 -0
  312. package/src/runtime/routes/conversation-list-routes.ts +22 -7
  313. package/src/runtime/routes/conversation-management-routes.ts +15 -9
  314. package/src/runtime/routes/conversation-routes.ts +36 -22
  315. package/src/runtime/routes/credential-in-use.ts +85 -0
  316. package/src/runtime/routes/credential-routes.ts +55 -85
  317. package/src/runtime/routes/guardian-approval-interception.ts +22 -22
  318. package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
  319. package/src/runtime/routes/identity-routes.ts +5 -121
  320. package/src/runtime/routes/inbound-message-handler.ts +71 -27
  321. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
  322. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
  323. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
  324. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
  325. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
  326. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
  327. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
  328. package/src/runtime/routes/index.ts +2 -0
  329. package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
  330. package/src/runtime/routes/monitoring-routes.ts +9 -1
  331. package/src/runtime/routes/plugins-routes.ts +17 -5
  332. package/src/runtime/routes/resource-pressure-routes.ts +22 -0
  333. package/src/runtime/routes/secret-routes.ts +39 -8
  334. package/src/runtime/routes/settings-routes.ts +6 -6
  335. package/src/runtime/routes/surface-action-routes.ts +20 -19
  336. package/src/runtime/routes/user-route-dispatcher.ts +31 -2
  337. package/src/runtime/routes/user-route-resolution.ts +18 -1
  338. package/src/runtime/slack-reply-session.test.ts +23 -13
  339. package/src/runtime/slack-reply-session.ts +38 -55
  340. package/src/runtime/web-presence.ts +90 -0
  341. package/src/skills/validate-input.ts +177 -40
  342. package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
  343. package/src/subagent/consult-context.ts +7 -23
  344. package/src/tools/credentials/broker.ts +24 -79
  345. package/src/tools/credentials/ref-parse.ts +35 -0
  346. package/src/tools/credentials/resolve.ts +4 -10
  347. package/src/tools/credentials/store.ts +168 -0
  348. package/src/tools/credentials/tool-policy.ts +67 -0
  349. package/src/tools/executor.ts +53 -15
  350. package/src/tools/network/__tests__/web-search.test.ts +41 -1
  351. package/src/tools/network/url-safety.ts +9 -16
  352. package/src/tools/network/web-search.ts +21 -3
  353. package/src/tools/permission-checker.ts +43 -52
  354. package/src/tools/schema-transforms.ts +40 -5
  355. package/src/tools/shared/input-repairs.ts +160 -0
  356. package/src/tools/skills/skill-tool-factory.ts +18 -4
  357. package/src/tools/subagent/spawn.ts +0 -1
  358. package/src/tools/tool-approval-handler.ts +16 -10
  359. package/src/tools/tool-types.ts +26 -13
  360. package/src/tools/types.ts +6 -6
  361. package/src/util/__tests__/cgroup-memory.test.ts +3 -0
  362. package/src/util/cgroup-memory.ts +3 -0
  363. package/src/util/container-cpu-sampler.ts +250 -0
  364. package/src/util/image-conversion.ts +178 -14
  365. package/src/util/platform.ts +93 -10
  366. package/src/permissions/ipc-risk-types.ts +0 -143
  367. package/src/permissions/risk-types.ts +0 -76
@@ -45,7 +45,11 @@
45
45
  * 6. Verify the run drained the buffer. `runResult.ok` only means the
46
46
  * background run completed — the trim itself is delegated to the agent.
47
47
  * A run that completes without shrinking the buffer is reported as
48
- * `invoked` with `noProgress: true` and enqueues no follow-ups.
48
+ * `invoked` with `noProgress: true` and enqueues no follow-ups. The
49
+ * post-run page index is also read for `danglingLinks` (structural
50
+ * references with no target page): reported on the outcome and in the
51
+ * log, and fed into the NEXT pass's prompt as a repair step like
52
+ * `parseFailures`. They never gate follow-ups.
49
53
  * 7. On progress, enqueue `memory_v2_reembed` (re-index any pages the agent
50
54
  * touched). Tracking touched pages via mtime would be more precise but
51
55
  * is fragile across filesystems; the embedder's content-hash cache makes
@@ -104,6 +108,7 @@ import {
104
108
  tryAcquireLock,
105
109
  } from "./consolidation-lock.js";
106
110
  import { getPageIndex, type PageParseFailure } from "./page-index.js";
111
+ import type { DanglingLink } from "./page-links.js";
107
112
  import { resolveConsolidationPrompt } from "./prompts/consolidation.js";
108
113
  import { resolveSubstrateTuning } from "./tuning.js";
109
114
 
@@ -296,8 +301,17 @@ export type ConsolidationOutcome =
296
301
  * follow-ups were enqueued.
297
302
  */
298
303
  noProgress: boolean;
304
+ /**
305
+ * Structural references (`links:`, `[[wikilinks]]`, `edges:`) in the
306
+ * post-run corpus whose target page does not exist; `null` when the
307
+ * post-run index could not be read.
308
+ */
309
+ danglingLinks: number | null;
299
310
  };
300
311
 
312
+ /** Dangling links named in the post-run warn line before the count takes over. */
313
+ const MAX_LOGGED_DANGLING_LINKS = 20;
314
+
301
315
  export async function memoryV2ConsolidateJob(
302
316
  _job: MemoryJob,
303
317
  config: AssistantConfig,
@@ -419,16 +433,20 @@ export async function memoryV2ConsolidateJob(
419
433
  // v2-only install must keep producing `summary:`-bearing fragment pages.
420
434
  const memoryV3Live = isMemoryV3Live(config);
421
435
  // Pages the index build dropped (malformed frontmatter) or degraded
422
- // (unterminated fence) — rendered into the prompt's repair step so the
423
- // agent fixes them this pass. Best-effort: prompt assembly must never
424
- // fail because the index build did.
436
+ // (unterminated fence), and structural references with no target page,
437
+ // rendered into the prompt's repair steps so the agent fixes them this
438
+ // pass. Best-effort: prompt assembly must never fail because the index
439
+ // build did.
425
440
  let parseFailures: PageParseFailure[] = [];
441
+ let danglingLinks: DanglingLink[] = [];
426
442
  try {
427
- parseFailures = (await getPageIndex(getWorkspaceDir())).parseFailures;
443
+ const index = await getPageIndex(getWorkspaceDir());
444
+ parseFailures = index.parseFailures;
445
+ danglingLinks = index.danglingLinks;
428
446
  } catch (err) {
429
447
  log.warn(
430
448
  { err },
431
- "consolidation: page-index read failed; omitting the repair section",
449
+ "consolidation: page-index read failed; omitting the repair sections",
432
450
  );
433
451
  }
434
452
  const prompt = resolveConsolidationPrompt(
@@ -438,6 +456,7 @@ export async function memoryV2ConsolidateJob(
438
456
  includeCorePagesSection: memoryV3Live,
439
457
  articleShape: memoryV3Live ? "v3" : "v2",
440
458
  parseFailures,
459
+ danglingLinks,
441
460
  },
442
461
  );
443
462
 
@@ -496,6 +515,13 @@ export async function memoryV2ConsolidateJob(
496
515
  const bufferLinesAfter = countBufferLines(bufferPath);
497
516
  const noProgress = bufferLinesAfter >= bufferLinesBefore;
498
517
 
518
+ // The agent's file-tool writes invalidate the page index, so this read
519
+ // sees the post-run corpus.
520
+ const danglingAfter = await readDanglingLinks(
521
+ runResult.conversationId,
522
+ danglingLinks.length,
523
+ );
524
+
499
525
  // Failure-state bookkeeping. A skipped run (`skipReason`) never invoked
500
526
  // the agent, so it neither clears nor records. A completed run that made
501
527
  // no progress behaves like a failure for scheduling — the size trigger
@@ -526,6 +552,7 @@ export async function memoryV2ConsolidateJob(
526
552
  deferredEntries,
527
553
  followUpJobIds: [],
528
554
  noProgress: true,
555
+ danglingLinks: danglingAfter,
529
556
  };
530
557
  }
531
558
 
@@ -577,12 +604,47 @@ export async function memoryV2ConsolidateJob(
577
604
  deferredEntries,
578
605
  followUpJobIds,
579
606
  noProgress: false,
607
+ danglingLinks: danglingAfter,
580
608
  };
581
609
  } finally {
582
610
  releaseLock(lockPath);
583
611
  }
584
612
  }
585
613
 
614
+ /**
615
+ * Post-run dangling-link count; warns (with a capped sample and the pre-run
616
+ * count) when any remain. `null` when the index cannot be read.
617
+ */
618
+ async function readDanglingLinks(
619
+ conversationId: string,
620
+ danglingBefore: number,
621
+ ): Promise<number | null> {
622
+ let dangling: DanglingLink[];
623
+ try {
624
+ dangling = (await getPageIndex(getWorkspaceDir())).danglingLinks;
625
+ } catch (err) {
626
+ log.warn(
627
+ { err, conversationId },
628
+ "consolidation: post-run page-index read failed; dangling links unknown",
629
+ );
630
+ return null;
631
+ }
632
+ if (dangling.length > 0) {
633
+ log.warn(
634
+ {
635
+ conversationId,
636
+ danglingBefore,
637
+ danglingAfter: dangling.length,
638
+ sample: dangling
639
+ .slice(0, MAX_LOGGED_DANGLING_LINKS)
640
+ .map((d) => `${d.from} -> ${d.to} (${d.kind})`),
641
+ },
642
+ "consolidation left structural links whose target page does not exist; the next pass renders them as a repair step",
643
+ );
644
+ }
645
+ return dangling.length;
646
+ }
647
+
586
648
  /**
587
649
  * Read `memory/buffer.md`. Missing file → empty string so the skip-on-empty
588
650
  * branch doesn't have to distinguish "no file" from "blank file".
@@ -166,37 +166,6 @@ export function getReachable(
166
166
  return result;
167
167
  }
168
168
 
169
- export interface EdgeValidationResult {
170
- ok: boolean;
171
- /**
172
- * Outgoing-edge targets that don't correspond to any known slug. Sorted by
173
- * `(from, to)` for deterministic output.
174
- */
175
- missing: Array<{ from: string; to: string }>;
176
- }
177
-
178
- /**
179
- * Validate every outgoing edge target against `knownSlugs`. Returns a sorted
180
- * list of `(from, to)` pairs whose `to` slug has no corresponding concept
181
- * page on disk.
182
- */
183
- export function validateEdgeTargets(
184
- index: EdgeIndex,
185
- knownSlugs: ReadonlySet<string>,
186
- ): EdgeValidationResult {
187
- const missing: Array<{ from: string; to: string }> = [];
188
- const sources = [...index.outgoing.keys()].sort();
189
- for (const from of sources) {
190
- const targets = [...(index.outgoing.get(from) ?? [])].sort();
191
- for (const to of targets) {
192
- if (!knownSlugs.has(to)) {
193
- missing.push({ from, to });
194
- }
195
- }
196
- }
197
- return { ok: missing.length === 0, missing };
198
- }
199
-
200
169
  /**
201
170
  * Total count of directed edges in the index — i.e. the sum of every page's
202
171
  * outgoing-edge fanout. Each (from, to) pair counts once.
@@ -29,7 +29,9 @@ import {
29
29
  tryAcquireLock,
30
30
  } from "./consolidation-lock.js";
31
31
  import { parseOriginDate } from "./page-index.js";
32
+ import { findDanglingLinks } from "./page-links.js";
32
33
  import {
34
+ isValidSlug,
33
35
  listPages,
34
36
  parsePageContent,
35
37
  validateSlug,
@@ -127,6 +129,14 @@ const REINDEX_JOB_TYPES: readonly MemoryJobType[] = [
127
129
  * consolidation lock, so a page committed by another writer between
128
130
  * validation and the write phase is still skipped rather than
129
131
  * overwritten.
132
+ * 5. One more non-blocking warning, once the on-disk snapshot exists: a
133
+ * `links:`, `[[wikilink]]`, or `edges:` target that is neither on disk
134
+ * nor a valid page in this batch. Retrieval drops such a reference, so
135
+ * the caller either stages the missing page or turns the reference into
136
+ * prose. Warned, not rejected: a page is not worth losing over one link,
137
+ * and a multi-batch import legitimately references a later batch.
138
+ * Targets under the synthetic capability prefixes are not checked (their
139
+ * catalog is not on disk).
130
140
  *
131
141
  * With `dryRun` the summary is returned without touching disk or the lock;
132
142
  * the dry-run collision snapshot is lock-free and therefore advisory.
@@ -250,6 +260,7 @@ export async function ingestPages(
250
260
  // concurrent writer can change what a later real run would do.
251
261
  const existingSlugs = new Set(await listPages(workspaceDir));
252
262
  classifyCollisions(pending, existingSlugs, overwrite);
263
+ warnDanglingLinks(pending, existingSlugs);
253
264
  return summarize(results, dryRun);
254
265
  }
255
266
 
@@ -264,6 +275,7 @@ export async function ingestPages(
264
275
  // writer after validation still classifies as a collision.
265
276
  const existingSlugs = new Set(await listPages(workspaceDir));
266
277
  const toWrite = classifyCollisions(pending, existingSlugs, overwrite);
278
+ warnDanglingLinks(pending, existingSlugs);
267
279
  const summary = summarize(results, dryRun);
268
280
 
269
281
  for (const page of toWrite) {
@@ -319,6 +331,53 @@ function classifyCollisions(
319
331
  return toWrite;
320
332
  }
321
333
 
334
+ /**
335
+ * Append a dangling-link warning to every page about to be written whose
336
+ * structural references name a slug that is neither on disk nor a valid page
337
+ * in this batch. Runs after collision classification so pages the batch will
338
+ * not write (`skipped_exists`) are not warned about.
339
+ */
340
+ function warnDanglingLinks(
341
+ pending: readonly PendingPage[],
342
+ existingSlugs: ReadonlySet<string>,
343
+ ): void {
344
+ const knownSlugs = new Set(existingSlugs);
345
+ for (const { page } of pending) {
346
+ knownSlugs.add(page.slug);
347
+ }
348
+ const toWrite = pending.filter(({ result }) => result.action === "written");
349
+ const dangling = findDanglingLinks(
350
+ toWrite.map(({ page }) => page),
351
+ knownSlugs,
352
+ isValidSlug,
353
+ ).filter(
354
+ (d) => !RESERVED_SLUG_PREFIXES.some((prefix) => d.to.startsWith(prefix)),
355
+ );
356
+ if (dangling.length === 0) {
357
+ return;
358
+ }
359
+ const targetsBySlug = new Map<string, string[]>();
360
+ for (const { from, to } of dangling) {
361
+ const targets = targetsBySlug.get(from) ?? [];
362
+ if (!targets.includes(to)) {
363
+ targets.push(to);
364
+ }
365
+ targetsBySlug.set(from, targets);
366
+ }
367
+ for (const { page, result } of toWrite) {
368
+ const targets = targetsBySlug.get(page.slug);
369
+ if (targets === undefined) {
370
+ continue;
371
+ }
372
+ result.warnings.push(
373
+ `link target${targets.length === 1 ? "" : "s"} not on disk or in ` +
374
+ `this batch: ${targets.join(", ")}; stage the missing page or ` +
375
+ `turn the reference into prose (retrieval drops a link whose ` +
376
+ `target page does not exist)`,
377
+ );
378
+ }
379
+ }
380
+
322
381
  function summarize(
323
382
  results: IngestPageResult[],
324
383
  dryRun: boolean,
@@ -20,7 +20,14 @@
20
20
  */
21
21
 
22
22
  import { getLogger } from "../logging.js";
23
- import { getPageMtimeMs, listPages, readPage } from "./page-store.js";
23
+ import { type DanglingLink, findDanglingLinks } from "./page-links.js";
24
+ import {
25
+ getPageMtimeMs,
26
+ isValidSlug,
27
+ listPages,
28
+ readPage,
29
+ } from "./page-store.js";
30
+ import type { ConceptPage } from "./types.js";
24
31
 
25
32
  // Dynamic import for `./skill-store.js` happens inside `getPageIndex` so that
26
33
  // modules that only need `invalidatePageIndex` (page-store.ts,
@@ -103,8 +110,9 @@ export interface PageParseFailure {
103
110
  * ASCII so IDs are deterministic across rebuilds with the same input. The
104
111
  * `bySlug` and `byId` maps are convenience lookups; `rendered` is the prompt
105
112
  * block consumed by the router. `parseFailures` lists the pages this build
106
- * dropped or degraded, so consumers (the consolidation prompt) can surface
107
- * them for repair.
113
+ * dropped or degraded and `danglingLinks` the structural references whose
114
+ * target has no entry, so consumers (the consolidation prompt, the validate
115
+ * route) can surface them for repair.
108
116
  */
109
117
  export interface PageIndex {
110
118
  entries: PageIndexEntry[];
@@ -112,6 +120,8 @@ export interface PageIndex {
112
120
  byId: Map<number, PageIndexEntry>;
113
121
  rendered: string;
114
122
  parseFailures: PageParseFailure[];
123
+ /** Resolved against every `bySlug` key, synthetic rows included. */
124
+ danglingLinks: DanglingLink[];
115
125
  }
116
126
 
117
127
  interface CachedIndex {
@@ -187,6 +197,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
187
197
 
188
198
  const drafts: DraftEntry[] = [];
189
199
  const parseFailures: PageParseFailure[] = [];
200
+ const linkSources: ConceptPage[] = [];
190
201
  for (let i = 0; i < settled.length; i++) {
191
202
  const result = settled[i];
192
203
  const slug = slugs[i];
@@ -232,6 +243,7 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
232
243
  modifiedAt: mtimeMs,
233
244
  freshAt: resolveFreshAt(page.frontmatter.origin_date, mtimeMs),
234
245
  });
246
+ linkSources.push(page);
235
247
  }
236
248
 
237
249
  for (const entry of skillEntries) {
@@ -276,8 +288,8 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
276
288
  return entry;
277
289
  });
278
290
 
279
- // Edges whose target slug isn't in the index are dropped silently — the
280
- // frontmatter sweep is responsible for surfacing those as warnings.
291
+ // Edges whose target slug isn't in the index are not resolved here; they are
292
+ // reported in `danglingLinks` below instead.
281
293
  for (let i = 0; i < entries.length; i++) {
282
294
  const draft = drafts[i];
283
295
  const resolved: number[] = [];
@@ -297,8 +309,21 @@ export async function getPageIndex(workspaceDir: string): Promise<PageIndex> {
297
309
  a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0,
298
310
  );
299
311
 
312
+ const danglingLinks = findDanglingLinks(
313
+ linkSources,
314
+ new Set(bySlug.keys()),
315
+ isValidSlug,
316
+ );
317
+
300
318
  const rendered = renderIndex(entries);
301
- const index: PageIndex = { entries, bySlug, byId, rendered, parseFailures };
319
+ const index: PageIndex = {
320
+ entries,
321
+ bySlug,
322
+ byId,
323
+ rendered,
324
+ parseFailures,
325
+ danglingLinks,
326
+ };
302
327
  cache = { workspaceDir, index };
303
328
  return index;
304
329
  }
@@ -525,9 +550,10 @@ function buildLocalPageIndex(
525
550
  bySlug: localBySlug,
526
551
  byId: localById,
527
552
  rendered: renderIndex(localEntries),
528
- // Parse failures are a property of the workspace build, not of a carved
529
- // batch — the full index is the reporting surface.
553
+ // Parse failures and dangling links are properties of the workspace
554
+ // build, not of a carved batch: the full index is the reporting surface.
530
555
  parseFailures: [],
556
+ danglingLinks: [],
531
557
  };
532
558
  }
533
559
 
@@ -626,5 +652,6 @@ function emptyPageIndex(): PageIndex {
626
652
  byId: new Map(),
627
653
  rendered: "",
628
654
  parseFailures: [],
655
+ danglingLinks: [],
629
656
  };
630
657
  }
@@ -0,0 +1,133 @@
1
+ // SUBSTRATE (v2+v3).
2
+ /**
3
+ * Memory substrate: the single owner of concept-page link syntax.
4
+ *
5
+ * A `links:` frontmatter entry is `"<target-slug> <sep> <why>"` with the
6
+ * separator {@link LINK_SEPARATOR}, split on its first occurrence (no
7
+ * separator: bare slug). An inline `[[wikilink]]` may carry `|display` and/or
8
+ * `#anchor`, neither part of the target. `edges:` entries are bare slugs.
9
+ *
10
+ * Every reader of these shapes imports from here (the v3 edge lane, the
11
+ * pending-buffer hints, the page index, ingest); do not add a second wikilink
12
+ * matcher or `links:` splitter, for the same reason `buffer-format.ts` keeps
13
+ * one buffer-entry matcher.
14
+ *
15
+ * Leaf module: imports nothing from the rest of the substrate.
16
+ */
17
+
18
+ /** Space, U+2014 EM DASH, space. */
19
+ export const LINK_SEPARATOR = ` ${String.fromCodePoint(0x2014)} `;
20
+
21
+ /** Global; group 1 is the raw inner text. Pair with {@link wikilinkTarget}. */
22
+ export const WIKILINK_REGEX = /\[\[([^\]]+)\]\]/g;
23
+
24
+ export function parseLinkEntry(entry: string): {
25
+ target: string;
26
+ description: string | undefined;
27
+ } {
28
+ const sep = entry.indexOf(LINK_SEPARATOR);
29
+ if (sep === -1) {
30
+ return { target: entry.trim(), description: undefined };
31
+ }
32
+ return {
33
+ target: entry.slice(0, sep).trim(),
34
+ description: entry.slice(sep + LINK_SEPARATOR.length).trim() || undefined,
35
+ };
36
+ }
37
+
38
+ /** The slug a wikilink's inner text names; empty for `[[|label]]` / `[[#anchor]]`. */
39
+ export function wikilinkTarget(inner: string): string {
40
+ let target = inner;
41
+ const pipe = target.indexOf("|");
42
+ if (pipe !== -1) {
43
+ target = target.slice(0, pipe);
44
+ }
45
+ const hash = target.indexOf("#");
46
+ if (hash !== -1) {
47
+ target = target.slice(0, hash);
48
+ }
49
+ return target.trim();
50
+ }
51
+
52
+ /** Every wikilink target in `text`, in order, empty targets dropped, duplicates kept. */
53
+ export function extractWikilinkTargets(text: string): string[] {
54
+ const targets: string[] = [];
55
+ for (const match of text.matchAll(WIKILINK_REGEX)) {
56
+ const target = wikilinkTarget(match[1]);
57
+ if (target) {
58
+ targets.push(target);
59
+ }
60
+ }
61
+ return targets;
62
+ }
63
+
64
+ export type PageLinkKind = "links" | "wikilink" | "edges";
65
+
66
+ /** A structural reference from page `from` to slug `to` that has no page behind it. */
67
+ export interface DanglingLink {
68
+ from: string;
69
+ to: string;
70
+ kind: PageLinkKind;
71
+ }
72
+
73
+ /** The slice of a `ConceptPage` the finder reads (a parsed page satisfies it). */
74
+ export interface PageLinkSource {
75
+ slug: string;
76
+ frontmatter: {
77
+ links?: readonly string[] | undefined;
78
+ edges?: readonly string[] | undefined;
79
+ };
80
+ /** Frontmatter-stripped body. */
81
+ body: string;
82
+ }
83
+
84
+ /**
85
+ * Structural references on `pages` whose target is not in `knownSlugs`.
86
+ * Only slug-shaped targets (`isSlug`, the page store's rule) are references
87
+ * at all: `[[ -f foo ]]` in a shell snippet is not a link to a page. Self
88
+ * references are ignored, each `(from, to, kind)` is reported once, and the
89
+ * result is sorted by `(from, to, kind)` so renderings are byte-stable.
90
+ */
91
+ export function findDanglingLinks(
92
+ pages: readonly PageLinkSource[],
93
+ knownSlugs: ReadonlySet<string>,
94
+ isSlug: (target: string) => boolean,
95
+ ): DanglingLink[] {
96
+ const seen = new Set<string>();
97
+ const dangling: DanglingLink[] = [];
98
+ const report = (from: string, to: string, kind: PageLinkKind): void => {
99
+ if (to === from || knownSlugs.has(to) || !isSlug(to)) {
100
+ return;
101
+ }
102
+ const key = `${kind} ${from} ${to}`;
103
+ if (seen.has(key)) {
104
+ return;
105
+ }
106
+ seen.add(key);
107
+ dangling.push({ from, to, kind });
108
+ };
109
+
110
+ for (const page of pages) {
111
+ for (const entry of page.frontmatter.links ?? []) {
112
+ report(page.slug, parseLinkEntry(entry).target, "links");
113
+ }
114
+ for (const target of extractWikilinkTargets(page.body)) {
115
+ report(page.slug, target, "wikilink");
116
+ }
117
+ for (const target of page.frontmatter.edges ?? []) {
118
+ report(page.slug, target.trim(), "edges");
119
+ }
120
+ }
121
+
122
+ dangling.sort(
123
+ (a, b) =>
124
+ compareStrings(a.from, b.from) ||
125
+ compareStrings(a.to, b.to) ||
126
+ compareStrings(a.kind, b.kind),
127
+ );
128
+ return dangling;
129
+ }
130
+
131
+ function compareStrings(a: string, b: string): number {
132
+ return a < b ? -1 : a > b ? 1 : 0;
133
+ }
@@ -153,6 +153,16 @@ export function validateSlug(slug: string): void {
153
153
  }
154
154
  }
155
155
 
156
+ /** Boolean form of {@link validateSlug}. */
157
+ export function isValidSlug(slug: string): boolean {
158
+ try {
159
+ validateSlug(slug);
160
+ return true;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
156
166
  // ---------------------------------------------------------------------------
157
167
  // Path helpers
158
168
  // ---------------------------------------------------------------------------