@vellumai/assistant 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +29 -28
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/knip.json +1 -0
  6. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  7. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  8. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  9. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  11. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  12. package/openapi.yaml +22 -4
  13. package/package.json +3 -1
  14. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  15. package/src/__tests__/approval-cascade.test.ts +8 -16
  16. package/src/__tests__/approval-routes-http.test.ts +6 -0
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  18. package/src/__tests__/call-constants.test.ts +10 -1
  19. package/src/__tests__/call-controller.test.ts +127 -0
  20. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  21. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  22. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  23. package/src/__tests__/context-search-pkb-source.test.ts +12 -6
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  27. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  28. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  29. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -6
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  32. package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
  33. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  34. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  37. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  38. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  39. package/src/__tests__/filing-service.test.ts +2 -19
  40. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  41. package/src/__tests__/injector-chain.test.ts +24 -16
  42. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  43. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  44. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  45. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  46. package/src/__tests__/oauth-cli.test.ts +121 -0
  47. package/src/__tests__/relay-server.test.ts +46 -2
  48. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  49. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  50. package/src/__tests__/secret-response-routing.test.ts +7 -5
  51. package/src/__tests__/server-history-render.test.ts +82 -0
  52. package/src/__tests__/skill-include-graph.test.ts +31 -0
  53. package/src/__tests__/skill-load-tool.test.ts +44 -16
  54. package/src/__tests__/skills.test.ts +39 -0
  55. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  56. package/src/__tests__/tool-executor.test.ts +155 -0
  57. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  58. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  59. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  60. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  61. package/src/agent/loop.ts +11 -0
  62. package/src/approvals/guardian-decision-primitive.ts +0 -13
  63. package/src/approvals/guardian-request-resolvers.ts +4 -32
  64. package/src/calls/call-constants.ts +5 -8
  65. package/src/calls/call-controller.ts +130 -67
  66. package/src/calls/relay-server.ts +7 -1
  67. package/src/calls/voice-session-bridge.ts +1 -1
  68. package/src/cli/commands/memory-v2.ts +7 -7
  69. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
  70. package/src/cli/commands/oauth/connect.ts +10 -52
  71. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  72. package/src/config/feature-flag-registry.json +1 -17
  73. package/src/config/loader.ts +72 -19
  74. package/src/config/schemas/memory-v2.ts +1 -1
  75. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  76. package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
  77. package/src/daemon/conversation-agent-loop.ts +13 -10
  78. package/src/daemon/conversation-lifecycle.ts +22 -8
  79. package/src/daemon/conversation-surfaces.ts +16 -14
  80. package/src/daemon/conversation-tool-setup.ts +9 -5
  81. package/src/daemon/conversation.ts +1 -1
  82. package/src/daemon/handlers/shared.ts +26 -0
  83. package/src/daemon/host-bash-proxy.ts +1 -1
  84. package/src/daemon/host-browser-proxy.ts +1 -1
  85. package/src/daemon/host-cu-proxy.ts +1 -1
  86. package/src/daemon/host-file-proxy.ts +1 -1
  87. package/src/daemon/host-transfer-proxy.ts +2 -2
  88. package/src/daemon/lifecycle.ts +88 -73
  89. package/src/daemon/memory-v2-startup.ts +55 -14
  90. package/src/daemon/message-types/messages.ts +19 -1
  91. package/src/documents/document-store.ts +35 -1
  92. package/src/filing/filing-service.ts +2 -3
  93. package/src/heartbeat/heartbeat-service.ts +1 -1
  94. package/src/ipc/assistant-server.ts +93 -36
  95. package/src/ipc/skill-server.ts +99 -42
  96. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  97. package/src/memory/context-search/sources/memory-v2.ts +1 -17
  98. package/src/memory/context-search/sources/memory.ts +2 -2
  99. package/src/memory/context-search/sources/pkb.ts +2 -3
  100. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  101. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  102. package/src/memory/graph/conversation-graph-memory.ts +32 -9
  103. package/src/memory/graph/graph-search.test.ts +6 -5
  104. package/src/memory/graph/graph-search.ts +3 -4
  105. package/src/memory/graph/retriever.test.ts +12 -7
  106. package/src/memory/graph/retriever.ts +4 -5
  107. package/src/memory/graph/tool-handlers.ts +3 -4
  108. package/src/memory/graph/tools.ts +4 -4
  109. package/src/memory/indexer.ts +1 -2
  110. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  111. package/src/memory/jobs/embed-concept-page.ts +223 -87
  112. package/src/memory/jobs-worker.ts +8 -4
  113. package/src/memory/pkb/pkb-search.test.ts +6 -5
  114. package/src/memory/pkb/pkb-search.ts +4 -5
  115. package/src/memory/qdrant-client.ts +3 -0
  116. package/src/memory/search/semantic.ts +4 -5
  117. package/src/memory/v2/__tests__/activation.test.ts +35 -5
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
  119. package/src/memory/v2/__tests__/injection.test.ts +140 -23
  120. package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
  121. package/src/memory/v2/__tests__/sim.test.ts +118 -7
  122. package/src/memory/v2/__tests__/static-context.test.ts +1 -13
  123. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  124. package/src/memory/v2/consolidation-job.ts +7 -8
  125. package/src/memory/v2/injection.ts +32 -12
  126. package/src/memory/v2/page-store.ts +39 -0
  127. package/src/memory/v2/prompts/consolidation.ts +5 -0
  128. package/src/memory/v2/qdrant.ts +209 -48
  129. package/src/memory/v2/sim.ts +67 -26
  130. package/src/memory/v2/static-context.ts +4 -8
  131. package/src/memory/v2/sweep-job.ts +5 -6
  132. package/src/memory/v2/types.ts +7 -0
  133. package/src/notifications/copy-composer.ts +46 -12
  134. package/src/notifications/decision-engine.ts +46 -0
  135. package/src/permissions/gateway-threshold-reader.ts +116 -8
  136. package/src/permissions/prompter.ts +86 -96
  137. package/src/permissions/secret-prompter.ts +31 -31
  138. package/src/plugins/defaults/injectors.ts +1 -2
  139. package/src/proactive-artifact/job.test.ts +51 -4
  140. package/src/proactive-artifact/job.ts +16 -2
  141. package/src/proactive-artifact/message-copy.ts +18 -1
  142. package/src/prompts/templates/SOUL.md +13 -28
  143. package/src/runtime/auth/route-policy.ts +1 -0
  144. package/src/runtime/channel-approvals.ts +3 -2
  145. package/src/runtime/guardian-reply-router.ts +0 -10
  146. package/src/runtime/pending-interactions.ts +19 -15
  147. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  148. package/src/runtime/routes/approval-routes.ts +7 -3
  149. package/src/runtime/routes/consolidation-routes.ts +8 -9
  150. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  151. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  152. package/src/runtime/routes/filing-routes.ts +2 -3
  153. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
  154. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  155. package/src/runtime/routes/memory-item-routes.ts +5 -6
  156. package/src/runtime/routes/memory-v2-routes.ts +103 -17
  157. package/src/skills/include-graph.ts +35 -13
  158. package/src/tools/document/document-tool.ts +20 -0
  159. package/src/tools/executor.ts +18 -2
  160. package/src/tools/memory/register.test.ts +7 -5
  161. package/src/tools/permission-checker.ts +15 -0
  162. package/src/tools/skills/load.ts +24 -20
  163. package/src/tools/tool-name-aliases.ts +19 -0
  164. package/src/tools/types.ts +19 -1
  165. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  166. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  167. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  168. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  169. package/src/workspace/migrations/registry.ts +6 -0
@@ -61,14 +61,10 @@ type IncludeValidationResult =
61
61
  | IncludeValidationError
62
62
  | IncludeValidationCycleError;
63
63
 
64
- /**
65
- * Validate the include graph starting from the given root skill ID.
66
- * Uses three-state DFS (unseen/visiting/done) to detect both missing children
67
- * and cycles. Returns the first error encountered in DFS order.
68
- */
69
- export function validateIncludes(
64
+ function validateIncludeGraph(
70
65
  rootId: string,
71
66
  catalogIndex: Map<string, SkillSummary>,
67
+ options: { failOnMissing: boolean },
72
68
  ): IncludeValidationResult {
73
69
  const visited: string[] = [];
74
70
  type State = "unseen" | "visiting" | "done";
@@ -97,13 +93,16 @@ export function validateIncludes(
97
93
  if (skill?.includes) {
98
94
  for (const childId of skill.includes) {
99
95
  if (!catalogIndex.has(childId)) {
100
- return {
101
- ok: false,
102
- error: "missing",
103
- missingChildId: childId,
104
- parentId: id,
105
- path: [...ancestry],
106
- };
96
+ if (options.failOnMissing) {
97
+ return {
98
+ ok: false,
99
+ error: "missing",
100
+ missingChildId: childId,
101
+ parentId: id,
102
+ path: [...ancestry],
103
+ };
104
+ }
105
+ continue;
107
106
  }
108
107
  const childError = dfs(childId);
109
108
  if (childError) return childError;
@@ -120,6 +119,29 @@ export function validateIncludes(
120
119
  return { ok: true, visited };
121
120
  }
122
121
 
122
+ /**
123
+ * Validate the include graph starting from the given root skill ID.
124
+ * Uses three-state DFS (unseen/visiting/done) to detect both missing children
125
+ * and cycles. Returns the first error encountered in DFS order.
126
+ */
127
+ export function validateIncludes(
128
+ rootId: string,
129
+ catalogIndex: Map<string, SkillSummary>,
130
+ ): IncludeValidationResult {
131
+ return validateIncludeGraph(rootId, catalogIndex, { failOnMissing: true });
132
+ }
133
+
134
+ /**
135
+ * Validate only cycle safety for a graph that may intentionally have missing
136
+ * advisory includes. Missing child IDs are skipped during traversal.
137
+ */
138
+ export function validateIncludeCycles(
139
+ rootId: string,
140
+ catalogIndex: Map<string, SkillSummary>,
141
+ ): IncludeValidationResult {
142
+ return validateIncludeGraph(rootId, catalogIndex, { failOnMissing: false });
143
+ }
144
+
123
145
  /**
124
146
  * Recursively traverse the include graph starting from the given root skill ID.
125
147
  * Returns all visited skill IDs in DFS pre-order.
@@ -1,5 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
+ import {
4
+ saveDocument,
5
+ updateDocumentContent,
6
+ } from "../../documents/document-store.js";
3
7
  import type { ToolContext, ToolExecutionResult } from "../types.js";
4
8
 
5
9
  // ── Exported execute functions ──────────────────────────────────────
@@ -12,6 +16,20 @@ export function executeDocumentCreate(
12
16
  const initialContent = (input.initial_content as string | undefined) || "";
13
17
  const surfaceId = `doc-${randomUUID()}`;
14
18
 
19
+ // Persist the document so any client (web or macOS) can fetch it via
20
+ // GET /v1/documents/:id. The macOS client may later update the row
21
+ // via document_save; ON CONFLICT DO UPDATE handles that.
22
+ const wordCount = initialContent
23
+ .split(/\s+/)
24
+ .filter((w) => w.length > 0).length;
25
+ saveDocument({
26
+ surfaceId,
27
+ conversationId: context.conversationId,
28
+ title,
29
+ content: initialContent,
30
+ wordCount,
31
+ });
32
+
15
33
  // Send document_editor_show message to open the built-in RTE
16
34
  if (context.sendToClient) {
17
35
  context.sendToClient({
@@ -67,6 +85,8 @@ export function executeDocumentUpdate(
67
85
  const content = input.content as string;
68
86
  const mode = (input.mode as string | undefined) || "append";
69
87
 
88
+ updateDocumentContent(surfaceId, content, mode);
89
+
70
90
  // Send document_editor_update message to update the built-in RTE
71
91
  if (context.sendToClient) {
72
92
  context.sendToClient({
@@ -27,6 +27,7 @@ import { applyEdit } from "./shared/filesystem/edit-engine.js";
27
27
  import { sandboxPolicy } from "./shared/filesystem/path-policy.js";
28
28
  import { MAX_FILE_SIZE_BYTES } from "./shared/filesystem/size-guard.js";
29
29
  import { ToolApprovalHandler } from "./tool-approval-handler.js";
30
+ import { resolveToolNameAlias } from "./tool-name-aliases.js";
30
31
  import type {
31
32
  ToolContext,
32
33
  ToolExecutionResult,
@@ -76,7 +77,12 @@ export class ToolExecutor {
76
77
  };
77
78
 
78
79
  const middlewares = getMiddlewaresFor("toolExecute");
79
- const pipelineArgs: ToolExecuteArgs = { name, input, context };
80
+ const executionName = resolveToolNameAlias(name, context.allowedToolNames);
81
+ const pipelineArgs: ToolExecuteArgs = {
82
+ name: executionName,
83
+ input,
84
+ context,
85
+ };
80
86
 
81
87
  // No pipeline-level timeout: `executeInternal` already wraps the real
82
88
  // tool invocation in `executeWithTimeout`, which is the sole enforcer
@@ -107,6 +113,11 @@ export class ToolExecutor {
107
113
  riskLevel: string;
108
114
  riskReason: string;
109
115
  riskScopeOptions: Array<{ pattern: string; label: string }>;
116
+ riskAllowlistOptions?: Array<{
117
+ label: string;
118
+ description: string;
119
+ pattern: string;
120
+ }>;
110
121
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
111
122
  isContainerized?: boolean;
112
123
  }
@@ -205,6 +216,7 @@ export class ToolExecutor {
205
216
  riskLevel: permRiskMeta?.riskLevel,
206
217
  riskReason: permRiskMeta?.riskReason,
207
218
  riskScopeOptions: permRiskMeta?.riskScopeOptions,
219
+ riskAllowlistOptions: permRiskMeta?.riskAllowlistOptions,
208
220
  riskDirectoryScopeOptions: permRiskMeta?.riskDirectoryScopeOptions,
209
221
  isContainerized: permRiskMeta?.isContainerized,
210
222
  matchedTrustRuleId: permMatchedTrustRuleId,
@@ -422,12 +434,16 @@ export class ToolExecutor {
422
434
  riskLevel: permRiskMeta.riskLevel,
423
435
  riskReason: permRiskMeta.riskReason,
424
436
  riskScopeOptions: permRiskMeta.riskScopeOptions,
437
+ riskAllowlistOptions: permRiskMeta.riskAllowlistOptions,
425
438
  riskDirectoryScopeOptions: permRiskMeta.riskDirectoryScopeOptions,
426
439
  isContainerized: permRiskMeta.isContainerized,
427
440
  };
428
441
  }
429
442
  if (permMatchedTrustRuleId) {
430
- execResult = { ...execResult, matchedTrustRuleId: permMatchedTrustRuleId };
443
+ execResult = {
444
+ ...execResult,
445
+ matchedTrustRuleId: permMatchedTrustRuleId,
446
+ };
431
447
  }
432
448
  if (permApprovalMode) {
433
449
  execResult = { ...execResult, approvalMode: permApprovalMode };
@@ -11,14 +11,16 @@ import {
11
11
  test,
12
12
  } from "bun:test";
13
13
 
14
- import { _setOverridesForTesting } from "../../config/assistant-feature-flags.js";
15
14
  import { PKB_WORKSPACE_SCOPE } from "../../memory/pkb/types.js";
16
15
  import type { ToolContext } from "../types.js";
17
16
 
18
- // This test exercises v1 PKB re-index enqueue. The `memory-v2-enabled` flag
19
- // (registry default `true`) makes the enqueue path skipped — disable it so
20
- // the v1 PKB index path stays under test.
21
- _setOverridesForTesting({ "memory-v2-enabled": false });
17
+ // This test exercises v1 PKB re-index enqueue. `config.memory.v2.enabled`
18
+ // (default `true`) makes the enqueue path skipped — force it off so the
19
+ // v1 PKB index path stays under test.
20
+ mock.module("../../config/loader.js", () => ({
21
+ getConfig: () => ({ memory: { v2: { enabled: false } } }),
22
+ loadConfig: () => ({ memory: { v2: { enabled: false } } }),
23
+ }));
22
24
 
23
25
  let tmpWorkspace: string;
24
26
  let previousWorkspaceEnv: string | undefined;
@@ -32,6 +32,11 @@ export type PermissionDecision =
32
32
  riskLevel: string;
33
33
  riskReason: string;
34
34
  riskScopeOptions: Array<{ pattern: string; label: string }>;
35
+ riskAllowlistOptions?: Array<{
36
+ label: string;
37
+ description: string;
38
+ pattern: string;
39
+ }>;
35
40
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
36
41
  isContainerized?: boolean;
37
42
  };
@@ -51,6 +56,11 @@ export type PermissionDecision =
51
56
  riskLevel: string;
52
57
  riskReason: string;
53
58
  riskScopeOptions: Array<{ pattern: string; label: string }>;
59
+ riskAllowlistOptions?: Array<{
60
+ label: string;
61
+ description: string;
62
+ pattern: string;
63
+ }>;
54
64
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
55
65
  isContainerized?: boolean;
56
66
  };
@@ -111,7 +121,12 @@ export class PermissionChecker {
111
121
  ? {
112
122
  riskLevel: cachedAssessment.riskLevel,
113
123
  riskReason: cachedAssessment.reason,
124
+ // Display ladder (regex patterns — internal only, not for save).
114
125
  riskScopeOptions: cachedAssessment.scopeOptions,
126
+ // Save ladder (Minimatch globs — what the gateway matches against).
127
+ // Populated for classifiers that produce allowlist options
128
+ // (bash, file, skill); undefined otherwise.
129
+ riskAllowlistOptions: cachedAssessment.allowlistOptions,
115
130
  riskDirectoryScopeOptions: cachedAssessment.directoryScopeOptions,
116
131
  isContainerized: getIsContainerized(),
117
132
  }
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  collectAllMissing,
21
21
  indexCatalogById,
22
- validateIncludes,
22
+ validateIncludeCycles,
23
23
  } from "../../skills/include-graph.js";
24
24
  import { renderInlineCommands } from "../../skills/inline-command-render.js";
25
25
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
@@ -204,6 +204,7 @@ export class SkillLoadTool implements Tool {
204
204
 
205
205
  // Load catalog for include validation and child metadata output
206
206
  let catalogIndex: Map<string, SkillSummary> | undefined;
207
+ let missingIncludedSkillIds: string[] = [];
207
208
  if (skill.includes && skill.includes.length > 0) {
208
209
  let catalog = loadSkillCatalog();
209
210
  catalogIndex = indexCatalogById(catalog);
@@ -260,19 +261,13 @@ export class SkillLoadTool implements Tool {
260
261
  catalogIndex = indexCatalogById(catalog);
261
262
  }
262
263
 
263
- // Validate (fail-closed - catches genuinely missing deps + cycles)
264
- const validation = validateIncludes(skill.id, catalogIndex);
264
+ missingIncludedSkillIds = [...collectAllMissing(skill.id, catalogIndex)];
265
+
266
+ // Validate cycles fail closed. Missing includes are advisory: the parent
267
+ // skill should still load so the assistant can decide whether to search
268
+ // for and install the suggested dependency.
269
+ const validation = validateIncludeCycles(skill.id, catalogIndex);
265
270
  if (!validation.ok) {
266
- if (validation.error === "missing") {
267
- return {
268
- content: `Error: skill "${skill.id}" includes "${
269
- validation.missingChildId
270
- }" which was not found (referenced by "${
271
- validation.parentId
272
- }" via path: ${validation.path.join(" → ")})`,
273
- isError: true,
274
- };
275
- }
276
271
  if (validation.error === "cycle") {
277
272
  return {
278
273
  content: `Error: skill "${
@@ -283,10 +278,6 @@ export class SkillLoadTool implements Tool {
283
278
  isError: true,
284
279
  };
285
280
  }
286
- return {
287
- content: `Error: skill "${skill.id}" has an invalid include graph`,
288
- isError: true,
289
- };
290
281
  }
291
282
  }
292
283
 
@@ -444,13 +435,25 @@ export class SkillLoadTool implements Tool {
444
435
  }
445
436
  }
446
437
  }
447
- immediateChildrenSection = `Included Skills (immediate):\n${childLines.join(
448
- "\n",
449
- )}`;
438
+ immediateChildrenSection =
439
+ childLines.length > 0
440
+ ? `Included Skills (immediate):\n${childLines.join("\n")}`
441
+ : "Included Skills (immediate): none";
450
442
  } else {
451
443
  immediateChildrenSection = "Included Skills (immediate): none";
452
444
  }
453
445
 
446
+ const missingIncludesSection =
447
+ missingIncludedSkillIds.length > 0
448
+ ? [
449
+ "Suggested Included Skills (not loaded):",
450
+ ...missingIncludedSkillIds.map(
451
+ (id) =>
452
+ ` - ${id}: not installed or unavailable. If this task needs it, search for and install this skill, then load it.`,
453
+ ),
454
+ ].join("\n")
455
+ : undefined;
456
+
454
457
  let versionHash: string | undefined;
455
458
  try {
456
459
  versionHash = computeSkillVersionHash(skill.directoryPath);
@@ -512,6 +515,7 @@ export class SkillLoadTool implements Tool {
512
515
  : []),
513
516
  ...includedBodies.flatMap((b) => [b, ""]),
514
517
  immediateChildrenSection,
518
+ ...(missingIncludesSection ? [missingIncludesSection] : []),
515
519
  "",
516
520
  `<loaded_skill id="${skill.id}"${versionAttr} />`,
517
521
  ...includeMarkers,
@@ -0,0 +1,19 @@
1
+ const TOOL_NAME_ALIASES = new Map<string, string>([
2
+ ["create_app", "app_create"],
3
+ ]);
4
+
5
+ /**
6
+ * Resolve high-confidence compatibility aliases before active-tool gating.
7
+ * Keep this list narrow: aliases should only cover observed model drift where
8
+ * the canonical target is active for the turn.
9
+ */
10
+ export function resolveToolNameAlias(
11
+ name: string,
12
+ allowedToolNames?: ReadonlySet<string>,
13
+ ): string {
14
+ if (allowedToolNames?.has(name)) return name;
15
+ const canonical = TOOL_NAME_ALIASES.get(name);
16
+ if (!canonical) return name;
17
+ if (allowedToolNames && !allowedToolNames.has(canonical)) return name;
18
+ return canonical;
19
+ }
@@ -115,8 +115,26 @@ export interface ToolExecutionResult {
115
115
  riskThreshold?: string;
116
116
  /** Whether the daemon is running in a containerized (Docker) environment. */
117
117
  isContainerized?: boolean;
118
- /** Scope options ladder for the rule editor (narrowest to broadest). */
118
+ /**
119
+ * Display-only ladder of scope option labels for the rule editor
120
+ * (narrowest to broadest). The `pattern` field here is a regex-style
121
+ * descriptor used internally by the daemon and is NOT a valid trust
122
+ * rule pattern. Use `riskAllowlistOptions` for the pattern that gets
123
+ * saved as a trust rule.
124
+ */
119
125
  riskScopeOptions?: Array<{ pattern: string; label: string }>;
126
+ /**
127
+ * Allowlist options for the rule editor save path (narrowest to
128
+ * broadest). Each `pattern` is a Minimatch-glob compatible string
129
+ * (e.g. raw command for exact match, `action:<program>` for command
130
+ * wildcards) — what the gateway actually matches against. Mirrors
131
+ * the `allowlistOptions` field on `ConfirmationRequest` SSE events.
132
+ */
133
+ riskAllowlistOptions?: Array<{
134
+ label: string;
135
+ description: string;
136
+ pattern: string;
137
+ }>;
120
138
  /** Directory scope ladder for the rule editor (narrowest to broadest). */
121
139
  riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
122
140
  /**
@@ -1,72 +1,14 @@
1
- import {
2
- appendFileSync,
3
- existsSync,
4
- readFileSync,
5
- writeFileSync,
6
- } from "node:fs";
7
- import { join } from "node:path";
8
-
9
- import { getLogger } from "../../util/logger.js";
10
1
  import type { WorkspaceMigration } from "./types.js";
11
2
 
12
- const log = getLogger(
13
- "workspace-migration-067-release-notes-safe-storage-limits",
14
- );
15
-
16
3
  const MIGRATION_ID = "067-release-notes-safe-storage-limits";
17
- const MARKER = `<!-- release-note-id:${MIGRATION_ID} -->`;
18
-
19
- const RELEASE_NOTE = `${MARKER}
20
- ## Safe storage limits
21
-
22
- A new storage protection mode is available behind the safe-storage-limits
23
- rollout flag. When enabled, the assistant watches workspace disk usage and
24
- enters cleanup mode if the volume reaches the critical 95% threshold.
25
-
26
- In cleanup mode, background processes pause and remote messages, including
27
- trusted-contact messages, are blocked until the guardian frees enough space or
28
- explicitly overrides the lock. The macOS app now shows a storage cleanup banner
29
- that must be acknowledged before cleanup chat continues, then keeps a status
30
- banner visible while cleanup mode is active.
31
- `;
32
4
 
33
5
  export const releaseNotesSafeStorageLimitsMigration: WorkspaceMigration = {
34
6
  id: MIGRATION_ID,
35
- description: "Append release notes for safe storage limits to UPDATES.md",
36
-
37
- run(workspaceDir: string): void {
38
- const updatesPath = join(workspaceDir, "UPDATES.md");
7
+ description: "Reserved migration slot for safe storage limits release notes",
39
8
 
40
- try {
41
- if (existsSync(updatesPath)) {
42
- const existing = readFileSync(updatesPath, "utf-8");
43
- if (existing.includes(MARKER)) {
44
- return;
45
- }
46
- const needsLeadingNewline = !existing.endsWith("\n\n");
47
- const prefix = existing.endsWith("\n") ? "\n" : "\n\n";
48
- appendFileSync(
49
- updatesPath,
50
- needsLeadingNewline ? `${prefix}${RELEASE_NOTE}` : RELEASE_NOTE,
51
- "utf-8",
52
- );
53
- } else {
54
- writeFileSync(updatesPath, RELEASE_NOTE, "utf-8");
55
- }
56
- log.info(
57
- { path: updatesPath },
58
- "Appended safe storage limits release note",
59
- );
60
- } catch (err) {
61
- log.warn(
62
- { err, path: updatesPath },
63
- "Failed to append safe storage limits release note to UPDATES.md",
64
- );
65
- }
9
+ run(_workspaceDir: string): void {
10
+ // Registered no-op slot retained for workspace migration checkpoint compatibility.
66
11
  },
67
12
 
68
- down(_workspaceDir: string): void {
69
- // Forward-only: UPDATES.md is a user-facing bulletin the assistant
70
- // processes and deletes on its own.
71
- },
13
+ down(_workspaceDir: string): void {},
72
14
  };
@@ -0,0 +1,28 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ const ONBOARDING_THREADS = `- Figure out what kind of personality would best mesh with your user. Figure out who you are and what your voice should be. Your choice should be DISTINCT and have CHARACTER. Once you've figured this out, rewrite SOUL.md and IDENTITY.md in your own voice to define who you are.
7
+ - Work with your user to set a custom avatar for yourself. This task is done once data/avatar/avatar-image.png exists.
8
+ - Ask your user if they use ChatGPT, Claude or another AI tool and offer to help them import memories from there.
9
+ - Suggest connecting to available messaging services like Slack or Telegram
10
+ `;
11
+
12
+ export const seedOnboardingThreadsMigration: WorkspaceMigration = {
13
+ id: "069-seed-onboarding-threads",
14
+ description:
15
+ "Seed memory/threads.md with onboarding tasks for brand new assistants",
16
+
17
+ run(workspaceDir: string): void {
18
+ const filePath = join(workspaceDir, "memory", "threads.md");
19
+ if (!existsSync(filePath)) return;
20
+ const current = readFileSync(filePath, "utf-8");
21
+ if (current.trim().length > 0) return;
22
+ writeFileSync(filePath, ONBOARDING_THREADS, "utf-8");
23
+ },
24
+
25
+ down(_workspaceDir: string): void {
26
+ // Forward-only: never delete user-visible memory content on rollback.
27
+ },
28
+ };
@@ -0,0 +1,31 @@
1
+ import type { WorkspaceMigration } from "./types.js";
2
+
3
+ /**
4
+ * Audit-only entry for the v2 concept-page schema upgrade introduced in PR
5
+ * #29823 (summary_dense / summary_sparse named vectors). The destructive
6
+ * collection rebuild and reembed enqueue both run inside the daemon at
7
+ * Qdrant init time — see `maybeRebuildMemoryV2Concepts` in
8
+ * `assistant/src/daemon/memory-v2-startup.ts`. The "exactly-once" fence is
9
+ * per-collection schema introspection, not per-workspace checkpoint, so
10
+ * users who wipe Qdrant separately still get re-rebuilt without resetting
11
+ * any workspace flag.
12
+ *
13
+ * This migration exists so the registry chronology records the schema
14
+ * upgrade alongside the release that introduced it.
15
+ */
16
+ export const memoryV2SummarySchemaRebuildMigration: WorkspaceMigration = {
17
+ id: "070-memory-v2-summary-schema-rebuild",
18
+ description:
19
+ "Audit entry: v2 concept-page Qdrant collection now carries summary_dense / summary_sparse named vectors. Rebuild + reembed handled by the daemon's startup hook.",
20
+
21
+ run(_workspaceDir: string): void {
22
+ // No-op: the actual rebuild lives in the Qdrant client layer where the
23
+ // collection schema is owned. Workspace migrations cannot touch Qdrant
24
+ // (they run before it starts and must be self-contained per
25
+ // assistant/src/workspace/migrations/AGENTS.md).
26
+ },
27
+
28
+ down(_workspaceDir: string): void {
29
+ // Forward-only: schema rollbacks happen outside the migration system.
30
+ },
31
+ };
@@ -0,0 +1,111 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { WorkspaceMigration } from "./types.js";
5
+
6
+ const MIGRATION_ID = "071-remove-safe-storage-release-note";
7
+ const SAFE_STORAGE_RELEASE_NOTE_ID = "067-release-notes-safe-storage-limits";
8
+ const SAFE_STORAGE_MARKER = `<!-- release-note-id:${SAFE_STORAGE_RELEASE_NOTE_ID} -->`;
9
+ const RELEASE_NOTE_MARKER_PREFIX = "<!-- release-note-id:";
10
+
11
+ const SAFE_STORAGE_RELEASE_NOTE = `${SAFE_STORAGE_MARKER}
12
+ ## Safe storage limits
13
+
14
+ A new storage protection mode is available behind the safe-storage-limits
15
+ rollout flag. When enabled, the assistant watches workspace disk usage and
16
+ enters cleanup mode if the volume reaches the critical 95% threshold.
17
+
18
+ In cleanup mode, background processes pause and remote messages, including
19
+ trusted-contact messages, are blocked until the guardian frees enough space or
20
+ explicitly overrides the lock. The macOS app now shows a storage cleanup banner
21
+ that must be acknowledged before cleanup chat continues, then keeps a status
22
+ banner visible while cleanup mode is active.
23
+ `;
24
+
25
+ const LEADING_NEWLINES = /^(?:(?:\r\n)|\n|\r)+/;
26
+ const TRAILING_NEWLINES = /(?:(?:\r\n)|\n|\r)+$/;
27
+
28
+ function findNewlineStyle(...samples: string[]): string {
29
+ for (const sample of samples) {
30
+ const match = sample.match(/\r\n|\n|\r/);
31
+ if (match) {
32
+ return match[0];
33
+ }
34
+ }
35
+
36
+ return "\n";
37
+ }
38
+
39
+ function stitchAroundRemovedBlock(before: string, after: string): string {
40
+ if (before.trim() === "") {
41
+ return after.replace(LEADING_NEWLINES, "");
42
+ }
43
+
44
+ if (after.trim() === "") {
45
+ return `${before.replace(TRAILING_NEWLINES, "")}${findNewlineStyle(before)}`;
46
+ }
47
+
48
+ const newline = findNewlineStyle(before, after);
49
+ return `${before.replace(TRAILING_NEWLINES, "")}${newline}${newline}${after.replace(LEADING_NEWLINES, "")}`;
50
+ }
51
+
52
+ function removeRange(content: string, start: number, end: number): string {
53
+ return stitchAroundRemovedBlock(content.slice(0, start), content.slice(end));
54
+ }
55
+
56
+ function removeSafeStorageReleaseNote(content: string): string {
57
+ const exactStart = content.indexOf(SAFE_STORAGE_RELEASE_NOTE);
58
+ if (exactStart >= 0) {
59
+ return removeRange(
60
+ content,
61
+ exactStart,
62
+ exactStart + SAFE_STORAGE_RELEASE_NOTE.length,
63
+ );
64
+ }
65
+
66
+ const markerStart = content.indexOf(SAFE_STORAGE_MARKER);
67
+ if (markerStart < 0) {
68
+ return content;
69
+ }
70
+
71
+ const nextMarkerStart = content.indexOf(
72
+ RELEASE_NOTE_MARKER_PREFIX,
73
+ markerStart + SAFE_STORAGE_MARKER.length,
74
+ );
75
+
76
+ return removeRange(
77
+ content,
78
+ markerStart,
79
+ nextMarkerStart >= 0 ? nextMarkerStart : content.length,
80
+ );
81
+ }
82
+
83
+ export const removeSafeStorageReleaseNoteMigration: WorkspaceMigration = {
84
+ id: MIGRATION_ID,
85
+ description: "Remove safe storage release note from UPDATES.md",
86
+
87
+ run(workspaceDir: string): void {
88
+ const updatesPath = join(workspaceDir, "UPDATES.md");
89
+ if (!existsSync(updatesPath)) {
90
+ return;
91
+ }
92
+
93
+ const existing = readFileSync(updatesPath, "utf-8");
94
+ if (!existing.includes(SAFE_STORAGE_MARKER)) {
95
+ return;
96
+ }
97
+
98
+ const cleaned = removeSafeStorageReleaseNote(existing);
99
+ if (cleaned.trim() === "") {
100
+ rmSync(updatesPath, { force: true });
101
+ return;
102
+ }
103
+
104
+ writeFileSync(updatesPath, cleaned, "utf-8");
105
+ },
106
+
107
+ down(_workspaceDir: string): void {
108
+ // Forward-only: this removes a pending bulletin that should no longer be
109
+ // shown to users.
110
+ },
111
+ };
@@ -66,6 +66,9 @@ import { bumpStaleHeartbeatIntervalMigration } from "./065-bump-stale-heartbeat-
66
66
  import { seedHeartbeatCallsiteCostDefaultMigration } from "./066-seed-heartbeat-callsite-cost-default.js";
67
67
  import { releaseNotesSafeStorageLimitsMigration } from "./067-release-notes-safe-storage-limits.js";
68
68
  import { releaseNotesLocalTimezoneMigration } from "./068-release-notes-local-timezone.js";
69
+ import { seedOnboardingThreadsMigration } from "./069-seed-onboarding-threads.js";
70
+ import { memoryV2SummarySchemaRebuildMigration } from "./070-memory-v2-summary-schema-rebuild.js";
71
+ import { removeSafeStorageReleaseNoteMigration } from "./071-remove-safe-storage-release-note.js";
69
72
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
70
73
  import type { WorkspaceMigration } from "./types.js";
71
74
 
@@ -143,4 +146,7 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
143
146
  seedHeartbeatCallsiteCostDefaultMigration,
144
147
  releaseNotesSafeStorageLimitsMigration,
145
148
  releaseNotesLocalTimezoneMigration,
149
+ seedOnboardingThreadsMigration,
150
+ memoryV2SummarySchemaRebuildMigration,
151
+ removeSafeStorageReleaseNoteMigration,
146
152
  ];