@vellumai/assistant 0.8.12 → 0.9.0-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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -41,11 +41,18 @@ function manifestWith(name: string, ref: string): unknown {
41
41
  }
42
42
 
43
43
  /**
44
- * Build a `fetch` that serves the marketplace manifest. `marketplace:
45
- * undefined` answers 404 (empty catalog); `fail: true` rejects with a network
46
- * error. Anything else surfaces a 500 so test bugs are loud.
44
+ * Build a `fetch` that serves the marketplace manifest and the GitHub commit
45
+ * API. `marketplace: undefined` answers 404 (empty catalog); `fail: true`
46
+ * rejects the marketplace request with a network error. `remoteCommitDate`
47
+ * seeds the committer date the GitHub commit endpoint returns (its absence
48
+ * 404s, so the remote timestamp degrades to `null`). Anything else surfaces a
49
+ * 500 so test bugs are loud.
47
50
  */
48
- function makeFetch(opts: { marketplace?: unknown; fail?: boolean }): FetchLike {
51
+ function makeFetch(opts: {
52
+ marketplace?: unknown;
53
+ fail?: boolean;
54
+ remoteCommitDate?: string;
55
+ }): FetchLike {
49
56
  return (async (input: RequestInfo | URL) => {
50
57
  const url = typeof input === "string" ? input : input.toString();
51
58
  if (url.includes("marketplace.json")) {
@@ -55,6 +62,17 @@ function makeFetch(opts: { marketplace?: unknown; fail?: boolean }): FetchLike {
55
62
  }
56
63
  return new Response(JSON.stringify(opts.marketplace), { status: 200 });
57
64
  }
65
+ if (url.includes("api.github.com") && url.includes("/commits/")) {
66
+ if (opts.remoteCommitDate === undefined) {
67
+ return new Response("not found", { status: 404 });
68
+ }
69
+ return new Response(
70
+ JSON.stringify({
71
+ commit: { committer: { date: opts.remoteCommitDate } },
72
+ }),
73
+ { status: 200 },
74
+ );
75
+ }
58
76
  return new Response("unexpected url: " + url, { status: 500 });
59
77
  }) as FetchLike;
60
78
  }
@@ -65,7 +83,11 @@ function installPlugin(
65
83
  name: string,
66
84
  opts: {
67
85
  version?: string;
68
- sidecar?: { commit?: string | null; ref?: string } | null;
86
+ sidecar?: {
87
+ commit?: string | null;
88
+ ref?: string;
89
+ committedAt?: string;
90
+ } | null;
69
91
  /** Embed a content fingerprint of the materialized tree in the sidecar. */
70
92
  fingerprint?: boolean;
71
93
  } = {},
@@ -99,6 +121,7 @@ function installPlugin(
99
121
  ref: sidecar.ref ?? sidecar.commit ?? SHA_A,
100
122
  },
101
123
  commit: sidecar.commit ?? undefined,
124
+ committedAt: sidecar.committedAt,
102
125
  installedAt: "2026-06-10T12:00:00.000Z",
103
126
  fingerprint,
104
127
  }),
@@ -139,6 +162,47 @@ describe("inspectPlugin", () => {
139
162
  expect(result.remote?.license).toBe("MIT");
140
163
  });
141
164
 
165
+ test("surfaces commit timestamps as the human-readable version on both sides", async () => {
166
+ // GIVEN an installed copy with a recorded commit timestamp and a
167
+ // marketplace whose pinned commit GitHub dates a few days later
168
+ installPlugin(workspace, "level-up", {
169
+ sidecar: { commit: SHA_A, committedAt: "2026-06-01T12:34:56.000Z" },
170
+ });
171
+ const fetch = makeFetch({
172
+ marketplace: manifestWith("level-up", SHA_B),
173
+ remoteCommitDate: "2026-06-05T08:12:24.000Z",
174
+ });
175
+
176
+ // WHEN it is inspected
177
+ const result = await inspectPlugin(
178
+ { name: "level-up" },
179
+ { fetch, workspacePluginsDir: workspace },
180
+ );
181
+
182
+ // THEN the installed timestamp comes from the sidecar and the remote one
183
+ // is resolved from GitHub — both as ISO-8601 UTC strings
184
+ expect(result.local?.committedAt).toBe("2026-06-01T12:34:56.000Z");
185
+ expect(result.remote?.committedAt).toBe("2026-06-05T08:12:24.000Z");
186
+ });
187
+
188
+ test("leaves the remote timestamp null when the commit date cannot be fetched", async () => {
189
+ // GIVEN a marketplace entry but a GitHub commit endpoint that 404s
190
+ installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
191
+ const fetch = makeFetch({ marketplace: manifestWith("level-up", SHA_B) });
192
+
193
+ // WHEN it is inspected
194
+ const result = await inspectPlugin(
195
+ { name: "level-up" },
196
+ { fetch, workspacePluginsDir: workspace },
197
+ );
198
+
199
+ // THEN the SHA is still reported while the timestamp degrades to null
200
+ expect(result.remote?.commit).toBe(SHA_B);
201
+ expect(result.remote?.committedAt).toBeNull();
202
+ // AND an older install without a recorded commit date reports null too
203
+ expect(result.local?.committedAt).toBeNull();
204
+ });
205
+
142
206
  test("reports update-available when the marketplace pin has advanced", async () => {
143
207
  // GIVEN an installed plugin whose commit differs from the marketplace pin
144
208
  installPlugin(workspace, "level-up", { sidecar: { commit: SHA_A } });
@@ -146,6 +146,8 @@ function makeContentsFetch(opts: {
146
146
  function fakeGitRunner(opts: {
147
147
  tree: Record<string, string | null>;
148
148
  commit?: string;
149
+ /** UNIX committer seconds reported by `git show -s --format=%ct HEAD`. */
150
+ committedAtSeconds?: number;
149
151
  calls?: string[][];
150
152
  fetchError?: Error;
151
153
  }): GitRunner {
@@ -170,6 +172,13 @@ function fakeGitRunner(opts: {
170
172
  }
171
173
  case "rev-parse":
172
174
  return { stdout: `${commit}\n` };
175
+ case "show":
176
+ return {
177
+ stdout:
178
+ opts.committedAtSeconds === undefined
179
+ ? ""
180
+ : `${opts.committedAtSeconds}\n`,
181
+ };
173
182
  default:
174
183
  return { stdout: "" };
175
184
  }
@@ -338,6 +347,10 @@ describe("installPlugin — marketplace resolution", () => {
338
347
  ".claude-plugin/plugin.json": "{}",
339
348
  },
340
349
  commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
350
+ // 2026-06-05T08:12:24Z as UNIX seconds, so the install captures it.
351
+ committedAtSeconds: Math.floor(
352
+ Date.parse("2026-06-05T08:12:24.000Z") / 1000,
353
+ ),
341
354
  calls,
342
355
  });
343
356
 
@@ -354,6 +367,8 @@ describe("installPlugin — marketplace resolution", () => {
354
367
  expect(result.target).toBe(target);
355
368
  expect(result.ref).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
356
369
  expect(result.commit).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
370
+ // AND the commit's committer date is captured as an ISO-8601 UTC string
371
+ expect(result.committedAt).toBe("2026-06-05T08:12:24.000Z");
357
372
  expect(result.fileCount).toBe(3);
358
373
  expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
359
374
  '{"name":"caveman"}',
@@ -61,6 +61,7 @@ function manifestWith(name: string, ref: string): unknown {
61
61
  function makeFetch(opts: {
62
62
  manifest?: unknown;
63
63
  manifestStatus?: number;
64
+ remoteCommitDate?: string;
64
65
  }): FetchLike {
65
66
  return (async (input: RequestInfo | URL) => {
66
67
  const url = typeof input === "string" ? input : input.toString();
@@ -75,6 +76,19 @@ function makeFetch(opts: {
75
76
  }
76
77
  return new Response(JSON.stringify(opts.manifest), { status: 200 });
77
78
  }
79
+ // The remote commit date `inspect` resolves to surface the human-readable
80
+ // "to" timestamp; absent it 404s and the timestamp degrades to null.
81
+ if (url.includes("api.github.com") && url.includes("/commits/")) {
82
+ if (opts.remoteCommitDate === undefined) {
83
+ return new Response("not found", { status: 404 });
84
+ }
85
+ return new Response(
86
+ JSON.stringify({
87
+ commit: { committer: { date: opts.remoteCommitDate } },
88
+ }),
89
+ { status: 200 },
90
+ );
91
+ }
78
92
  // No adapter stub: the Contents API listing for plugins/<name> is empty.
79
93
  if (url.startsWith(CONTENTS)) {
80
94
  return new Response("not found", { status: 404 });
@@ -83,10 +97,17 @@ function makeFetch(opts: {
83
97
  }) as FetchLike;
84
98
  }
85
99
 
86
- /** A fake clone that materializes one file and reports `commit` at HEAD. */
87
- function fakeGitRunner(commit: string, calls?: string[][]): GitRunner {
100
+ /**
101
+ * A fake clone that materializes one file and reports `commit` at HEAD. When
102
+ * `committedAtSeconds` is given, `git show -s --format=%ct HEAD` reports that
103
+ * UNIX committer time, mirroring how install captures the commit timestamp.
104
+ */
105
+ function fakeGitRunner(
106
+ commit: string,
107
+ opts: { calls?: string[][]; committedAtSeconds?: number } = {},
108
+ ): GitRunner {
88
109
  return async (args, { cwd }) => {
89
- calls?.push([...args]);
110
+ opts.calls?.push([...args]);
90
111
  switch (args[0]) {
91
112
  case "fetch": {
92
113
  mkdirSync(join(cwd, ".git"), { recursive: true });
@@ -96,6 +117,13 @@ function fakeGitRunner(commit: string, calls?: string[][]): GitRunner {
96
117
  }
97
118
  case "rev-parse":
98
119
  return { stdout: `${commit}\n` };
120
+ case "show":
121
+ return {
122
+ stdout:
123
+ opts.committedAtSeconds === undefined
124
+ ? ""
125
+ : `${opts.committedAtSeconds}\n`,
126
+ };
99
127
  default:
100
128
  return { stdout: "" };
101
129
  }
@@ -111,7 +139,7 @@ const unusedGitRunner: GitRunner = async (args) => {
111
139
  function installCopy(
112
140
  pluginsDir: string,
113
141
  name: string,
114
- sidecar: { commit: string } | null,
142
+ sidecar: { commit: string; committedAt?: string } | null,
115
143
  ): void {
116
144
  const dir = join(pluginsDir, name);
117
145
  mkdirSync(dir, { recursive: true });
@@ -132,6 +160,7 @@ function installCopy(
132
160
  ref: sidecar.commit,
133
161
  },
134
162
  commit: sidecar.commit,
163
+ committedAt: sidecar.committedAt,
135
164
  installedAt: "2026-06-10T12:00:00.000Z",
136
165
  }),
137
166
  );
@@ -181,6 +210,54 @@ describe("upgradePlugin", () => {
181
210
  expect(sidecarCommit(pluginsDir, "level-up")).toBe(SHA_B);
182
211
  });
183
212
 
213
+ test("threads commit timestamps through the move as the human-readable version", async () => {
214
+ // GIVEN an installed copy with a recorded commit timestamp
215
+ installCopy(pluginsDir, "level-up", {
216
+ commit: SHA_A,
217
+ committedAt: "2026-06-01T12:34:56.000Z",
218
+ });
219
+ // AND the marketplace pins a newer commit whose date the clone reports
220
+ const fetch = makeFetch({ manifest: manifestWith("level-up", SHA_B) });
221
+ // 2026-06-05T08:12:24Z expressed as UNIX seconds for `git show %ct`.
222
+ const runGit = fakeGitRunner(SHA_B, {
223
+ committedAtSeconds: Math.floor(
224
+ Date.parse("2026-06-05T08:12:24.000Z") / 1000,
225
+ ),
226
+ });
227
+
228
+ // WHEN the plugin is upgraded
229
+ const result = await upgradePlugin(
230
+ { name: "level-up" },
231
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
232
+ );
233
+
234
+ // THEN both ends of the move carry their commit timestamps
235
+ expect(result.fromTimestamp).toBe("2026-06-01T12:34:56.000Z");
236
+ expect(result.toTimestamp).toBe("2026-06-05T08:12:24.000Z");
237
+ });
238
+
239
+ test("a dry run resolves the target timestamp from the marketplace pin", async () => {
240
+ // GIVEN an installed copy and a marketplace pin whose commit GitHub dates
241
+ installCopy(pluginsDir, "level-up", {
242
+ commit: SHA_A,
243
+ committedAt: "2026-06-01T12:34:56.000Z",
244
+ });
245
+ const fetch = makeFetch({
246
+ manifest: manifestWith("level-up", SHA_B),
247
+ remoteCommitDate: "2026-06-05T08:12:24.000Z",
248
+ });
249
+
250
+ // WHEN a dry-run upgrade is performed (git must never run)
251
+ const result = await upgradePlugin(
252
+ { name: "level-up", dryRun: true },
253
+ { fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
254
+ );
255
+
256
+ // THEN the preview shows the timestamp move resolved without a clone
257
+ expect(result.fromTimestamp).toBe("2026-06-01T12:34:56.000Z");
258
+ expect(result.toTimestamp).toBe("2026-06-05T08:12:24.000Z");
259
+ });
260
+
184
261
  test("is a no-op when the installed commit already equals the pin", async () => {
185
262
  // GIVEN an installed copy already pinned to SHA_A
186
263
  installCopy(pluginsDir, "level-up", { commit: SHA_A });
@@ -70,6 +70,13 @@ export interface PluginLocalInfo {
70
70
  readonly target: string;
71
71
  /** Resolved commit the copy was installed at; `null` when no provenance was recorded. */
72
72
  readonly commit: string | null;
73
+ /**
74
+ * ISO-8601 committer timestamp of {@link PluginLocalInfo.commit} (UTC), the
75
+ * human-readable version `plugins inspect` shows. `null` for older installs
76
+ * written before commit timestamps were recorded. Distinct from
77
+ * {@link PluginLocalInfo.installedAt} (when this machine ran the install).
78
+ */
79
+ readonly committedAt: string | null;
73
80
  /** `package.json` `version`, when present. */
74
81
  readonly version: string | null;
75
82
  /** `package.json` `description`, when present. */
@@ -96,6 +103,12 @@ export interface PluginRemoteInfo {
96
103
  readonly path: string;
97
104
  /** Pinned commit SHA the marketplace currently resolves installs to. */
98
105
  readonly commit: string;
106
+ /**
107
+ * ISO-8601 committer timestamp of {@link PluginRemoteInfo.commit} (UTC),
108
+ * resolved from GitHub. `null` when the commit metadata could not be fetched
109
+ * (network / rate-limit); the SHA is still reported.
110
+ */
111
+ readonly committedAt: string | null;
99
112
  readonly description: string | null;
100
113
  readonly homepage: string | null;
101
114
  readonly license: string | null;
@@ -167,6 +180,7 @@ function readLocal(
167
180
  return {
168
181
  target: entry.target,
169
182
  commit,
183
+ committedAt: manifest?.committedAt ?? null,
170
184
  version: entry.packageJson?.version ?? null,
171
185
  description: entry.packageJson?.description ?? null,
172
186
  installedAt: manifest?.installedAt || null,
@@ -179,11 +193,13 @@ function readLocal(
179
193
  function readRemote(
180
194
  entry: MarketplaceEntry,
181
195
  marketplaceRef: string,
196
+ committedAt: string | null,
182
197
  ): PluginRemoteInfo {
183
198
  return {
184
199
  repo: entry.source.repo,
185
200
  path: entry.source.path ?? "",
186
201
  commit: entry.source.ref,
202
+ committedAt,
187
203
  description: entry.description ?? null,
188
204
  homepage: entry.homepage ?? null,
189
205
  license: entry.license ?? null,
@@ -192,6 +208,44 @@ function readRemote(
192
208
  };
193
209
  }
194
210
 
211
+ /**
212
+ * Resolve the committer date of a pinned commit from GitHub, normalized to a
213
+ * UTC ISO-8601 string so it is directly comparable with the install-time
214
+ * {@link PluginLocalInfo.committedAt}.
215
+ *
216
+ * Best-effort: any failure (network, rate-limit, unexpected shape) yields
217
+ * `null` so inspection still reports the remote pin's SHA without its date,
218
+ * mirroring how the local side degrades to `unknown` when no date was recorded.
219
+ */
220
+ async function fetchCommitDate(
221
+ repo: string,
222
+ sha: string,
223
+ fetch: FetchLike,
224
+ ): Promise<string | null> {
225
+ const url = `https://api.github.com/repos/${repo}/commits/${encodeURIComponent(sha)}`;
226
+ try {
227
+ const res = await fetch(url, {
228
+ headers: {
229
+ Accept: "application/vnd.github+json",
230
+ "User-Agent": "vellum-assistant-cli",
231
+ },
232
+ });
233
+ if (!res.ok) return null;
234
+ const json: unknown = JSON.parse(await res.text());
235
+ if (typeof json !== "object" || json === null) return null;
236
+ const commit = (json as Record<string, unknown>).commit;
237
+ if (typeof commit !== "object" || commit === null) return null;
238
+ const committer = (commit as Record<string, unknown>).committer;
239
+ if (typeof committer !== "object" || committer === null) return null;
240
+ const date = (committer as Record<string, unknown>).date;
241
+ if (typeof date !== "string") return null;
242
+ const ms = Date.parse(date);
243
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
195
249
  /**
196
250
  * Resolve the local-vs-remote inspection for a single plugin.
197
251
  *
@@ -221,7 +275,14 @@ export async function inspectPlugin(
221
275
  { ref: marketplaceRef },
222
276
  );
223
277
  const match = entries.find((e) => e.name === name);
224
- if (match) remote = readRemote(match, marketplaceRef);
278
+ if (match) {
279
+ const committedAt = await fetchCommitDate(
280
+ match.source.repo,
281
+ match.source.ref,
282
+ deps.fetch,
283
+ );
284
+ remote = readRemote(match, marketplaceRef, committedAt);
285
+ }
225
286
  } catch (err) {
226
287
  remoteError = err instanceof Error ? err.message : String(err);
227
288
  }
@@ -135,6 +135,11 @@ export interface InstallPluginResult {
135
135
  readonly ref: string;
136
136
  /** Resolved commit SHA the external source was cloned at; null when it could not be read. */
137
137
  readonly commit: string | null;
138
+ /**
139
+ * ISO-8601 committer timestamp of {@link InstallPluginResult.commit} (UTC);
140
+ * null when the commit or its date could not be read.
141
+ */
142
+ readonly committedAt: string | null;
138
143
  }
139
144
 
140
145
  /** Plugin name failed sanitization. */
@@ -369,6 +374,7 @@ export async function installPlugin(
369
374
 
370
375
  let fileCount: number;
371
376
  let commit: string | null = null;
377
+ let committedAt: string | null = null;
372
378
  try {
373
379
  const cloned = await copyExternalViaGit(
374
380
  source,
@@ -377,6 +383,7 @@ export async function installPlugin(
377
383
  );
378
384
  fileCount = cloned.fileCount;
379
385
  commit = cloned.commit;
386
+ committedAt = cloned.committedAt;
380
387
  // An external clone is often a foreign-ecosystem plugin (e.g. a Claude
381
388
  // Code plugin) that the Vellum loader can't run as-is. When we curate an
382
389
  // adapter stub for it, overlay the stub and run its transform so the
@@ -411,6 +418,7 @@ export async function installPlugin(
411
418
  source,
412
419
  ref,
413
420
  commit,
421
+ committedAt,
414
422
  fingerprint,
415
423
  contentHash,
416
424
  });
@@ -426,7 +434,7 @@ export async function installPlugin(
426
434
  }
427
435
  renameSync(stagingDir, target);
428
436
 
429
- return { name, target, fileCount, ref, commit };
437
+ return { name, target, fileCount, ref, commit, committedAt };
430
438
  }
431
439
 
432
440
  /** Cap on any single git invocation; a shallow fetch is well under this. */
@@ -497,6 +505,16 @@ export interface InstallMeta {
497
505
  readonly source: InstallMetaSource;
498
506
  /** Resolved commit SHA the source was cloned at; `null` when it could not be read at install time. */
499
507
  readonly commit: string | null;
508
+ /**
509
+ * ISO-8601 committer timestamp of {@link InstallMeta.commit}, in UTC
510
+ * (e.g. `2026-06-01T12:34:56.000Z`). This is a property of the commit
511
+ * itself, distinct from {@link InstallMeta.installedAt} (when the local
512
+ * machine ran the install). `null` for older installs written before commit
513
+ * timestamps were recorded, or when the date could not be read. Reserved for
514
+ * the human-readable "version" surfaced by `plugins inspect`; the optional
515
+ * {@link InstallMeta.version} field stays free for future tag/semver support.
516
+ */
517
+ readonly committedAt?: string | null;
500
518
  /**
501
519
  * Per-file content digest of the materialized tree, captured at install
502
520
  * time. `null` for older installs written before fingerprinting; callers
@@ -525,7 +543,11 @@ async function copyExternalViaGit(
525
543
  source: PluginFetchSource,
526
544
  destDir: string,
527
545
  runGit: GitRunner,
528
- ): Promise<{ fileCount: number; commit: string | null }> {
546
+ ): Promise<{
547
+ fileCount: number;
548
+ commit: string | null;
549
+ committedAt: string | null;
550
+ }> {
529
551
  const cloneDir = `${destDir}.gitclone`;
530
552
  rmSync(cloneDir, { recursive: true, force: true });
531
553
  mkdirSync(cloneDir, { recursive: true });
@@ -543,7 +565,8 @@ async function copyExternalViaGit(
543
565
  // A missing repo/ref (or a private one we can't reach) is a hard
544
566
  // not-found, surfaced as zero files. Anything else — network loss, a
545
567
  // transient GitHub outage — is retryable, so map it to a 503.
546
- if (isGitRefNotFound(err)) return { fileCount: 0, commit: null };
568
+ if (isGitRefNotFound(err))
569
+ return { fileCount: 0, commit: null, committedAt: null };
547
570
  throw new PluginSourceUnavailableError(
548
571
  `git clone failed for ${sourceLabel(source)} @ ${source.ref}: ${subprocessErrorText(err)}`,
549
572
  503,
@@ -553,6 +576,7 @@ async function copyExternalViaGit(
553
576
  await runGit(["checkout", "--quiet", "FETCH_HEAD"], { cwd: cloneDir });
554
577
 
555
578
  let commit: string | null = null;
579
+ let committedAt: string | null = null;
556
580
  try {
557
581
  const { stdout } = await runGit(["rev-parse", "HEAD"], { cwd: cloneDir });
558
582
  commit = stdout.trim() || null;
@@ -560,6 +584,24 @@ async function copyExternalViaGit(
560
584
  // Provenance is best-effort; a missing commit must not fail the install.
561
585
  commit = null;
562
586
  }
587
+ if (commit) {
588
+ // The committer date (`%ct`, UNIX seconds) is the version timestamp
589
+ // `plugins inspect` shows. Normalize to a UTC ISO-8601 string so installs
590
+ // made in different local timezones remain directly comparable.
591
+ try {
592
+ const { stdout } = await runGit(
593
+ ["show", "-s", "--format=%ct", "HEAD"],
594
+ { cwd: cloneDir },
595
+ );
596
+ const seconds = Number.parseInt(stdout.trim(), 10);
597
+ if (Number.isFinite(seconds)) {
598
+ committedAt = new Date(seconds * 1000).toISOString();
599
+ }
600
+ } catch {
601
+ // Best-effort: a missing date must not fail the install.
602
+ committedAt = null;
603
+ }
604
+ }
563
605
 
564
606
  // Defense in depth: external marketplace refs are full commit SHAs (the
565
607
  // manifest schema rejects mutable tags/branches), so the checked-out
@@ -577,11 +619,11 @@ async function copyExternalViaGit(
577
619
  ? join(cloneDir, source.rootPath)
578
620
  : cloneDir;
579
621
  if (!existsSync(srcRoot) || !statSync(srcRoot).isDirectory()) {
580
- return { fileCount: 0, commit };
622
+ return { fileCount: 0, commit, committedAt };
581
623
  }
582
624
 
583
625
  const fileCount = copyTreeSkippingGit(srcRoot, destDir);
584
- return { fileCount, commit };
626
+ return { fileCount, commit, committedAt };
585
627
  } finally {
586
628
  rmSync(cloneDir, { recursive: true, force: true });
587
629
  }
@@ -948,6 +990,8 @@ interface WriteInstallMetaParams {
948
990
  readonly source: PluginFetchSource;
949
991
  readonly ref: string;
950
992
  readonly commit: string | null;
993
+ /** ISO-8601 committer timestamp of {@link WriteInstallMetaParams.commit} (UTC); null when unknown. */
994
+ readonly committedAt: string | null;
951
995
  readonly fingerprint: Fingerprint;
952
996
  readonly contentHash: string;
953
997
  }
@@ -985,6 +1029,7 @@ function writeInstallMeta(
985
1029
  source,
986
1030
  ref,
987
1031
  commit,
1032
+ committedAt,
988
1033
  fingerprint,
989
1034
  contentHash,
990
1035
  }: WriteInstallMetaParams,
@@ -1004,6 +1049,7 @@ function writeInstallMeta(
1004
1049
  ref,
1005
1050
  },
1006
1051
  commit,
1052
+ committedAt,
1007
1053
  fingerprint,
1008
1054
  };
1009
1055
  writeFileSync(
@@ -1072,6 +1118,7 @@ export function readInstallMeta(pluginDir: string): InstallMeta | null {
1072
1118
  ref: source.ref,
1073
1119
  },
1074
1120
  commit: typeof obj.commit === "string" ? obj.commit : null,
1121
+ committedAt: typeof obj.committedAt === "string" ? obj.committedAt : null,
1075
1122
  fingerprint: parseFingerprint(obj.fingerprint),
1076
1123
  };
1077
1124
  }
@@ -81,8 +81,18 @@ export interface PluginUpgradeResult {
81
81
  readonly outcome: PluginUpgradeOutcome;
82
82
  /** Installed commit before the upgrade; `null` when no provenance was recorded. */
83
83
  readonly fromCommit: string | null;
84
+ /**
85
+ * ISO-8601 committer timestamp (UTC) of {@link PluginUpgradeResult.fromCommit},
86
+ * the human-readable version moved from; `null` when it was not recorded.
87
+ */
88
+ readonly fromTimestamp: string | null;
84
89
  /** Marketplace-pinned commit the install was (or would be) moved to. */
85
90
  readonly toCommit: string;
91
+ /**
92
+ * ISO-8601 committer timestamp (UTC) of {@link PluginUpgradeResult.toCommit},
93
+ * the human-readable version moved to; `null` when it could not be resolved.
94
+ */
95
+ readonly toTimestamp: string | null;
86
96
  /** Absolute path to the installed plugin directory. */
87
97
  readonly target: string;
88
98
  /** Files materialized by the upgrade; `null` for a no-op or dry run. */
@@ -173,7 +183,9 @@ export async function upgradePlugin(
173
183
  }
174
184
 
175
185
  const fromCommit = local.commit;
186
+ const fromTimestamp = local.committedAt;
176
187
  const toCommit = remote.commit;
188
+ const toTimestamp = remote.committedAt;
177
189
  const provenanceWasUnknown = inspection.status === "unknown-provenance";
178
190
 
179
191
  if (inspection.status === "up-to-date") {
@@ -181,7 +193,9 @@ export async function upgradePlugin(
181
193
  name,
182
194
  outcome: "already-up-to-date",
183
195
  fromCommit,
196
+ fromTimestamp,
184
197
  toCommit,
198
+ toTimestamp,
185
199
  target: local.target,
186
200
  fileCount: null,
187
201
  dryRun,
@@ -194,7 +208,9 @@ export async function upgradePlugin(
194
208
  name,
195
209
  outcome: "would-upgrade",
196
210
  fromCommit,
211
+ fromTimestamp,
197
212
  toCommit,
213
+ toTimestamp,
198
214
  target: local.target,
199
215
  fileCount: null,
200
216
  dryRun: true,
@@ -216,7 +232,9 @@ export async function upgradePlugin(
216
232
  name,
217
233
  outcome: "upgraded",
218
234
  fromCommit,
235
+ fromTimestamp,
219
236
  toCommit: result.commit ?? toCommit,
237
+ toTimestamp: result.committedAt ?? toTimestamp,
220
238
  target: result.target,
221
239
  fileCount: result.fileCount,
222
240
  dryRun: false,
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { AssistantConfigSchema } from "../schema.js";
4
+ import { WorkflowsConfigSchema } from "../schemas/workflows.js";
5
+
6
+ describe("WorkflowsConfigSchema", () => {
7
+ test("empty object parses to full defaults", () => {
8
+ const parsed = WorkflowsConfigSchema.parse({});
9
+ expect(parsed).toEqual({
10
+ maxAgentsPerRun: 500,
11
+ maxConcurrentLeaves: 6,
12
+ maxConcurrentRuns: 3,
13
+ journalRetentionDays: 30,
14
+ });
15
+ });
16
+
17
+ test("custom values round-trip", () => {
18
+ const parsed = WorkflowsConfigSchema.parse({
19
+ maxAgentsPerRun: 100,
20
+ maxConcurrentLeaves: 12,
21
+ maxConcurrentRuns: 1,
22
+ journalRetentionDays: 7,
23
+ });
24
+ expect(parsed).toEqual({
25
+ maxAgentsPerRun: 100,
26
+ maxConcurrentLeaves: 12,
27
+ maxConcurrentRuns: 1,
28
+ journalRetentionDays: 7,
29
+ });
30
+ });
31
+
32
+ test("rejects non-positive and non-integer values", () => {
33
+ expect(
34
+ WorkflowsConfigSchema.safeParse({ maxAgentsPerRun: 0 }).success,
35
+ ).toBe(false);
36
+ expect(
37
+ WorkflowsConfigSchema.safeParse({ maxConcurrentLeaves: -1 }).success,
38
+ ).toBe(false);
39
+ expect(
40
+ WorkflowsConfigSchema.safeParse({ journalRetentionDays: 1.5 }).success,
41
+ ).toBe(false);
42
+ });
43
+
44
+ test("root config resolves workflow defaults when section is absent", () => {
45
+ const config = AssistantConfigSchema.parse({});
46
+ expect(config.workflows.maxAgentsPerRun).toBe(500);
47
+ expect(config.workflows.maxConcurrentLeaves).toBe(6);
48
+ expect(config.workflows.maxConcurrentRuns).toBe(3);
49
+ expect(config.workflows.journalRetentionDays).toBe(30);
50
+ });
51
+
52
+ test("root config applies workflow overrides", () => {
53
+ const config = AssistantConfigSchema.parse({
54
+ workflows: { maxConcurrentLeaves: 10 },
55
+ });
56
+ expect(config.workflows.maxConcurrentLeaves).toBe(10);
57
+ // Unspecified leaves still fall back to defaults.
58
+ expect(config.workflows.maxAgentsPerRun).toBe(500);
59
+ });
60
+ });