@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

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 (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -1,196 +0,0 @@
1
- /**
2
- * Tests for the silent ACP adapter auto-installer.
3
- *
4
- * `execFile` is stubbed via the shared `installExecFileStub` helper: a
5
- * process-global `mock.module("node:child_process", ...)` driven by per-call
6
- * scripted responses keyed on `${command} ${args[0]}`. The
7
- * `resolveAgentWithAutoInstall` ordering suite additionally stubs the ACP
8
- * config and `Bun.which` so it can flip bun / adapter-binary presence.
9
- */
10
-
11
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
12
-
13
- import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
14
- import { installExecFileStub } from "./__tests__/helpers/exec-file-stub.js";
15
- import { installWhichStub } from "./__tests__/helpers/which-stub.js";
16
-
17
- const { execScripts, execFileMock, reset } = installExecFileStub();
18
- const config = await installAcpConfigStub();
19
- const which = installWhichStub();
20
-
21
- afterAll(() => {
22
- which.restore();
23
- });
24
-
25
- // Spread the real module so other test files that load logger consumers
26
- // (e.g. `truncateForLog` importers) after this process-global mock still
27
- // resolve every named export.
28
- const realLogger = await import("../util/logger.js");
29
- mock.module("../util/logger.js", () => ({
30
- ...realLogger,
31
- getLogger: () =>
32
- new Proxy({} as Record<string, unknown>, {
33
- get: () => () => {},
34
- }),
35
- }));
36
-
37
- const {
38
- ensureAdapterInstalled,
39
- resolveAgentWithAutoInstall,
40
- _resetAdapterInstallCacheForTests,
41
- } = await import("./auto-install.js");
42
-
43
- beforeEach(() => {
44
- reset();
45
- _resetAdapterInstallCacheForTests();
46
- config.setConfig({ agents: {} });
47
- which.setWhich({});
48
- });
49
-
50
- describe("ensureAdapterInstalled", () => {
51
- test("known command: runs `npm i -g <pkg>` and reports installed", async () => {
52
- execScripts.set("npm i", { stdout: "" });
53
-
54
- const result = await ensureAdapterInstalled("claude-agent-acp");
55
-
56
- expect(result).toEqual({ installed: true });
57
- expect(execFileMock).toHaveBeenCalledTimes(1);
58
- const [command, args] = execFileMock.mock.calls[0];
59
- expect(command).toBe("npm");
60
- expect(args).toEqual([
61
- "i",
62
- "-g",
63
- "@agentclientprotocol/claude-agent-acp",
64
- ]);
65
- });
66
-
67
- test("unknown command: never invokes npm (security allowlist)", async () => {
68
- const result = await ensureAdapterInstalled("some-arbitrary-binary");
69
-
70
- expect(result.installed).toBe(false);
71
- expect(result.error).toBeUndefined();
72
- expect(execFileMock).not.toHaveBeenCalled();
73
- });
74
-
75
- test("npm failure: reports the error and does not install", async () => {
76
- execScripts.set("npm i", {
77
- error: new Error("EACCES: permission denied"),
78
- });
79
-
80
- const result = await ensureAdapterInstalled("codex-acp");
81
-
82
- expect(result.installed).toBe(false);
83
- expect(result.error).toContain("EACCES");
84
- });
85
-
86
- test("failed install is retried on the next call", async () => {
87
- execScripts.set("npm i", { error: new Error("network down") });
88
- const first = await ensureAdapterInstalled("claude-agent-acp");
89
- expect(first.installed).toBe(false);
90
-
91
- execScripts.set("npm i", { stdout: "" });
92
- const second = await ensureAdapterInstalled("claude-agent-acp");
93
- expect(second.installed).toBe(true);
94
- expect(execFileMock).toHaveBeenCalledTimes(2);
95
- });
96
-
97
- test("successful install is cached for the process lifetime", async () => {
98
- execScripts.set("npm i", { stdout: "" });
99
-
100
- await ensureAdapterInstalled("claude-agent-acp");
101
- await ensureAdapterInstalled("claude-agent-acp");
102
-
103
- expect(execFileMock).toHaveBeenCalledTimes(1);
104
- });
105
-
106
- test("concurrent calls dedupe to exactly one `npm i -g`", async () => {
107
- execScripts.set("npm i", { stdout: "" });
108
-
109
- const [a, b, c] = await Promise.all([
110
- ensureAdapterInstalled("claude-agent-acp"),
111
- ensureAdapterInstalled("claude-agent-acp"),
112
- ensureAdapterInstalled("claude-agent-acp"),
113
- ]);
114
-
115
- expect(a.installed).toBe(true);
116
- expect(b.installed).toBe(true);
117
- expect(c.installed).toBe(true);
118
- expect(execFileMock).toHaveBeenCalledTimes(1);
119
- });
120
-
121
- test("different commands install independently", async () => {
122
- execScripts.set("npm i", { stdout: "" });
123
-
124
- const [claude, codex] = await Promise.all([
125
- ensureAdapterInstalled("claude-agent-acp"),
126
- ensureAdapterInstalled("codex-acp"),
127
- ]);
128
-
129
- expect(claude.installed).toBe(true);
130
- expect(codex.installed).toBe(true);
131
- expect(execFileMock).toHaveBeenCalledTimes(2);
132
- const installedPackages = execFileMock.mock.calls.map(
133
- (call) => (call[1] as string[])[2],
134
- );
135
- expect(installedPackages.sort()).toEqual([
136
- "@agentclientprotocol/claude-agent-acp",
137
- "@zed-industries/codex-acp",
138
- ]);
139
- });
140
- });
141
-
142
- describe("resolveAgentWithAutoInstall - resolution order", () => {
143
- test("binary missing + bun present: resolves via bunx without invoking npm", async () => {
144
- which.setWhich({ bun: "/usr/local/bin/bun" });
145
-
146
- const result = await resolveAgentWithAutoInstall("claude");
147
-
148
- expect(result.resolved.ok).toBe(true);
149
- if (!result.resolved.ok) return;
150
- expect(result.resolved.agent.command).toBe("bun");
151
- expect(result.resolved.agent.adapterCommand).toBe("claude-agent-acp");
152
- expect(result.autoInstalledPackage).toBeUndefined();
153
- expect(result.failureMessage).toBeUndefined();
154
- expect(execFileMock).not.toHaveBeenCalled();
155
- });
156
-
157
- test("binary + bun missing, npm install succeeds: falls back to the npm flow", async () => {
158
- let installed = false;
159
- which.setWhich((cmd) =>
160
- installed && cmd === "claude-agent-acp"
161
- ? "/usr/local/bin/claude-agent-acp"
162
- : null,
163
- );
164
- execScripts.set("npm i", {
165
- stdout: "",
166
- onCall: () => {
167
- installed = true;
168
- },
169
- });
170
-
171
- const result = await resolveAgentWithAutoInstall("claude");
172
-
173
- expect(result.resolved.ok).toBe(true);
174
- if (!result.resolved.ok) return;
175
- expect(result.resolved.agent.command).toBe("claude-agent-acp");
176
- expect(result.autoInstalledPackage).toBe(
177
- "@agentclientprotocol/claude-agent-acp",
178
- );
179
- expect(execFileMock).toHaveBeenCalledTimes(1);
180
- });
181
-
182
- test("binary, bun, and npm all missing: failure message carries the hint and install error", async () => {
183
- execScripts.set("npm i", { error: new Error("spawn npm ENOENT") });
184
-
185
- const result = await resolveAgentWithAutoInstall("claude");
186
-
187
- expect(result.resolved.ok).toBe(false);
188
- expect(result.failureMessage).toContain(
189
- "claude-agent-acp is not on PATH",
190
- );
191
- expect(result.failureMessage).toContain(
192
- "npm i -g @agentclientprotocol/claude-agent-acp",
193
- );
194
- expect(result.failureMessage).toContain("ENOENT");
195
- });
196
- });
@@ -1,177 +0,0 @@
1
- /**
2
- * Silent auto-install for known ACP adapter binaries.
3
- *
4
- * Fallback path: the resolver (`resolve-agent.ts`) already rewrites missing
5
- * allowlisted adapters to run via `bun x` when `bun` is on PATH, so
6
- * `binary_not_found` only reaches this module when bun is absent (or the
7
- * command has no package mapping). When a spawn does fail preflight with
8
- * `binary_not_found`, callers (the `acp_spawn` tool and the `/v1/acp/spawn`
9
- * route) call `resolveAgentWithAutoInstall(agentId)`, which tries a global
10
- * npm install of the mapped adapter package, then re-resolves and continues.
11
- * On failure they fall back to the existing actionable install hint.
12
- *
13
- * Security boundary: only commands present in `DEFAULT_AGENT_NPM_PACKAGES`
14
- * are ever installed. The package names are vendored constants, NOT user
15
- * input: an arbitrary command from user config must never be turned into
16
- * an `npm i -g <attacker-controlled-name>` execution.
17
- */
18
-
19
- import { execFile } from "node:child_process";
20
-
21
- import { DEFAULT_AGENT_NPM_PACKAGES } from "../config/acp-defaults.js";
22
- import { getLogger } from "../util/logger.js";
23
- import {
24
- resolveAcpAgent,
25
- type ResolveAcpAgentResult,
26
- } from "./resolve-agent.js";
27
-
28
- const log = getLogger("acp:auto-install");
29
-
30
- /** Per-install timeout for `npm i -g`. Generous: cold npm caches are slow. */
31
- const NPM_INSTALL_TIMEOUT_MS = 120_000;
32
-
33
- export interface AdapterInstallResult {
34
- installed: boolean;
35
- error?: string;
36
- }
37
-
38
- /**
39
- * Per-command install promises. Concurrent spawns for the same missing
40
- * binary dedupe to a single `npm i -g`; successful results are cached for
41
- * the process lifetime. Failed installs are evicted so a later spawn can
42
- * retry (e.g. after the user fixes network or npm permissions).
43
- */
44
- const installPromises = new Map<string, Promise<AdapterInstallResult>>();
45
-
46
- /**
47
- * Run `execFile` with an AbortController-driven timeout. Returns the stdout
48
- * on success; throws on error or timeout. Shared by the adapter installer
49
- * here and the version probes in `tools/acp/spawn.ts`.
50
- */
51
- export function execFileWithTimeout(
52
- command: string,
53
- args: string[],
54
- timeoutMs: number,
55
- ): Promise<string> {
56
- return new Promise((resolve, reject) => {
57
- const controller = new AbortController();
58
- const timer = setTimeout(() => controller.abort(), timeoutMs);
59
- execFile(
60
- command,
61
- args,
62
- { signal: controller.signal, encoding: "utf8" },
63
- (err, stdout) => {
64
- clearTimeout(timer);
65
- if (err) {
66
- reject(err);
67
- return;
68
- }
69
- resolve(stdout);
70
- },
71
- );
72
- });
73
- }
74
-
75
- /**
76
- * Install the npm package mapped to `command` if (and only if) the command
77
- * is a known adapter binary. Unknown commands resolve to
78
- * `{ installed: false }` without ever invoking npm (see the security
79
- * boundary note in the module doc).
80
- */
81
- export function ensureAdapterInstalled(
82
- command: string,
83
- ): Promise<AdapterInstallResult> {
84
- const packageName = DEFAULT_AGENT_NPM_PACKAGES[command];
85
- if (!packageName) {
86
- return Promise.resolve({ installed: false });
87
- }
88
-
89
- const inFlight = installPromises.get(command);
90
- if (inFlight) return inFlight;
91
-
92
- const promise = runInstall(command, packageName).then((result) => {
93
- if (!result.installed) installPromises.delete(command);
94
- return result;
95
- });
96
- installPromises.set(command, promise);
97
- return promise;
98
- }
99
-
100
- async function runInstall(
101
- command: string,
102
- packageName: string,
103
- ): Promise<AdapterInstallResult> {
104
- log.info({ command, packageName }, "Auto-installing missing ACP adapter");
105
- try {
106
- await execFileWithTimeout(
107
- "npm",
108
- ["i", "-g", packageName],
109
- NPM_INSTALL_TIMEOUT_MS,
110
- );
111
- log.info({ command, packageName }, "ACP adapter auto-install succeeded");
112
- return { installed: true };
113
- } catch (err) {
114
- const error = err instanceof Error ? err.message : String(err);
115
- log.warn(
116
- { err, command, packageName },
117
- "ACP adapter auto-install failed (falling back to install hint)",
118
- );
119
- return { installed: false, error };
120
- }
121
- }
122
-
123
- export interface ResolveWithAutoInstallResult {
124
- /** The final resolver outcome (post-install re-resolve when applicable). */
125
- resolved: ResolveAcpAgentResult;
126
- /** Set when a missing adapter binary was silently installed via npm. */
127
- autoInstalledPackage?: string;
128
- /**
129
- * Set when the auto-install itself failed: the original install hint
130
- * augmented with the npm failure reason. Callers should surface this
131
- * instead of re-deriving a message from `resolved`.
132
- */
133
- failureMessage?: string;
134
- }
135
-
136
- /**
137
- * Resolve an ACP agent id, silently auto-installing the mapped adapter
138
- * package when (and only when) the failure is a missing allowlisted binary.
139
- * Shared by the `acp_spawn` tool and the `/v1/acp/spawn` route so the
140
- * resolve-install-re-resolve flow has a single implementation; callers map
141
- * the result to their transport (tool error result vs. HTTP error class).
142
- */
143
- export async function resolveAgentWithAutoInstall(
144
- agentId: string,
145
- ): Promise<ResolveWithAutoInstallResult> {
146
- const resolved = resolveAcpAgent(agentId);
147
- if (resolved.ok || resolved.reason !== "binary_not_found") {
148
- return { resolved };
149
- }
150
-
151
- const { command, hint } = resolved;
152
- const install = await ensureAdapterInstalled(command);
153
- if (install.installed) {
154
- const retried = resolveAcpAgent(agentId);
155
- if (retried.ok) {
156
- log.info(
157
- { agentId, command },
158
- "Auto-installed missing ACP adapter binary",
159
- );
160
- return {
161
- resolved: retried,
162
- autoInstalledPackage: DEFAULT_AGENT_NPM_PACKAGES[command],
163
- };
164
- }
165
- } else if (install.error) {
166
- return {
167
- resolved,
168
- failureMessage: `${command} is not on PATH. ${hint} (auto-install failed: ${install.error})`,
169
- };
170
- }
171
- return { resolved };
172
- }
173
-
174
- /** @internal: exposed for tests only. */
175
- export function _resetAdapterInstallCacheForTests(): void {
176
- installPromises.clear();
177
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Tests for the ACP feature gate.
3
- *
4
- * `isAcpEnabled` is an OR of the `acp` feature flag and the legacy
5
- * `config.acp.enabled` field: either switch enables the subsystem, and
6
- * neither implicitly flips the other.
7
- */
8
-
9
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
-
11
- import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
12
- import type { AssistantConfig } from "../config/schema.js";
13
- import { ACP_FLAG_KEY, isAcpEnabled } from "./feature-gate.js";
14
-
15
- beforeEach(() => {
16
- setOverridesForTesting({});
17
- });
18
-
19
- afterEach(() => {
20
- setOverridesForTesting({});
21
- });
22
-
23
- /** Minimal AssistantConfig carrying only the `acp` section the gate reads. */
24
- function makeConfig(acpEnabled: boolean): AssistantConfig {
25
- return {
26
- acp: { enabled: acpEnabled, maxConcurrentSessions: 4, agents: {} },
27
- } as AssistantConfig;
28
- }
29
-
30
- describe("isAcpEnabled", () => {
31
- test("returns false when both the flag and config.acp.enabled are off", () => {
32
- expect(isAcpEnabled(makeConfig(false))).toBe(false);
33
- });
34
-
35
- test("returns true when config.acp.enabled is true and the flag is off", () => {
36
- expect(isAcpEnabled(makeConfig(true))).toBe(true);
37
- });
38
-
39
- test("returns true when the flag is on and config.acp.enabled is false", () => {
40
- setOverridesForTesting({ [ACP_FLAG_KEY]: true });
41
- expect(isAcpEnabled(makeConfig(false))).toBe(true);
42
- });
43
-
44
- test("explicit flag-off override does not defeat config.acp.enabled", () => {
45
- setOverridesForTesting({ [ACP_FLAG_KEY]: false });
46
- expect(isAcpEnabled(makeConfig(true))).toBe(true);
47
- });
48
- });
@@ -1,34 +0,0 @@
1
- /**
2
- * ACP (Agent Client Protocol) feature gate.
3
- *
4
- * Single source of truth for whether the ACP subsystem is enabled. Modeled
5
- * on `credential-execution/feature-gates.ts`: the flag key is declared in
6
- * `meta/feature-flags/feature-flag-registry.json` and resolved through the
7
- * unified feature-flag resolver.
8
- *
9
- * The gate is an OR of two independent switches:
10
- * 1. `config.acp.enabled` — the original workspace config field. Preserved
11
- * so existing workspaces with `acp.enabled: true` keep working without
12
- * any migration.
13
- * 2. The `acp` feature flag — adds a UI toggle. Flag state is persisted by
14
- * the gateway and hot-refreshed in the daemon via the
15
- * `feature_flags_changed` SSE event, so toggling it takes effect
16
- * without a restart or config-file edit.
17
- */
18
-
19
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
20
- import type { AssistantConfig } from "../config/schema.js";
21
-
22
- /** Gate for the ACP coding-agent subsystem (must match the registry). */
23
- export const ACP_FLAG_KEY = "acp" as const;
24
-
25
- /**
26
- * Whether ACP agent spawning/steering is enabled, via either the legacy
27
- * `acp.enabled` config field or the `acp` feature flag (see module doc).
28
- */
29
- export function isAcpEnabled(config: AssistantConfig): boolean {
30
- return (
31
- config.acp?.enabled === true ||
32
- isAssistantFeatureFlagEnabled(ACP_FLAG_KEY, config)
33
- );
34
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Shared `claude --resume` hint for ACP sessions.
3
- *
4
- * `claude --resume <id>` is Claude Code-specific (the claude-agent-acp
5
- * adapter binary). Other adapters resume differently or not at all, so the
6
- * hint is gated by the resolved adapter, not the agent id - this stays
7
- * correct when a user aliases an id to a different binary. Callers pass the
8
- * CANONICAL adapter command (`adapterCommandOf` in `resolve-agent.ts`), not
9
- * the raw spawn command, so the hint still fires when the claude adapter is
10
- * run via `bun x` (spawn command "bun").
11
- *
12
- * Used by the `acp_spawn` tool's success payload and the session manager's
13
- * completion notification so both surfaces render identical copy. Returns
14
- * an empty string for non-claude adapters; callers add their own
15
- * surrounding separators.
16
- */
17
- export function claudeResumeHint(
18
- command: string,
19
- cwd: string,
20
- sessionId: string,
21
- ): string {
22
- return command === "claude-agent-acp"
23
- ? `To resume: cd ${cwd} && claude --resume ${sessionId}`
24
- : "";
25
- }
@@ -1,48 +0,0 @@
1
- # Design System
2
-
3
- A design system CSS is auto-injected inside a `@layer`, so your styles always take priority. It provides element defaults and automatic light/dark mode switching via `prefers-color-scheme`.
4
-
5
- **Use `--v-*` variables and `.v-*` classes** — they handle light/dark mode automatically. No manual dark mode CSS needed.
6
-
7
- ---
8
-
9
- ## Design tokens
10
-
11
- | Category | Tokens |
12
- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | **Backgrounds** | `--v-bg`, `--v-surface`, `--v-surface-border` |
14
- | **Text** | `--v-text`, `--v-text-secondary`, `--v-text-muted` |
15
- | **Accent** | `--v-accent`, `--v-accent-hover` |
16
- | **Status** | `--v-success`, `--v-danger`, `--v-warning` |
17
- | **Spacing** | `--v-spacing-xxs` (2px) / `-xs` (4px) / `-sm` (8px) / `-md` (12px) / `-lg` (16px) / `-xl` (24px) / `-xxl` (32px) / `-xxxl` (48px) |
18
- | **Radius** | `--v-radius-xs` (2px) / `-sm` (4px) / `-md` (8px) / `-lg` (12px) / `-xl` (16px) / `-pill` (999px) |
19
- | **Shadows** | `--v-shadow-sm`, `--v-shadow-md`, `--v-shadow-lg` |
20
- | **Typography** | `--v-font-family`, `--v-font-mono`, `--v-font-size-xs` (10px) / `-sm` (11px) / `-base` (14px) / `-lg` (17px) / `-xl` (22px) / `-2xl` (26px), `--v-line-height` |
21
- | **Animation** | `--v-duration-fast` (0.15s) / `-standard` (0.25s) / `-slow` (0.4s) |
22
- | **Palettes** | `--v-slate-{950..50}`, `--v-emerald-*`, `--v-violet-*`, `--v-indigo-*`, `--v-rose-*`, `--v-amber-*` |
23
- | **Constant** | `--v-aux-white` (always `#FFFFFF` in both modes — use for text on filled/accent backgrounds) |
24
-
25
- ---
26
-
27
- ## Utility classes
28
-
29
- `.v-button` (`.secondary` / `.danger` / `.ghost`), `.v-card`, `.v-list` / `.v-list-item`, `.v-badge` (`.success` / `.warning` / `.danger`), `.v-input-row`, `.v-empty-state`, `.v-toggle`.
30
-
31
- ⚠️ **Never hardcode `color: white` or `color: #fff`.** Use `var(--v-aux-white)` for text on filled/accent backgrounds, or `var(--v-text)` / `var(--v-text-secondary)` for text on surface backgrounds. Hardcoded white causes invisible text on light surfaces.
32
-
33
- ---
34
-
35
- ## Custom themes
36
-
37
- When the user wants a specific branded look, write complete CSS with hardcoded colors and `@media (prefers-color-scheme: dark)` for dark variants. Don't mix `--v-*` auto-switching variables with hardcoded colors in the same element.
38
-
39
- ---
40
-
41
- ## Theme detection in JavaScript
42
-
43
- ```javascript
44
- console.log(window.vellum.theme.mode); // 'light' or 'dark'
45
- window.addEventListener("vellum-theme-change", (e) => {
46
- console.log("Theme:", e.detail.mode);
47
- });
48
- ```
@@ -1,57 +0,0 @@
1
- # Responsive Baseline & Mobile-First Mode
2
-
3
- Every app must work across phone (~360px) to desktop (~1400px+).
4
-
5
- ## Mode selection
6
-
7
- The conversation context's `<turn_context>` block carries an `interface:` field.
8
-
9
- **If `interface: ios`** (or any future mobile-web / android identifier):
10
- → Mobile-first build. Design the narrow viewport first, enhance upward.
11
-
12
- **If `interface: macos` or `web`**:
13
- → Desktop-first build. Design larger composition first; narrow fallback still meets the universal baseline below.
14
-
15
- **If field is absent or ambiguous**:
16
- → Default to desktop-first unless the request implies phone use ("for my iPhone", "a tap-tracker I'll use on the go").
17
-
18
- ---
19
-
20
- ## Universal baseline (every build, regardless of interface)
21
-
22
- ### Viewport & safe areas
23
-
24
- - Set viewport meta: `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`. Never set `user-scalable=no` — it blocks accessibility zoom.
25
- - Pad the root container with `env(safe-area-inset-*)` so content clears the notch: `padding-top: max(var(--v-spacing-lg), env(safe-area-inset-top))`, mirrored for `-bottom` / `-left` / `-right`.
26
- - Use `100dvh` (dynamic viewport height), not `100vh`, for full-height containers.
27
-
28
- ### Form controls
29
-
30
- - `<input>`, `<textarea>`, `<select>` must be `font-size: 16px` or larger, or iOS Safari will zoom on focus and break the layout. This applies to every build.
31
- - Add `inputmode` to text fields with structured input: `numeric` for integers, `decimal` for amounts, `email`, `tel`, `url`.
32
-
33
- ### Touch & hover
34
-
35
- - Interactive elements must be ≥44×44pt. `.v-button` already meets this; for custom controls, set `min-height: 44px` explicitly.
36
- - Gate hover affordances behind `@media (hover: hover)` so they don't stick on touch devices.
37
- - Disable text selection on app chrome with `user-select: none; -webkit-user-select: none`.
38
-
39
- ### Layout fluidity
40
-
41
- - Use fluid widths only. No fixed-pixel layouts. Prefer `%`, `fr`, `minmax`, `clamp()` over `px` on container widths.
42
- - At narrow widths, collapse tables into stacked cards with labels and values arranged vertically.
43
- - Size `vellum.widgets.*` chart containers in `vw` / `%`, not fixed `px`.
44
-
45
- ---
46
-
47
- ## Mobile-first priorities (`interface: ios`)
48
-
49
- - Default body text to `--v-font-size-lg` (17px), not `--v-font-size-base` (14px).
50
- - Bump default vertical rhythm one step (e.g. `--v-spacing-md` → `--v-spacing-lg`).
51
- - One column as the default, not a fallback. Opt into multi-column only above `@media (min-width: 720px)`.
52
- - Bottom-anchor the primary action: `position: sticky; bottom: env(safe-area-inset-bottom)`.
53
- - Replace side modals and popovers with bottom sheets.
54
-
55
- ## Desktop-first priorities (`interface: macos` / `web`)
56
-
57
- Multi-column composition, hover-rich affordances, denser information, side modals, inline primary actions. The universal baseline above is still the floor — narrow view must still work.
@@ -1,38 +0,0 @@
1
- # Presentation Slide Design
2
-
3
- Slides are a different domain from apps. Skip app-specific patterns (contextual headers, search/filter, toast notifications, form validation, custom routes). Slides are static content — build navigation and layouts with custom HTML/CSS.
4
-
5
- ## Key principles
6
-
7
- - One idea per slide — understood in 3 seconds
8
- - Layout variety — 3+ different types per deck, never consecutive same-type
9
- - 8 layout types: Title, Stats, Bullets, Quote, Comparison, Timeline, Visual/Immersive, Closing/CTA
10
- - Bold backgrounds — dark, gradient, or strongly tinted
11
- - Max 6 bullets per slide, max 3 sentences body text
12
- - Never go below 15px for any visible text
13
-
14
- ## Navigation
15
-
16
- Build slide navigation as your own component. Common patterns:
17
- - Keyboard: `ArrowLeft` / `ArrowRight` / `Space` / `Escape`
18
- - Click affordances at left/right edges
19
- - Slide counter pill in a corner (e.g. `3 / 12`)
20
- - Optional progress bar at the top
21
-
22
- ## Layout templates
23
-
24
- - **Title** — Centered headline, optional subtitle, no body. Full-bleed background or gradient.
25
- - **Stats** — One huge number, label below, supporting paragraph optional.
26
- - **Bullets** — Heading + 3–6 short bullets. Avoid wall-of-text.
27
- - **Quote** — Pull quote in large italic type, attribution below, contrasting background.
28
- - **Comparison** — Two columns (before/after, us/them, problem/solution). Visual symmetry matters.
29
- - **Timeline** — Horizontal or vertical sequence with dates and milestones.
30
- - **Visual/Immersive** — Full-bleed image, gradient, or generated graphic with minimal text overlay.
31
- - **Closing/CTA** — Headline + single call to action. Mirror the title slide aesthetic.
32
-
33
- ## What to avoid
34
-
35
- - Generic Keynote / PowerPoint aesthetic (default white background, sans-serif body, bullet lists everywhere)
36
- - Tiny text below 15px — slides are read across rooms
37
- - Same layout type used 3+ times in a row — vary the rhythm
38
- - Body paragraphs longer than 3 sentences — split into multiple slides