@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
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Tests for `assistant/src/memory/v2/sweep-job.ts`.
3
3
  *
4
- * Coverage matrix (from PR 18 acceptance criteria):
5
- * - Flag off → no provider/DB calls, returns 0 (early bail).
6
- * - Flag on + no recent messages → no provider call, returns 0.
7
- * - Flag on + recent messages → provider invoked with rendered prompt;
4
+ * Coverage matrix:
5
+ * - v2 disabled in config → no provider/DB calls, returns 0 (early bail).
6
+ * - sweep_enabled off → no provider call, returns 0.
7
+ * - v2 on + no recent messages → no provider call, returns 0.
8
+ * - v2 on + recent messages → provider invoked with rendered prompt;
8
9
  * each entry is appended to `memory/buffer.md` AND today's archive.
9
10
  * - Tool-call response shape mismatch → returns 0 without writes.
10
11
  * - Empty entries are skipped (the model can't pad the buffer).
@@ -24,7 +25,6 @@ import { tmpdir } from "node:os";
24
25
  import { join } from "node:path";
25
26
  import {
26
27
  afterAll,
27
- afterEach,
28
28
  beforeAll,
29
29
  beforeEach,
30
30
  describe,
@@ -85,23 +85,20 @@ afterAll(() => {
85
85
  const { resetDb, getDb } = await import("../../db-connection.js");
86
86
  const { initializeDb } = await import("../../db-init.js");
87
87
  const { messages, conversations } = await import("../../schema.js");
88
- const { _setOverridesForTesting } =
89
- await import("../../../config/assistant-feature-flags.js");
90
88
  const { memoryV2SweepJob } = await import("../sweep-job.js");
91
89
 
92
- // `isAssistantFeatureFlagEnabled` ignores the `config` argument it receives
93
- // (resolution is purely from the overrides + registry caches), so we hand
94
- // the handler a minimal stand-in instead of materializing the full default
90
+ // The handler reads `config.memory.v2.enabled` and `sweep_enabled`, so we
91
+ // hand it a minimal stand-in instead of materializing the full default
95
92
  // config — which would otherwise pull in heavy schemas this test doesn't
96
- // exercise. The handler reads `config.memory.v2.sweep_enabled`, so the
97
- // `flag on` cases need the field set; the `flag off` case bails before
98
- // the check and uses the bare empty stand-in.
93
+ // exercise.
99
94
  const CONFIG = {
100
- memory: { v2: { sweep_enabled: true } },
95
+ memory: { v2: { enabled: true, sweep_enabled: true } },
96
+ } as Parameters<typeof memoryV2SweepJob>[1];
97
+ const CONFIG_V2_OFF = {
98
+ memory: { v2: { enabled: false, sweep_enabled: true } },
101
99
  } as Parameters<typeof memoryV2SweepJob>[1];
102
- const CONFIG_FLAG_OFF = {} as Parameters<typeof memoryV2SweepJob>[1];
103
100
  const CONFIG_SWEEP_OFF = {
104
- memory: { v2: { sweep_enabled: false } },
101
+ memory: { v2: { enabled: true, sweep_enabled: false } },
105
102
  } as Parameters<typeof memoryV2SweepJob>[1];
106
103
 
107
104
  function makeJob(): Parameters<typeof memoryV2SweepJob>[0] {
@@ -205,18 +202,13 @@ beforeEach(() => {
205
202
  providerStub = null;
206
203
  });
207
204
 
208
- afterEach(() => {
209
- _setOverridesForTesting({});
210
- });
211
-
212
205
  // ---------------------------------------------------------------------------
213
206
 
214
- describe("memoryV2SweepJob — flag off", () => {
215
- test("returns 0 without invoking the provider when flag is off", async () => {
216
- _setOverridesForTesting({ "memory-v2-enabled": false });
207
+ describe("memoryV2SweepJob — v2 disabled", () => {
208
+ test("returns 0 without invoking the provider when memory.v2.enabled is false", async () => {
217
209
  providerStub = makeEntriesProvider(["should-not-be-written"]);
218
210
 
219
- const written = await memoryV2SweepJob(makeJob(), CONFIG_FLAG_OFF);
211
+ const written = await memoryV2SweepJob(makeJob(), CONFIG_V2_OFF);
220
212
 
221
213
  expect(written).toBe(0);
222
214
  expect(providerCalls).toHaveLength(0);
@@ -224,9 +216,8 @@ describe("memoryV2SweepJob — flag off", () => {
224
216
  });
225
217
  });
226
218
 
227
- describe("memoryV2SweepJob — flag on, sweep_enabled off", () => {
219
+ describe("memoryV2SweepJob — sweep_enabled off", () => {
228
220
  test("returns 0 without invoking the provider when sweep_enabled is false", async () => {
229
- _setOverridesForTesting({ "memory-v2-enabled": true });
230
221
  // No message seeding required — the sweep_enabled bail short-circuits
231
222
  // before any DB or workspace reads.
232
223
  providerStub = makeEntriesProvider(["should-not-be-written"]);
@@ -239,11 +230,7 @@ describe("memoryV2SweepJob — flag on, sweep_enabled off", () => {
239
230
  });
240
231
  });
241
232
 
242
- describe("memoryV2SweepJob — flag on, no recent messages", () => {
243
- beforeEach(() => {
244
- _setOverridesForTesting({ "memory-v2-enabled": true });
245
- });
246
-
233
+ describe("memoryV2SweepJob — no recent messages", () => {
247
234
  test("returns 0 when no messages exist in the recent window", async () => {
248
235
  providerStub = makeEntriesProvider(["should-not-be-written"]);
249
236
 
@@ -273,9 +260,8 @@ describe("memoryV2SweepJob — flag on, no recent messages", () => {
273
260
  // DB intact long enough for the SQL inserts here to clash.
274
261
  let convCounter = 0;
275
262
 
276
- describe("memoryV2SweepJob — flag on, recent messages", () => {
263
+ describe("memoryV2SweepJob — recent messages", () => {
277
264
  beforeEach(() => {
278
- _setOverridesForTesting({ "memory-v2-enabled": true });
279
265
  seedMessages(`conv-${++convCounter}`, [
280
266
  {
281
267
  role: "user",
@@ -15,8 +15,8 @@
15
15
  * substitute in.
16
16
  *
17
17
  * Lifecycle:
18
- * 1. Bail if the `memory-v2-enabled` feature flag is off (the worker may
19
- * have claimed a stale row at flag-flip time).
18
+ * 1. Bail if `config.memory.v2.enabled` is false (the worker may have
19
+ * claimed a stale row from before v2 was disabled).
20
20
  * 2. Acquire a single-process lock at `memory/.v2-state/consolidation.lock`
21
21
  * so two overlapping schedule windows can't fight over the same files.
22
22
  * The lock contains the holder's PID + timestamp so a crashed run leaves
@@ -53,7 +53,6 @@ import {
53
53
  } from "node:fs";
54
54
  import { dirname, join } from "node:path";
55
55
 
56
- import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
57
56
  import type { AssistantConfig } from "../../config/types.js";
58
57
  import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
59
58
  import { wakeAgentForOpportunity } from "../../runtime/agent-wake.js";
@@ -85,11 +84,11 @@ const FOLLOW_UP_JOB_TYPES: readonly MemoryJobType[] = [
85
84
 
86
85
  /**
87
86
  * Job handler. See file header for the full lifecycle. Returns a discriminated
88
- * union so tests can assert on the path taken (flag-off / locked / empty /
87
+ * union so tests can assert on the path taken (disabled / locked / empty /
89
88
  * invoked) without having to spy on the filesystem.
90
89
  */
91
90
  export type ConsolidationOutcome =
92
- | { kind: "flag_off" }
91
+ | { kind: "disabled" }
93
92
  | { kind: "locked"; holder: string }
94
93
  | { kind: "empty_buffer" }
95
94
  | { kind: "wake_failed"; reason?: string }
@@ -104,9 +103,9 @@ export async function memoryV2ConsolidateJob(
104
103
  _job: MemoryJob,
105
104
  config: AssistantConfig,
106
105
  ): Promise<ConsolidationOutcome> {
107
- if (!isAssistantFeatureFlagEnabled("memory-v2-enabled", config)) {
108
- log.debug("memory-v2-enabled flag off; consolidation skipped");
109
- return { kind: "flag_off" };
106
+ if (!config.memory.v2.enabled) {
107
+ log.debug("memory.v2.enabled is false; consolidation skipped");
108
+ return { kind: "disabled" };
110
109
  }
111
110
 
112
111
  const memoryDir = join(getWorkspaceDir(), "memory");
@@ -360,6 +360,16 @@ interface RenderInjectionBlockResult {
360
360
  missingSlugs: string[];
361
361
  }
362
362
 
363
+ /**
364
+ * Leading instruction line emitted at the top of every non-empty injection
365
+ * block. Tells the agent that what follows are page summaries and that it
366
+ * should read the underlying file when a summary looks relevant. Pages
367
+ * without a `summary` field render in full instead — the agent treats
368
+ * those as inline content and doesn't need to follow up.
369
+ */
370
+ const INJECTION_HEADER =
371
+ "**CRITICAL:** These are page summaries. Read the page file if it looks relevant.";
372
+
363
373
  /**
364
374
  * Render the inner content of the `<memory>` block for a list of slugs.
365
375
  * The caller wraps the result in `<memory>...</memory>` exactly once at
@@ -383,23 +393,24 @@ interface RenderInjectionBlockResult {
383
393
  * skill is an expected catalog-level outcome rather than a stale-index
384
394
  * bug.
385
395
  *
386
- * The block shape mirrors the §5 layout concept-page sections first,
387
- * skills subsection last preserving the prompt format the agent sees:
396
+ * Each concept-page section is rendered as a path header followed by either
397
+ * the page's `summary` (when present in frontmatter) or the full page (the
398
+ * fallback for pages predating the summary field). Skills sit at the end
399
+ * under `### Skills You Can Use`, unchanged. The leading `**CRITICAL:**`
400
+ * line tells the agent how to read the block.
401
+ *
402
+ * **CRITICAL:** These are page summaries. Read the page file if it looks relevant.
403
+ *
404
+ * # memory/concepts/<concept-slug-1>.md
405
+ * <summary-1>
388
406
  *
389
- * ### <concept-slug-1>
407
+ * # memory/concepts/<concept-slug-2>.md
390
408
  * ---
391
409
  * edges:
392
410
  * - <neighbor-slug>
393
411
  * ref_files:
394
412
  * - <path/to/asset>
395
413
  * ---
396
- * <body-1>
397
- *
398
- * ### <concept-slug-2>
399
- * ---
400
- * edges: []
401
- * ref_files: []
402
- * ---
403
414
  * <body-2>
404
415
  *
405
416
  * ### Skills You Can Use
@@ -427,9 +438,18 @@ async function renderInjectionBlock(
427
438
  missingSlugs.push(slug);
428
439
  continue;
429
440
  }
441
+ const summary = page.frontmatter.summary?.trim();
442
+ const path = `memory/concepts/${slug}.md`;
443
+ if (summary && summary.length > 0) {
444
+ sections.push(`# ${path}\n${summary}`);
445
+ continue;
446
+ }
447
+ // Fallback: page predates the `summary` field (or the field was set to
448
+ // empty). Render the full page — frontmatter + body — so retrieval
449
+ // still surfaces the same content the agent saw before this change.
430
450
  const content = renderPageContent(page).trim();
431
451
  if (content.length === 0) continue;
432
- sections.push(`### ${slug}\n${content}`);
452
+ sections.push(`# ${path}\n${content}`);
433
453
  }
434
454
 
435
455
  const skillLines: string[] = [];
@@ -445,7 +465,7 @@ async function renderInjectionBlock(
445
465
  if (sections.length === 0) return { block: null, missingSlugs };
446
466
 
447
467
  return {
448
- block: sections.join("\n\n"),
468
+ block: `${INJECTION_HEADER}\n\n${sections.join("\n\n")}`,
449
469
  missingSlugs,
450
470
  };
451
471
  }
@@ -338,6 +338,45 @@ export async function listPages(workspaceDir: string): Promise<string[]> {
338
338
  return slugs;
339
339
  }
340
340
 
341
+ /**
342
+ * Cheap "do any concept pages exist?" probe — walks the concepts/ tree only
343
+ * far enough to find one `.md` file and returns immediately. Used by the
344
+ * daemon-startup rebuild gate so the empty-after-create recovery path skips
345
+ * a full enumeration of all 1000+ pages just to ask a yes/no question.
346
+ */
347
+ export async function hasConceptPages(workspaceDir: string): Promise<boolean> {
348
+ const root = getConceptsDir(workspaceDir);
349
+ const queue: string[] = [root];
350
+
351
+ while (queue.length > 0) {
352
+ const dir = queue.shift()!;
353
+ let entries;
354
+ try {
355
+ entries = await readdir(dir, { withFileTypes: true });
356
+ } catch (err) {
357
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
358
+ if (dir === root) return false;
359
+ continue;
360
+ }
361
+ throw err;
362
+ }
363
+
364
+ for (const entry of entries) {
365
+ if (entry.name.startsWith(".")) continue;
366
+ if (entry.isDirectory()) {
367
+ queue.push(join(dir, entry.name));
368
+ continue;
369
+ }
370
+ if (!entry.isFile()) continue;
371
+ if (!entry.name.endsWith(PAGE_EXTENSION)) continue;
372
+ if (entry.name.includes(".tmp.")) continue;
373
+ return true;
374
+ }
375
+ }
376
+
377
+ return false;
378
+ }
379
+
341
380
  /**
342
381
  * Delete a concept page. Idempotent — missing files are not an error.
343
382
  *
@@ -131,6 +131,7 @@ edges:
131
131
  - path/to/sister
132
132
  - path/to/parent
133
133
  ref_files: []
134
+ summary: 1-4 sentences describing what this article is. Plain prose only — no bullets, no newlines, no markdown lists. Lead with the most identifying detail.
134
135
  ---
135
136
  # title
136
137
 
@@ -140,6 +141,8 @@ ref_files: []
140
141
  - **bullet 2.** ...
141
142
  \`\`\`
142
143
 
144
+ The \`summary\` field is required on every new or updated article. Retrieval injects \`path + summary\` into context — the agent reads the full file only when the summary looks relevant — so make the summary specific and terse. Keep it on a single YAML line (no \`|\` block scalars, no embedded newlines).
145
+
143
146
  **Caps:** ~5-8 bullets per topic/concept article. ~10-12 per arc-node (which can use bold inline labels: \`**the open**: ...\`).
144
147
 
145
148
  ## One fact, one home
@@ -285,6 +288,7 @@ edges:
285
288
  - some-named-phrase
286
289
  - objects/some-artifact
287
290
  ref_files: []
291
+ summary: A short prose description of the article — 1-4 sentences, single line.
288
292
  ---
289
293
  \`\`\`
290
294
 
@@ -416,6 +420,7 @@ For each article you touched:
416
420
  9. **Spawn check.** Did you ask "what's recognizable here?" not "what have I earned?" Did you catch any hedging — and spawn anyway? Any fold-into-parent / defer stealth-skips you almost did?
417
421
  10. **Split-not-compress.** If anything went over cap, did you split? If you compressed, can you name the rationale in one sentence?
418
422
  11. **Edges.** Outgoing within tiered caps (atomic ≤10, arc ≤15, gravity well ≤25, hard limit 20 on non-hubs)? No noise-edges to gravity wells from non-arc pages?
423
+ 11a. **Summary present.** Every new or updated article has a \`summary:\` line — 1-4 sentences, single YAML line, lead with the identifying detail.
419
424
  12. **Topic coherence.** Does each article answer ONE question? Gravity wells acting as hubs (pointing at topic articles), not absorbing body?
420
425
  13. **\`recent.md\`** under 2000 chars, today=full / older=one-liners?
421
426
  14. **\`[SOURCE NEEDED]\`** tags surfaced for human review?