akm-cli 0.9.1 → 0.9.2-alpha.1

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 (341) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/tasks/core/extract.yml +6 -5
  12. package/dist/assets/tasks/core/improve.yml +6 -5
  13. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  14. package/dist/assets/tasks/core/sync.yml +6 -5
  15. package/dist/assets/tasks/core/version-check.yml +6 -5
  16. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  17. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  21. package/dist/cli/confirm.js +2 -2
  22. package/dist/cli/parse-args.js +3 -24
  23. package/dist/cli/retired-commands.js +1 -1
  24. package/dist/cli/shared.js +2 -2
  25. package/dist/cli.js +11 -9
  26. package/dist/commands/agent/agent-dispatch.js +55 -89
  27. package/dist/commands/agent/contribute-cli.js +12 -45
  28. package/dist/commands/command/builtin-action.js +32 -0
  29. package/dist/commands/command/command-cli.js +99 -0
  30. package/dist/commands/command/command-execution.js +308 -0
  31. package/dist/commands/command/execution-source-loader.js +176 -0
  32. package/dist/commands/command/portable-template.js +60 -0
  33. package/dist/commands/config-cli.js +10 -4
  34. package/dist/commands/env/env.js +4 -2
  35. package/dist/commands/feedback-cli.js +1 -1
  36. package/dist/commands/health/checks.js +241 -29
  37. package/dist/commands/health/html-report.js +0 -14
  38. package/dist/commands/health/report-view-model.js +0 -1
  39. package/dist/commands/health/surfaces.js +6 -7
  40. package/dist/commands/health/types.js +0 -2
  41. package/dist/commands/health.js +63 -18
  42. package/dist/commands/improve/collapse-detector.js +2 -2
  43. package/dist/commands/improve/consolidate.js +251 -214
  44. package/dist/commands/improve/distill/promote-memory.js +71 -34
  45. package/dist/commands/improve/distill/quality-gate.js +17 -5
  46. package/dist/commands/improve/distill.js +232 -155
  47. package/dist/commands/improve/eligibility.js +112 -79
  48. package/dist/commands/improve/execution.js +57 -0
  49. package/dist/commands/improve/extract-cli.js +5 -5
  50. package/dist/commands/improve/extract.js +521 -342
  51. package/dist/commands/improve/improve-strategies.js +43 -14
  52. package/dist/commands/improve/improve.js +249 -29
  53. package/dist/commands/improve/loop-stages.js +11 -17
  54. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  55. package/dist/commands/improve/outcome-loop.js +22 -38
  56. package/dist/commands/improve/planner.js +134 -0
  57. package/dist/commands/improve/preparation.js +730 -409
  58. package/dist/commands/improve/reflect.js +386 -223
  59. package/dist/commands/improve/run-context.js +3 -4
  60. package/dist/commands/improve/salience.js +6 -58
  61. package/dist/commands/improve/session-asset.js +12 -12
  62. package/dist/commands/lint/index.js +101 -29
  63. package/dist/commands/migrate-cli.js +11 -69
  64. package/dist/commands/migration-tool.js +6 -9
  65. package/dist/commands/models-cli.js +27 -0
  66. package/dist/commands/proposal/drain.js +258 -186
  67. package/dist/commands/proposal/proposal-cli.js +32 -10
  68. package/dist/commands/proposal/proposal.js +2 -5
  69. package/dist/commands/proposal/propose.js +192 -172
  70. package/dist/commands/proposal/repository.js +54 -91
  71. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  72. package/dist/commands/read/curate.js +10 -0
  73. package/dist/commands/read/registry-search.js +25 -9
  74. package/dist/commands/read/remember-cli.js +14 -2
  75. package/dist/commands/read/search.js +10 -4
  76. package/dist/commands/read/show.js +139 -153
  77. package/dist/commands/registry-cli.js +16 -7
  78. package/dist/commands/remember.js +33 -18
  79. package/dist/commands/sources/add-cli.js +19 -178
  80. package/dist/commands/sources/bundle-cli.js +15 -3
  81. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  82. package/dist/commands/sources/info.js +2 -1
  83. package/dist/commands/sources/installed-stashes.js +835 -152
  84. package/dist/commands/sources/schema-repair.js +174 -95
  85. package/dist/commands/sources/self-update.js +30 -74
  86. package/dist/commands/sources/source-add.js +3 -5
  87. package/dist/commands/sources/sources-cli.js +2 -15
  88. package/dist/commands/sources/update-transaction.js +220 -0
  89. package/dist/commands/tasks/tasks-cli.js +3 -3
  90. package/dist/commands/tasks/tasks.js +736 -317
  91. package/dist/commands/workflow-cli.js +2 -2
  92. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  93. package/dist/core/adapter/adapters/akm-adapter.js +83 -34
  94. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  95. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  96. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  97. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  98. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  99. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  100. package/dist/core/adapter/adapters/index.js +0 -9
  101. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  102. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  103. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  104. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  105. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  106. package/dist/core/adapter/execution-source.js +308 -0
  107. package/dist/core/adapter/recognize-match.js +36 -13
  108. package/dist/core/adapter/registry.js +0 -9
  109. package/dist/core/asset/stash-meta.js +94 -4
  110. package/dist/core/common.js +6 -11
  111. package/dist/core/config/config-io.js +3 -3
  112. package/dist/core/config/config-schema.js +18 -40
  113. package/dist/core/config/config-sources.js +11 -21
  114. package/dist/core/config/config-walker.js +31 -13
  115. package/dist/core/config/config.js +21 -20
  116. package/dist/core/config/schema/engines.js +8 -7
  117. package/dist/core/config/schema/improve-processes.js +29 -5
  118. package/dist/core/config/schema/primitives.js +1 -23
  119. package/dist/core/config/schema/sources-bundles.js +13 -16
  120. package/dist/core/errors.js +2 -0
  121. package/dist/core/events.js +68 -32
  122. package/dist/core/extra-params.js +1 -0
  123. package/dist/core/improve-result.js +315 -0
  124. package/dist/core/lesson-lint.js +0 -6
  125. package/dist/core/maintenance-barrier.js +4 -4
  126. package/dist/core/network-policy.js +152 -0
  127. package/dist/core/paths.js +1 -1
  128. package/dist/core/recognition-util.js +4 -4
  129. package/dist/core/registry-url.js +456 -0
  130. package/dist/core/state/migrations.js +161 -47
  131. package/dist/core/state-db.js +453 -80
  132. package/dist/core/system-error.js +32 -0
  133. package/dist/core/time.js +2 -12
  134. package/dist/core/write-source.js +0 -18
  135. package/dist/execution/directory-identity.js +52 -0
  136. package/dist/execution/executable-identity.js +107 -0
  137. package/dist/execution/guarded-source.js +398 -0
  138. package/dist/execution/json.js +95 -0
  139. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  140. package/dist/execution/record.js +55 -0
  141. package/dist/execution/resolved-request.js +730 -0
  142. package/dist/execution/source.js +320 -0
  143. package/dist/indexer/bundle-identity-guard.js +5 -4
  144. package/dist/indexer/db/graph-db.js +33 -0
  145. package/dist/indexer/graph/graph-boost.js +3 -4
  146. package/dist/indexer/graph/graph-extraction.js +562 -373
  147. package/dist/indexer/index-written-assets.js +65 -34
  148. package/dist/indexer/indexer.js +443 -222
  149. package/dist/indexer/installations.js +6 -0
  150. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  151. package/dist/indexer/passes/memory-inference.js +227 -174
  152. package/dist/indexer/scan/doc-to-entry.js +7 -9
  153. package/dist/indexer/scan/drain-dir.js +51 -23
  154. package/dist/indexer/search/db-search.js +98 -22
  155. package/dist/indexer/search/search-fields.js +1 -1
  156. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  157. package/dist/indexer/search/search-source.js +7 -16
  158. package/dist/indexer/search/semantic-status.js +10 -1
  159. package/dist/indexer/usage/show-usage.js +105 -0
  160. package/dist/indexer/usage/usage-events.js +7 -2
  161. package/dist/indexer/walk/matchers.js +40 -10
  162. package/dist/indexer/walk/path-resolver.js +5 -2
  163. package/dist/indexer/walk/walker.js +20 -2
  164. package/dist/integrations/agent/builder-shared.js +3 -6
  165. package/dist/integrations/agent/conversation-fallback.js +16 -0
  166. package/dist/integrations/agent/engine-resolution.js +87 -87
  167. package/dist/integrations/agent/execution-cascade.js +566 -0
  168. package/dist/integrations/agent/execution-definitions.js +211 -0
  169. package/dist/integrations/agent/execution-lowering.js +811 -0
  170. package/dist/integrations/agent/execution-preparation.js +67 -0
  171. package/dist/integrations/agent/index.js +0 -2
  172. package/dist/integrations/agent/inline-execution.js +74 -0
  173. package/dist/integrations/agent/model-map.js +515 -0
  174. package/dist/integrations/agent/persona-fallback.js +30 -0
  175. package/dist/integrations/agent/request-lowering.js +186 -0
  176. package/dist/integrations/agent/runner-dispatch.js +230 -37
  177. package/dist/integrations/agent/runner.js +12 -83
  178. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  179. package/dist/integrations/harnesses/aider/index.js +0 -1
  180. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  181. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  182. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  183. package/dist/integrations/harnesses/claude/index.js +1 -5
  184. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  185. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  186. package/dist/integrations/harnesses/codex/index.js +0 -1
  187. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/copilot/index.js +0 -1
  189. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/gemini/index.js +0 -1
  191. package/dist/integrations/harnesses/index.js +4 -44
  192. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  193. package/dist/integrations/harnesses/opencode/index.js +0 -2
  194. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  195. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  196. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  197. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  198. package/dist/integrations/harnesses/openhands/index.js +0 -1
  199. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  200. package/dist/integrations/harnesses/pi/index.js +0 -1
  201. package/dist/integrations/harnesses/shared.js +0 -1
  202. package/dist/integrations/harnesses/types.js +1 -3
  203. package/dist/integrations/lockfile.js +82 -79
  204. package/dist/integrations/session-logs/index.js +6 -17
  205. package/dist/integrations/session-logs/provider-base.js +1 -29
  206. package/dist/llm/client.js +10 -5
  207. package/dist/llm/embedder.js +6 -7
  208. package/dist/llm/embedders/local.js +37 -88
  209. package/dist/llm/embedders/types.js +1 -1
  210. package/dist/llm/graph-extract.js +75 -50
  211. package/dist/llm/index-passes.js +43 -5
  212. package/dist/llm/memory-infer.js +8 -6
  213. package/dist/llm/metadata-enhance.js +5 -3
  214. package/dist/llm/structured-call.js +122 -25
  215. package/dist/output/format-exempt.js +1 -1
  216. package/dist/output/render-registry.js +0 -16
  217. package/dist/output/renderers.js +12 -7
  218. package/dist/output/shapes/curate.js +1 -0
  219. package/dist/output/shapes/helpers.js +10 -2
  220. package/dist/output/shapes/passthrough.js +2 -0
  221. package/dist/output/text/command-format.js +28 -32
  222. package/dist/output/text/health-format.js +1 -29
  223. package/dist/output/text/migrate.js +6 -56
  224. package/dist/output/text/proposal-format.js +16 -1
  225. package/dist/output/text/workflow-format.js +16 -0
  226. package/dist/registry/network.js +279 -0
  227. package/dist/registry/pinned-request-helper.js +247 -0
  228. package/dist/registry/pinned-transport.js +717 -0
  229. package/dist/registry/providers/skills-sh.js +18 -6
  230. package/dist/registry/providers/static-index.js +20 -7
  231. package/dist/registry/resolve.js +53 -28
  232. package/dist/scripts/akm-migrate-node.js +19122 -52218
  233. package/dist/scripts/akm-migrate.js +19064 -51567
  234. package/dist/setup/registry-stash-loader.js +64 -20
  235. package/dist/setup/semantic-assets.js +9 -34
  236. package/dist/setup/setup.js +12 -30
  237. package/dist/setup/source-identity.js +17 -0
  238. package/dist/setup/steps/sources.js +36 -15
  239. package/dist/setup/steps/tasks.js +39 -11
  240. package/dist/sources/providers/git-provider.js +3 -3
  241. package/dist/sources/providers/npm.js +2 -2
  242. package/dist/sources/providers/provider-utils.js +4 -3
  243. package/dist/sources/providers/website.js +11 -7
  244. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  245. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  246. package/dist/sources/website-url.js +73 -0
  247. package/dist/storage/engines/sqlite-migrations.js +81 -26
  248. package/dist/storage/managed-db.js +27 -24
  249. package/dist/storage/repositories/events-repository.js +3 -0
  250. package/dist/storage/repositories/index-connection.js +42 -10
  251. package/dist/storage/repositories/index-entries-repository.js +167 -207
  252. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  253. package/dist/storage/repositories/index-entry-schema.js +255 -0
  254. package/dist/storage/repositories/index-fts-repository.js +10 -10
  255. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  256. package/dist/storage/repositories/index-meta-repository.js +0 -11
  257. package/dist/storage/repositories/index-schema.js +71 -342
  258. package/dist/storage/repositories/index-utility-repository.js +12 -17
  259. package/dist/storage/repositories/index-vec-repository.js +1 -1
  260. package/dist/storage/repositories/proposals-repository.js +4 -127
  261. package/dist/storage/repositories/registry-cache.js +2 -1
  262. package/dist/storage/repositories/task-history-repository.js +20 -40
  263. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  264. package/dist/storage/sqlite-read-snapshot.js +148 -0
  265. package/dist/tasks/backends/cron.js +170 -42
  266. package/dist/tasks/backends/index.js +1 -1
  267. package/dist/tasks/backends/launchd.js +787 -202
  268. package/dist/tasks/backends/schtasks.js +282 -83
  269. package/dist/tasks/embedded.js +7 -7
  270. package/dist/tasks/frozen-script.js +50 -0
  271. package/dist/tasks/resolve-akm-bin.js +5 -1
  272. package/dist/tasks/runner.js +239 -251
  273. package/dist/tasks/runtime-v3.js +281 -0
  274. package/dist/tasks/scheduler-binding.js +272 -0
  275. package/dist/tasks/scheduler-invocation.js +57 -43
  276. package/dist/tasks/scheduler-sync.js +654 -0
  277. package/dist/tasks/source-v3.js +752 -0
  278. package/dist/tasks/standalone-script-entry.js +5 -0
  279. package/dist/tasks/task-id.js +29 -0
  280. package/dist/workflows/authoring/authoring.js +15 -32
  281. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  282. package/dist/workflows/exec/exec-unit.js +7 -28
  283. package/dist/workflows/exec/frozen-judge.js +57 -89
  284. package/dist/workflows/exec/lowering-notices.js +23 -0
  285. package/dist/workflows/exec/native-executor.js +301 -458
  286. package/dist/workflows/exec/param-secrets.js +4 -3
  287. package/dist/workflows/exec/run-workflow.js +26 -32
  288. package/dist/workflows/exec/step-work.js +105 -109
  289. package/dist/workflows/exec/unit-dispatch.js +103 -27
  290. package/dist/workflows/exec/unit-writer.js +3 -3
  291. package/dist/workflows/exec/worktree.js +2 -2
  292. package/dist/workflows/ir/compile.js +86 -72
  293. package/dist/workflows/ir/environment-v4.js +328 -0
  294. package/dist/workflows/ir/freeze-v4.js +122 -0
  295. package/dist/workflows/ir/plan-hash.js +13 -7
  296. package/dist/workflows/ir/schema-v4.js +525 -0
  297. package/dist/workflows/ir/schema.js +25 -284
  298. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  299. package/dist/workflows/parser.js +27 -24
  300. package/dist/workflows/program/schema.js +1 -2
  301. package/dist/workflows/renderer.js +42 -29
  302. package/dist/workflows/resource-limits.js +4 -5
  303. package/dist/workflows/runtime/agent-identity.js +11 -13
  304. package/dist/workflows/runtime/plan-classifier.js +8 -8
  305. package/dist/workflows/runtime/runs.js +27 -43
  306. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  307. package/dist/workflows/source-files.js +373 -0
  308. package/dist/workflows/source-ir/compile.js +196 -0
  309. package/dist/workflows/source-ir/github-yaml.js +577 -0
  310. package/dist/workflows/source-ir/ordering.js +38 -0
  311. package/dist/workflows/source-ir/program.js +50 -0
  312. package/dist/workflows/source-ir/result.js +26 -0
  313. package/dist/workflows/source-ir/schema.js +772 -0
  314. package/dist/workflows/source-ir/semantics.js +242 -0
  315. package/dist/workflows/source-ir/uses.js +14 -0
  316. package/docs/README.md +2 -0
  317. package/docs/migration/README.md +3 -1
  318. package/docs/migration/release-notes/0.9.2.md +55 -0
  319. package/docs/migration/release-notes/README.md +5 -0
  320. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  321. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  322. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  323. package/docs/reference/README.md +1 -0
  324. package/docs/reference/cli.md +230 -98
  325. package/docs/reference/configuration.md +153 -27
  326. package/docs/reference/data-and-telemetry.md +19 -1
  327. package/docs/reference/supported-formats.md +23 -3
  328. package/docs/reference/tasks.md +182 -0
  329. package/docs/reference/workflow-schema.md +91 -40
  330. package/docs/reference/workflows.md +33 -6
  331. package/package.json +10 -6
  332. package/schemas/akm-config.json +372 -216
  333. package/schemas/akm-task.json +324 -80
  334. package/schemas/akm-workflow.json +6 -9
  335. package/dist/core/migration-operation.js +0 -75
  336. package/dist/integrations/agent/model-aliases.js +0 -74
  337. package/dist/tasks/parser.js +0 -380
  338. package/dist/tasks/schema.js +0 -123
  339. package/dist/tasks/validator.js +0 -80
  340. package/dist/workflows/ir/freeze.js +0 -320
  341. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -1,14 +1,27 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Database-backed (SQLite + FTS5/vector) source search implementation.
6
+ *
7
+ * Extracted from source-search.ts to break the circular import:
8
+ * source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
9
+ *
10
+ * source-search.ts imports this module for the `searchLocal` export.
11
+ * sources/providers/filesystem.ts also imports `searchLocal` from here.
12
+ *
13
+ * Renamed from `local-search.ts` to signal that this is the DB-layer search
14
+ * implementation, not a "local vs. remote" distinction.
15
+ */
4
16
  import path from "node:path";
5
17
  import { buildActionFromContributors, defaultActionContributors } from "../../core/action-contributors.js";
6
18
  import { stashDirFor } from "../../core/asset/asset-placement.js";
7
19
  import { displayRef } from "../../core/asset/resolve-ref.js";
8
20
  import { classifyPathAccess } from "../../core/path-access.js";
9
21
  import { getDbPath } from "../../core/paths.js";
22
+ import { systemErrorCode } from "../../core/system-error.js";
10
23
  import { defaultRendererRegistry } from "../../core/type-presentation.js";
11
- import { warn } from "../../core/warn.js";
24
+ import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
12
25
  import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from "../../storage/repositories/index-connection.js";
13
26
  import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
14
27
  import { searchFts } from "../../storage/repositories/index-fts-repository.js";
@@ -188,7 +201,9 @@ export async function searchLocal(input) {
188
201
  const staleHint = buildStaleIndexHint(db);
189
202
  if (staleHint)
190
203
  warnings.push(staleHint);
191
- const { hits, embedMs, rankMs, usedSemantic } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
204
+ const { hits, embedMs, rankMs, mode, semanticWarning } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
205
+ if (semanticWarning)
206
+ warnings.push(semanticWarning);
192
207
  return {
193
208
  hits,
194
209
  tip: hits.length === 0 ? emptyResultTip(query) : undefined,
@@ -197,7 +212,7 @@ export async function searchLocal(input) {
197
212
  rankMs,
198
213
  // Report the mode the search ACTUALLY used, carried explicitly from the
199
214
  // vector scorer — not inferred from elapsed embedding milliseconds.
200
- mode: usedSemantic ? "semantic" : "keyword",
215
+ mode,
201
216
  };
202
217
  }
203
218
  finally {
@@ -246,7 +261,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
246
261
  conceptIdPrefix: refPrefix.conceptIdPrefix,
247
262
  ...(refPrefix.bundle !== undefined ? { bundle: refPrefix.bundle } : {}),
248
263
  })),
249
- usedSemantic: false,
264
+ mode: "keyword",
250
265
  };
251
266
  }
252
267
  // Empty queries — including ones that sanitize down to no searchable FTS
@@ -260,23 +275,13 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
260
275
  typeFilter: searchType === "any" ? undefined : searchType,
261
276
  excludeTypes: defaultExcludes,
262
277
  })),
263
- usedSemantic: false,
278
+ mode: "keyword",
264
279
  };
265
280
  }
266
281
  // Start the async embedding request without awaiting, then run FTS
267
282
  // synchronously while the HTTP/local embedding request is in-flight.
268
283
  const typeFilter = searchType === "any" ? undefined : searchType;
269
- const tEmbed0 = Date.now();
270
- const embeddingPromise = tryVecScores(db, query, limit * 3, config);
271
- const ftsResults = searchFts(db, query, limit * 3, typeFilter, defaultExcludes);
272
- const embeddingScores = await embeddingPromise;
273
- const embedMs = Date.now() - tEmbed0;
274
- // The vector scorer returns a (possibly empty) Map when the embedding + vector
275
- // search actually executed, or null when semantic was not runnable (disabled,
276
- // no embeddings, or the embed call threw). This is the AUTHORITATIVE "semantic
277
- // mode was used" signal — carried out to telemetry instead of guessing from
278
- // elapsed milliseconds (which timed the concurrent FTS work too).
279
- const usedSemantic = embeddingScores !== null;
284
+ const { ftsResults, embeddingScores, embedMs, mode, semanticWarning } = await collectSearchSignals(db, query, limit * 3, typeFilter, defaultExcludes, config);
280
285
  const tRank0 = Date.now();
281
286
  // ── Score normalization ──────────────────────────────────────────────
282
287
  // Normalized BM25 + cosine similarity with weighted addition
@@ -437,7 +442,25 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
437
442
  db,
438
443
  });
439
444
  }));
440
- return { embedMs, rankMs, hits, usedSemantic };
445
+ return { embedMs, rankMs, hits, mode, semanticWarning };
446
+ }
447
+ async function collectSearchSignals(db, query, candidateLimit, typeFilter, excludeTypes, config) {
448
+ const startedAt = Date.now();
449
+ const embeddingPromise = tryVecScores(db, query, candidateLimit, config);
450
+ const ftsResults = searchFts(db, query, candidateLimit, typeFilter, excludeTypes);
451
+ const embeddingResult = await embeddingPromise;
452
+ const mode = embeddingResult.warning
453
+ ? "fts-fallback"
454
+ : embeddingResult.scores !== null
455
+ ? "semantic"
456
+ : "keyword";
457
+ return {
458
+ ftsResults,
459
+ embeddingScores: embeddingResult.scores,
460
+ embedMs: Date.now() - startedAt,
461
+ mode,
462
+ semanticWarning: embeddingResult.warning,
463
+ };
441
464
  }
442
465
  /**
443
466
  * The no-hits tip. A query in the retired `<type>:` / `<type>:<prefix>/` browse
@@ -623,10 +646,10 @@ function matchBeliefFilter(beliefState, filter) {
623
646
  async function tryVecScores(db, query, k, config) {
624
647
  const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
625
648
  if (!isSemanticRuntimeReady(semanticStatus))
626
- return null;
649
+ return { scores: null };
627
650
  const hasEmbeddings = getMeta(db, "hasEmbeddings");
628
651
  if (hasEmbeddings !== "1")
629
- return null;
652
+ return { scores: null };
630
653
  try {
631
654
  const { embed } = await import("../../llm/embedder.js");
632
655
  const queryEmbedding = await embed(query, config.embedding);
@@ -638,12 +661,64 @@ async function tryVecScores(db, query, k, config) {
638
661
  const raw = 1 - (distance * distance) / 2;
639
662
  scores.set(id, Number.isFinite(raw) ? Math.max(0, raw) : 0);
640
663
  }
641
- return scores;
664
+ return { scores };
642
665
  }
643
666
  catch (error) {
644
- warn("Vector search failed, skipping:", error instanceof Error ? error.message : String(error));
645
- return null;
667
+ return { scores: null, warning: buildVectorFallbackWarning(config, error) };
668
+ }
669
+ }
670
+ function buildVectorFallbackWarning(config, error) {
671
+ const endpoint = safeEmbeddingEndpoint(config);
672
+ const reason = classifyVectorFailure(error);
673
+ const target = endpoint
674
+ ? `embedding endpoint ${endpoint}`
675
+ : config.embedding?.endpoint
676
+ ? "configured embedding endpoint"
677
+ : "local embedding model";
678
+ const unavailable = reason === "connection failed" ? `cannot reach ${target}` : `${target} is unavailable`;
679
+ return `Vector search unavailable: ${unavailable} (${reason}) — falling back to keyword search.`;
680
+ }
681
+ /**
682
+ * Name the useful endpoint without ever carrying URL userinfo, query secrets,
683
+ * or fragments into a warning. Invalid authored values fail closed.
684
+ */
685
+ function safeEmbeddingEndpoint(config) {
686
+ const endpoint = config.embedding?.endpoint;
687
+ if (!endpoint)
688
+ return undefined;
689
+ try {
690
+ const parsed = new URL(normalizeEmbeddingEndpoint(endpoint));
691
+ parsed.username = "";
692
+ parsed.password = "";
693
+ parsed.search = "";
694
+ parsed.hash = "";
695
+ return parsed.toString();
696
+ }
697
+ catch {
698
+ return undefined;
699
+ }
700
+ }
701
+ /** Map untrusted runtime failures onto a small, non-secret diagnostic set. */
702
+ function classifyVectorFailure(error) {
703
+ const code = systemErrorCode(error);
704
+ const message = error instanceof Error ? error.message : "";
705
+ if (code === "ECONNREFUSED" ||
706
+ code === "ECONNRESET" ||
707
+ code === "ENETUNREACH" ||
708
+ code === "EHOSTUNREACH" ||
709
+ code === "ENOTFOUND" ||
710
+ code === "EAI_AGAIN" ||
711
+ /typo in the url or port|connection (?:refused|failed|reset)|fetch failed/i.test(message)) {
712
+ return "connection failed";
646
713
  }
714
+ if (code === "ETIMEDOUT" || /timed? out|timeout/i.test(message))
715
+ return "request timed out";
716
+ const httpStatus = message.match(/Embedding (?:batch )?request failed \((\d{3})\)/i)?.[1];
717
+ if (httpStatus)
718
+ return `HTTP ${httpStatus}`;
719
+ if (/unexpected embedding response|missing data\[0\]\.embedding/i.test(message))
720
+ return "invalid embedding response";
721
+ return "request failed";
647
722
  }
648
723
  // ── Hit building ────────────────────────────────────────────────────────────
649
724
  export async function buildDbHit(input) {
@@ -703,6 +778,7 @@ export async function buildDbHit(input) {
703
778
  await enrichSearchHit(hit, {
704
779
  type: input.entry.type,
705
780
  stashDir: entryStashDir,
781
+ bundleId: input.bundleId,
706
782
  rendererRegistry,
707
783
  db: input.db,
708
784
  });
@@ -70,7 +70,7 @@ export function buildSearchFields(entry) {
70
70
  // folds into the lowest-weight catch-all column — never name/description/
71
71
  // tags/hints — so orientation prose is retrievable without outranking
72
72
  // structured-field matches. The fold is unconditional on the entry field:
73
- // `rebuildFts` rebuilds FTS rows from stored entry_json and must reproduce
73
+ // `rebuildFts` rebuilds FTS rows from stored document_json and must reproduce
74
74
  // the same fields without re-reading config.
75
75
  if (entry.bodyOpening)
76
76
  contentParts.push(entry.bodyOpening);
@@ -53,7 +53,7 @@ export const derivedMemoryEnricher = {
53
53
  // column now stores this same conceptId grammar (Group-C item 2 flip — the
54
54
  // metadata producer + this consumer move together).
55
55
  const parentRef = `memories/${hit.name}`;
56
- const derived = getDerivedForParent(ctx.db, parentRef, ctx.stashDir);
56
+ const derived = getDerivedForParent(ctx.db, parentRef, ctx.bundleId);
57
57
  if (!derived)
58
58
  return;
59
59
  // Swap description / searchHints / tags from the derived child.
@@ -3,12 +3,12 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { isSourceWriteActivated } from "../../core/activation-policy.js";
7
6
  import { isWithin, resolveStashDir } from "../../core/common.js";
8
7
  import { bundleComponentConfig, bundlesToSourceEntries, getSources, loadConfig } from "../../core/config/config.js";
9
8
  import { resolveGitContentRoot, resolveWritable } from "../../core/write-source.js";
10
9
  import { lockContentRootFor } from "../../integrations/lockfile.js";
11
10
  import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
11
+ import { ensureWebsiteMirror } from "../../sources/snapshot-fetchers/website-ingest.js";
12
12
  // Eager side-effect imports so all built-in source providers self-register
13
13
  // before resolveEntryContentDir() runs.
14
14
  import "../../sources/providers/index.js";
@@ -22,9 +22,8 @@ import { warn } from "../../core/warn.js";
22
22
  * 2. The configured `defaultBundle`, after component-root validation.
23
23
  * 3. Remaining configured bundles in installation-priority order.
24
24
  *
25
- * Replaces the previous four-pass loop that walked `stashes[]` separately
26
- * for each provider kind. Disabled entries (`enabled: false`) are filtered
27
- * after deduplication. Missing configured roots remain in the result so the
25
+ * Disabled entries (`enabled: false`) are filtered after deduplication.
26
+ * Missing configured roots remain in the result so the
28
27
  * indexer can classify their scan as incomplete instead of mistaking them for
29
28
  * removed sources.
30
29
  */
@@ -126,8 +125,8 @@ export function resolveEntryContentDir(entry) {
126
125
  // and writes agree on where content is); the localRoot is the already-walkable
127
126
  // content root (installed sources are extracted to their content dir), so no
128
127
  // content/-subdir step is applied. Fall back to the provider path logic when no
129
- // lock entry exists (e.g. a git bundle migrated from a `sources[]` url, whose
130
- // provider re-derives the mirror path).
128
+ // lock entry exists (for example while a configured source is being
129
+ // materialized for the first time).
131
130
  const localRoot = lockContentRootFor(entry.name, entry.type);
132
131
  if (localRoot != null)
133
132
  return localRoot;
@@ -165,14 +164,6 @@ export function resolveEntryContentDir(entry) {
165
164
  export function resolveAllStashDirs(overrideStashDir) {
166
165
  return resolveSourceEntries(overrideStashDir).map((s) => s.path);
167
166
  }
168
- /**
169
- * Return the resolved absolute paths of all writable stash sources.
170
- */
171
- export function getWritableStashDirs(overrideStashDir, existingConfig) {
172
- return resolveSourceEntries(overrideStashDir, existingConfig)
173
- .filter((s) => isSourceWriteActivated(s))
174
- .map((s) => s.path);
175
- }
176
167
  /**
177
168
  * Find which source a file path belongs to.
178
169
  *
@@ -263,7 +254,7 @@ export async function ensureSourceCaches(config, options) {
263
254
  // provider and call `sync()`. Every cache-backed kind (git, website, npm)
264
255
  // refreshes the same way — a bad source warns and is skipped without
265
256
  // aborting the others. The git content/-subdir layout convention stays in
266
- // resolveEntryContentDir. NEW shape reads `bundles`; old shape reads sources[].
257
+ // resolveEntryContentDir. Provider projection comes only from `bundles`.
267
258
  //
268
259
  // DISTINCTION (deliberately NOT lock-first): refresh derives the PROVIDER's
269
260
  // own cache path to git-pull/re-materialize INTO — that derived path is where
@@ -304,7 +295,7 @@ export async function ensureSourceCaches(config, options) {
304
295
  continue;
305
296
  }
306
297
  try {
307
- await provider.sync({ force, secrets: options?.secrets });
298
+ await provider.sync({ force, secrets: options?.secrets, ensureWebsiteMirror });
308
299
  }
309
300
  catch (err) {
310
301
  warn(`Warning: failed to refresh ${provider.kind} source "${provider.name}": ${err instanceof Error ? err.message : String(err)}`);
@@ -6,6 +6,11 @@ import { writeFileAtomic } from "../../core/common.js";
6
6
  import { getCacheDir, getSemanticStatusPath } from "../../core/paths.js";
7
7
  import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../../llm/embedders/deterministic.js";
8
8
  import { DEFAULT_LOCAL_MODEL } from "../../llm/embedders/local.js";
9
+ let semanticStatusMutationForTests;
10
+ /** TEST-ONLY. Inject a semantic-status filesystem publication fault. */
11
+ export function _setSemanticStatusMutationForTests(mutation) {
12
+ semanticStatusMutationForTests = mutation;
13
+ }
9
14
  export function deriveSemanticProviderFingerprint(embedding) {
10
15
  if (isDeterministicEmbedEnabled()) {
11
16
  return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
@@ -49,11 +54,13 @@ export function readSemanticStatus() {
49
54
  return undefined;
50
55
  }
51
56
  export function writeSemanticStatus(status) {
57
+ semanticStatusMutationForTests?.("write");
52
58
  const dir = getCacheDir();
53
59
  fs.mkdirSync(dir, { recursive: true });
54
60
  writeFileAtomic(getSemanticStatusPath(), `${JSON.stringify(status, null, 2)}\n`);
55
61
  }
56
62
  export function clearSemanticStatus() {
63
+ semanticStatusMutationForTests?.("clear");
57
64
  try {
58
65
  fs.unlinkSync(getSemanticStatusPath());
59
66
  }
@@ -110,7 +117,9 @@ export function classifySemanticFailure(message) {
110
117
  if (lower.includes("404") || lower.includes("model not found") || lower.includes("bad request")) {
111
118
  return "remote-model";
112
119
  }
113
- if (lower.includes("transformers") || lower.includes("missing-package")) {
120
+ if (lower.includes("transformers") ||
121
+ lower.includes("missing-package") ||
122
+ lower.includes("local embedding dependency is unavailable")) {
114
123
  return "missing-package";
115
124
  }
116
125
  if (lower.includes("download")) {
@@ -0,0 +1,105 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
5
+ import { rethrowIfTestIsolationError } from "../../core/errors.js";
6
+ import { appendEvent, readEvents } from "../../core/events.js";
7
+ import { withStateDbTelemetry } from "../../core/state-db.js";
8
+ import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
9
+ import { findEntryIdByRef, getEntryById, getEntryIdByFilePath, getItemRefById, } from "../../storage/repositories/index-entries-repository.js";
10
+ import { usageEventAttributionMetadata } from "../search/search-attribution.js";
11
+ import { insertUsageEvent } from "./usage-events.js";
12
+ /** Count prior show events in the current one-hour loop-detection window. */
13
+ export function recentShowCount(ref) {
14
+ try {
15
+ return readEvents({
16
+ type: "show",
17
+ ref,
18
+ since: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
19
+ }).events.length;
20
+ }
21
+ catch {
22
+ return 0;
23
+ }
24
+ }
25
+ function appendShowTrace(ref, type, name) {
26
+ appendEvent({ eventType: "show", ref, metadata: { type, name } });
27
+ try {
28
+ const { events: recentSearches } = readEvents({
29
+ type: "search",
30
+ since: new Date(Date.now() - 60_000).toISOString(),
31
+ });
32
+ const cutoffMs = Date.now() - 60_000;
33
+ const matchingSearch = [...recentSearches].reverse().find((event) => {
34
+ if (!event.ts || new Date(event.ts).getTime() < cutoffMs)
35
+ return false;
36
+ const refs = event.metadata?.resultRefs ?? [];
37
+ return refs.includes(ref);
38
+ });
39
+ if (!matchingSearch)
40
+ return;
41
+ const resultRefs = matchingSearch.metadata?.resultRefs ?? [];
42
+ appendEvent({
43
+ eventType: "select",
44
+ ref,
45
+ metadata: {
46
+ query: matchingSearch.metadata?.query,
47
+ searchTs: matchingSearch.ts,
48
+ rankPosition: resultRefs.indexOf(ref),
49
+ },
50
+ });
51
+ }
52
+ catch {
53
+ // Selection attribution is best-effort.
54
+ }
55
+ }
56
+ function insertShowUsage(entryId, entryRef, derivedFrom, eventSource) {
57
+ withStateDbTelemetry((stateDb) => {
58
+ insertUsageEvent(stateDb, {
59
+ event_type: "show",
60
+ entry_ref: entryRef,
61
+ entry_id: entryId,
62
+ metadata: usageEventAttributionMetadata(derivedFrom ? { memoryInference: { exposure: "direct" } } : undefined, entryRef),
63
+ source: eventSource,
64
+ });
65
+ }, TELEMETRY_BUSY_TIMEOUT_MS);
66
+ }
67
+ /** Record a successful show using the already-rendered response identity. */
68
+ export function recordShowUsage(ref, type, name, eventSource = "user", filePath) {
69
+ const parsed = parseBundleRef(ref);
70
+ const eventRef = makeBundleRef(parsed.bundle, parsed.conceptId);
71
+ appendShowTrace(eventRef, type, name);
72
+ try {
73
+ withIndexDb((db) => {
74
+ const entryId = filePath ? getEntryIdByFilePath(db, filePath) : findEntryIdByRef(db, eventRef);
75
+ if (entryId === undefined)
76
+ return;
77
+ const entryRef = getItemRefById(db, entryId);
78
+ if (!entryRef)
79
+ return;
80
+ insertShowUsage(entryId, entryRef, getEntryById(db, entryId)?.entry.derivedFrom, eventSource);
81
+ }, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
82
+ }
83
+ catch (error) {
84
+ rethrowIfTestIsolationError(error);
85
+ }
86
+ }
87
+ /** Record a nested execution-source read only after dispatch has crossed preflight. */
88
+ export function recordIndexedShowUsage(ref, eventSource = "user") {
89
+ try {
90
+ withIndexDb((db) => {
91
+ const entryId = findEntryIdByRef(db, ref);
92
+ if (entryId === undefined)
93
+ return;
94
+ const indexed = getEntryById(db, entryId);
95
+ const entryRef = getItemRefById(db, entryId);
96
+ if (!indexed || !entryRef)
97
+ return;
98
+ appendShowTrace(entryRef, indexed.entry.type, indexed.entry.name);
99
+ insertShowUsage(entryId, entryRef, indexed.entry.derivedFrom, eventSource);
100
+ }, { busyTimeoutMs: TELEMETRY_BUSY_TIMEOUT_MS });
101
+ }
102
+ catch (error) {
103
+ rethrowIfTestIsolationError(error);
104
+ }
105
+ }
@@ -75,12 +75,17 @@ export function countUsageEventsByType(db, eventType) {
75
75
  /**
76
76
  * Delete usage events older than the given number of days.
77
77
  */
78
- export function purgeOldUsageEvents(db, retentionDays) {
78
+ export function purgeOldUsageEvents(db, retentionDays, options) {
79
79
  if (!Number.isFinite(retentionDays) || retentionDays <= 0)
80
80
  return;
81
81
  try {
82
+ const stateSchema = options?.stateSchema;
83
+ if (stateSchema !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(stateSchema)) {
84
+ throw new Error("Invalid attached state schema name.");
85
+ }
86
+ const usageEvents = stateSchema === undefined ? "usage_events" : `"${stateSchema}".usage_events`;
82
87
  const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
83
- db.prepare("DELETE FROM usage_events WHERE created_at < ?").run(cutoff);
88
+ db.prepare(`DELETE FROM ${usageEvents} WHERE created_at < ?`).run(cutoff);
84
89
  }
85
90
  catch {
86
91
  /* Table may not exist yet */
@@ -49,7 +49,7 @@ const DIR_TYPE_MAP = [
49
49
  {
50
50
  dir: "workflows",
51
51
  type: "workflow",
52
- test: (ext) => ext === ".md",
52
+ test: (ext) => ext === ".md" || ext === ".yml",
53
53
  },
54
54
  {
55
55
  dir: "memories",
@@ -102,6 +102,13 @@ const DIR_TYPE_MAP = [
102
102
  },
103
103
  ];
104
104
  const COMMAND_PLACEHOLDER_RE = /\$ARGUMENTS|\$[123]\b/;
105
+ const SMART_MD_FACTS = {
106
+ workflow: { type: "workflow", specificity: 19 },
107
+ toolsAgent: { type: "agent", specificity: 20 },
108
+ command: { type: "command", specificity: 18 },
109
+ modelAgent: { type: "agent", specificity: 8 },
110
+ knowledge: { type: "knowledge", specificity: 5 },
111
+ };
105
112
  // Files that should never be treated as the typed asset for the surrounding
106
113
  // directory (e.g. `workflows/README.md` is documentation, not a workflow).
107
114
  // Lower-cased and matched case-insensitively against `ctx.fileName`. They are
@@ -175,7 +182,7 @@ function classifyBySmartMd(ctx) {
175
182
  // body shape would otherwise classify (e.g. step-list inside a project
176
183
  // README under workflows/). Fall straight through to `knowledge`.
177
184
  if (isTypedDirDocFile(ctx.fileName)) {
178
- return { type: "knowledge", specificity: 5 };
185
+ return SMART_MD_FACTS.knowledge;
179
186
  }
180
187
  const body = ctx.content();
181
188
  const fm = ctx.frontmatter();
@@ -185,33 +192,34 @@ function classifyBySmartMd(ctx) {
185
192
  // this only catches a workflow living OUTSIDE `workflows/` that still
186
193
  // declares its type explicitly.
187
194
  if (fm && fm.type === "workflow") {
188
- return { type: "workflow", specificity: 19 };
195
+ return SMART_MD_FACTS.workflow;
189
196
  }
190
197
  if (fm) {
191
198
  // `tools` is the one authoring key for an agent's tool grant. Recognition
192
199
  // covers only the key the renderer honors.
193
200
  if ("tools" in fm) {
194
- return { type: "agent", specificity: 20 };
201
+ return SMART_MD_FACTS.toolsAgent;
195
202
  }
196
203
  if ("agent" in fm) {
197
- return { type: "command", specificity: 18 };
204
+ return SMART_MD_FACTS.command;
198
205
  }
199
206
  }
200
207
  if (COMMAND_PLACEHOLDER_RE.test(body)) {
201
- return { type: "command", specificity: 18 };
208
+ return SMART_MD_FACTS.command;
202
209
  }
203
210
  if (fm && "model" in fm) {
204
- return { type: "agent", specificity: 8 };
211
+ return SMART_MD_FACTS.modelAgent;
205
212
  }
206
- return { type: "knowledge", specificity: 5 };
213
+ return SMART_MD_FACTS.knowledge;
207
214
  }
208
215
  // ---------------------------------------------------------------------------
209
216
  // Adapter: MatchFact → MatchResult
210
217
  // ---------------------------------------------------------------------------
211
218
  function toMatchResult(ctx, classify) {
212
219
  const fact = classify(ctx);
213
- if (!fact)
214
- return null;
220
+ return fact ? matchResultForFact(fact) : null;
221
+ }
222
+ function matchResultForFact(fact) {
215
223
  // Renderer name resolved via TYPE_PRESENTATION (core leaf), so matchers.ts
216
224
  // carries no edge into the taxonomy SCC (chunk-3 cutover enabler).
217
225
  // TYPE_PRESENTATION.renderer is the single source of truth for renderer names.
@@ -225,6 +233,28 @@ function toMatchResult(ctx, classify) {
225
233
  ...(fact.meta ? { meta: fact.meta } : {}),
226
234
  };
227
235
  }
236
+ /**
237
+ * Every smart-Markdown result possible from path fields alone. The actual
238
+ * classifier above returns only facts from this shared table, so owner
239
+ * discovery can conservatively model a byte request without reading bytes.
240
+ */
241
+ export function smartMdPathCandidates(ctx) {
242
+ if (ctx.ext !== ".md" || ctx.ancestorDirs.includes("secrets"))
243
+ return [];
244
+ const facts = isTypedDirDocFile(ctx.fileName)
245
+ ? [SMART_MD_FACTS.knowledge]
246
+ : [
247
+ SMART_MD_FACTS.workflow,
248
+ SMART_MD_FACTS.toolsAgent,
249
+ SMART_MD_FACTS.command,
250
+ SMART_MD_FACTS.modelAgent,
251
+ SMART_MD_FACTS.knowledge,
252
+ ];
253
+ return facts.flatMap((fact) => {
254
+ const result = matchResultForFact(fact);
255
+ return result ? [result] : [];
256
+ });
257
+ }
228
258
  // ---------------------------------------------------------------------------
229
259
  // Public matchers (API unchanged)
230
260
  // ---------------------------------------------------------------------------
@@ -8,6 +8,7 @@ import { parseRefInput } from "../../core/asset/resolve-ref.js";
8
8
  import { isWithin } from "../../core/common.js";
9
9
  import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
10
10
  import { lookup } from "../indexer.js";
11
+ import { deriveInstallations } from "../installations.js";
11
12
  import { resolveSourceEntries } from "../search/search-source.js";
12
13
  function normalizeRef(ref) {
13
14
  // String inputs use the current `[bundle//]conceptId` grammar.
@@ -65,8 +66,10 @@ function resolveViaDisk(ref, options) {
65
66
  }
66
67
  const directoryIndexNames = options.directoryIndexNames ?? ["SKILL.md"];
67
68
  const preserveDirectNameFallback = options.preserveDirectNameFallback ?? true;
68
- for (const source of sources) {
69
- if (options.writableDirSet && !options.writableDirSet.has(path.resolve(source.path)))
69
+ const installations = deriveInstallations(sources);
70
+ for (const [index, source] of sources.entries()) {
71
+ const bundleId = installations[index]?.id;
72
+ if (options.writableBundleIds && (!bundleId || !options.writableBundleIds.has(bundleId)))
70
73
  continue;
71
74
  const candidates = buildDiskCandidates(source.path, ref, preserveDirectNameFallback);
72
75
  for (const candidate of candidates) {
@@ -11,6 +11,7 @@
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { isRelevantAssetFile } from "../../core/asset/asset-placement.js";
14
+ import { WORKFLOW_EXTENSIONS } from "../../core/recognition-util.js";
14
15
  import { spawnSync } from "../../runtime.js";
15
16
  import { buildFileContext } from "./file-context.js";
16
17
  const ALWAYS_SKIP_DIRS = new Set([".git"]);
@@ -128,7 +129,8 @@ function walkStashGit(stashRoot, options) {
128
129
  // indexed. The manual walk below already skips symlinks for exactly this
129
130
  // reason; the two walkers have to agree regardless of whether the stash
130
131
  // happens to sit inside a git repo.
131
- if (fs.lstatSync(absPath).isFile()) {
132
+ const stat = fs.lstatSync(absPath);
133
+ if (stat.isFile() || (stat.isSymbolicLink() && isOwnedWorkflowSymlink(stashRoot, absPath, options))) {
132
134
  results.push(buildFileContext(stashRoot, absPath));
133
135
  }
134
136
  }
@@ -219,7 +221,11 @@ function walkStashManual(stashRoot, options) {
219
221
  continue;
220
222
  const fullPath = path.join(current, entry.name);
221
223
  if (entry.isSymbolicLink()) {
222
- // Skip symlinks entirely to prevent potential path traversal outside stashRoot
224
+ // Native workflow components arbitrate file symlinks explicitly in the
225
+ // drain before reading them. Directory symlinks remain unfollowed.
226
+ if (isOwnedWorkflowSymlink(stashRoot, fullPath, options)) {
227
+ results.push(buildFileContext(stashRoot, fullPath));
228
+ }
223
229
  continue;
224
230
  }
225
231
  if (entry.isDirectory()) {
@@ -235,3 +241,15 @@ function walkStashManual(stashRoot, options) {
235
241
  }
236
242
  return { files: results, complete };
237
243
  }
244
+ function isOwnedWorkflowSymlink(stashRoot, candidatePath, options) {
245
+ const adapterId = options.workflowSymlinkAdapter;
246
+ if (!adapterId)
247
+ return false;
248
+ const relativePath = path.relative(path.resolve(stashRoot), path.resolve(candidatePath)).replaceAll("\\", "/");
249
+ if (!relativePath || relativePath.startsWith("../") || path.posix.isAbsolute(relativePath))
250
+ return false;
251
+ if (adapterId === "akm" && !relativePath.startsWith("workflows/"))
252
+ return false;
253
+ const extension = path.posix.extname(relativePath).toLowerCase();
254
+ return WORKFLOW_EXTENSIONS.includes(extension);
255
+ }
@@ -14,12 +14,9 @@
14
14
  * their previous home in `builders.ts`.
15
15
  */
16
16
  import { UsageError } from "../../core/errors.js";
17
- import { resolveModel } from "./model-aliases.js";
18
- /** Resolve a raw dispatch model once, while preserving frozen/lowered models verbatim. */
19
- export function resolveDispatchModel(request, profile, platform) {
20
- if (!request.model || request.modelIsExact)
21
- return request.model;
22
- return resolveModel(request.model, platform, profile.modelAliases, profile.globalModelAliases);
17
+ /** Models reaching a harness have already crossed the single model-map boundary. */
18
+ export function resolveDispatchModel(request, _profile, _platform) {
19
+ return request.model;
23
20
  }
24
21
  /**
25
22
  * Guard against values that start with `--`, which would be mis-interpreted as
@@ -0,0 +1,16 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ export const CONVERSATION_FALLBACK_BEGIN = "<AKM_CONVERSATION_JSON>";
5
+ export const CONVERSATION_FALLBACK_END = "</AKM_CONVERSATION_JSON>";
6
+ /**
7
+ * Preserve ordered message roles for a one-shot transport with no native
8
+ * conversation channel. The fixed `{role,content}` projection and UTF-8 byte
9
+ * length make the embedded JSON unambiguous even when content contains marker
10
+ * text. The terminal command remains outside the prefix block.
11
+ */
12
+ export function composeConversationFallbackPrompt(conversation, command) {
13
+ const json = JSON.stringify(conversation.map((message) => ({ role: message.role, content: message.content })));
14
+ const byteLength = new TextEncoder().encode(json).byteLength;
15
+ return `${CONVERSATION_FALLBACK_BEGIN} ${byteLength}\n${json}\n${CONVERSATION_FALLBACK_END}\n\n${command}`;
16
+ }