create-egregore 0.20.2 → 0.21.0-runtime-mvp.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (706) hide show
  1. package/assets/egregore-launcher.js +1013 -16
  2. package/bin/cli.js +11 -0
  3. package/lib/claude-runtime.js +192 -0
  4. package/lib/local.js +38 -31
  5. package/lib/pi-runtime.js +1 -0
  6. package/lib/prime-runtime.js +1 -0
  7. package/lib/runtime-identity.js +146 -0
  8. package/lib/setup.js +116 -17
  9. package/package.json +10 -6
  10. package/runtime/claude/.claude/context/commit-format.md +110 -0
  11. package/runtime/claude/.claude/context/git-language.md +46 -0
  12. package/runtime/claude/.claude/context/notification-consent.md +66 -0
  13. package/runtime/claude/.claude/context/pr-format.md +67 -0
  14. package/runtime/claude/.claude/context/telemetry.md +133 -0
  15. package/runtime/claude/.claude/hooks/boundary-check.sh +333 -0
  16. package/runtime/claude/.claude/hooks/branch-guard.sh +231 -0
  17. package/runtime/claude/.claude/hooks/card-context.sh +38 -0
  18. package/runtime/claude/.claude/hooks/emissary-detect.sh +53 -0
  19. package/runtime/claude/.claude/hooks/memory-refresh.sh +67 -0
  20. package/runtime/claude/.claude/hooks/onboarding-guard.sh +86 -0
  21. package/runtime/claude/.claude/hooks/retrieval-context.sh +43 -0
  22. package/runtime/claude/.claude/rules/voice-bedrock.md +73 -0
  23. package/runtime/claude/.claude/settings.json +131 -0
  24. package/runtime/claude/.claude/skills/activity/SKILL.md +68 -0
  25. package/runtime/claude/.claude/skills/announce/SKILL.md +54 -0
  26. package/runtime/claude/.claude/skills/archive/SKILL.md +71 -0
  27. package/runtime/claude/.claude/skills/branch/SKILL.md +147 -0
  28. package/runtime/claude/.claude/skills/checkup/SKILL.md +159 -0
  29. package/runtime/claude/.claude/skills/commit/SKILL.md +103 -0
  30. package/runtime/claude/.claude/skills/contribute/SKILL.md +343 -0
  31. package/runtime/claude/.claude/skills/create-skill/SKILL.md +135 -0
  32. package/runtime/claude/.claude/skills/dark-mode/SKILL.md +410 -0
  33. package/runtime/claude/.claude/skills/dashboard/SKILL.md +43 -0
  34. package/runtime/claude/.claude/skills/deep-reflect/REPORT.md +140 -0
  35. package/runtime/claude/.claude/skills/deep-reflect/RESEARCH.md +132 -0
  36. package/runtime/claude/.claude/skills/deep-reflect/ROLES.md +104 -0
  37. package/runtime/claude/.claude/skills/deep-reflect/SKILL.md +170 -0
  38. package/runtime/claude/.claude/skills/delete-user/SKILL.md +150 -0
  39. package/runtime/claude/.claude/skills/emissary/SKILL.md +220 -0
  40. package/runtime/claude/.claude/skills/handoff/SKILL.md +124 -0
  41. package/runtime/claude/.claude/skills/harvest/AUDIT.md +546 -0
  42. package/runtime/claude/.claude/skills/harvest/FORMAT.md +436 -0
  43. package/runtime/claude/.claude/skills/harvest/PROCESS.md +395 -0
  44. package/runtime/claude/.claude/skills/harvest/QUESTION_PALETTE.md +76 -0
  45. package/runtime/claude/.claude/skills/harvest/SKILL.md +100 -0
  46. package/runtime/claude/.claude/skills/ingest/SKILL.md +112 -0
  47. package/runtime/claude/.claude/skills/ingest-corpus/SKILL.md +187 -0
  48. package/runtime/claude/.claude/skills/ingest-notion/SKILL.md +214 -0
  49. package/runtime/claude/.claude/skills/ingest-user-interview/SKILL.md +65 -0
  50. package/runtime/claude/.claude/skills/ingest-user-interview/references/analysis-contract.md +125 -0
  51. package/runtime/claude/.claude/skills/invite/SKILL.md +111 -0
  52. package/runtime/claude/.claude/skills/issue/SKILL.md +104 -0
  53. package/runtime/claude/.claude/skills/me/SKILL.md +81 -0
  54. package/runtime/claude/.claude/skills/meeting/SKILL.md +76 -0
  55. package/runtime/claude/.claude/skills/meeting/references/analysis-contract.md +102 -0
  56. package/runtime/claude/.claude/skills/note/SKILL.md +71 -0
  57. package/runtime/claude/.claude/skills/notion-connect/SKILL.md +124 -0
  58. package/runtime/claude/.claude/skills/onboarding/SKILL.md +159 -0
  59. package/runtime/claude/.claude/skills/pr/SKILL.md +85 -0
  60. package/runtime/claude/.claude/skills/pull/SKILL.md +79 -0
  61. package/runtime/claude/.claude/skills/push/SKILL.md +47 -0
  62. package/runtime/claude/.claude/skills/quest/SKILL.md +121 -0
  63. package/runtime/claude/.claude/skills/reflect/SKILL.md +69 -0
  64. package/runtime/claude/.claude/skills/review-pr/SKILL.md +276 -0
  65. package/runtime/claude/.claude/skills/save/SKILL.md +92 -0
  66. package/runtime/claude/.claude/skills/slack-connect/SKILL.md +148 -0
  67. package/runtime/claude/.claude/skills/summon/SKILL.md +240 -0
  68. package/runtime/claude/.claude/skills/sync-repos/SKILL.md +82 -0
  69. package/runtime/claude/.claude/skills/teams-connect/SKILL.md +216 -0
  70. package/runtime/claude/.claude/skills/telegram-connect/SKILL.md +110 -0
  71. package/runtime/claude/.claude/skills/telemetry/SKILL.md +117 -0
  72. package/runtime/claude/.claude/skills/test/SKILL.md +87 -0
  73. package/runtime/claude/.claude/skills/the-spiral/SKILL.md +492 -0
  74. package/runtime/claude/.claude/skills/tui-design/SKILL.md +188 -0
  75. package/runtime/claude/.claude/skills/tutorial/SKILL.md +100 -0
  76. package/runtime/claude/.claude/skills/update/SKILL.md +265 -0
  77. package/runtime/claude/.claude/skills/view/SKILL.md +393 -0
  78. package/runtime/claude/.claude/skills/view/compose-scaffold.html +221 -0
  79. package/runtime/claude/.claude/skills/visual-explain/SKILL.md +132 -0
  80. package/runtime/claude/.claude/skills/wrap/SKILL.md +98 -0
  81. package/runtime/claude/CLAUDE.md +447 -0
  82. package/runtime/claude/bin/activity-action.sh +92 -0
  83. package/runtime/claude/bin/activity-data.sh +17 -0
  84. package/runtime/claude/bin/agent.sh +783 -0
  85. package/runtime/claude/bin/artifact-register.sh +164 -0
  86. package/runtime/claude/bin/artifact-writeback.sh +53 -0
  87. package/runtime/claude/bin/artifacts.sh +288 -0
  88. package/runtime/claude/bin/attendant.sh +707 -0
  89. package/runtime/claude/bin/autosave.sh +112 -0
  90. package/runtime/claude/bin/boundary.sh +253 -0
  91. package/runtime/claude/bin/build-sealed-dataroom.mjs +722 -0
  92. package/runtime/claude/bin/capture-reconcile.sh +69 -0
  93. package/runtime/claude/bin/capture-run.sh +357 -0
  94. package/runtime/claude/bin/codex-install-skills.sh +78 -0
  95. package/runtime/claude/bin/codex-render-spec.mjs +462 -0
  96. package/runtime/claude/bin/codex-session-start.sh +275 -0
  97. package/runtime/claude/bin/codex-skill-render.mjs +349 -0
  98. package/runtime/claude/bin/codex-sync-skills.sh +237 -0
  99. package/runtime/claude/bin/connect-refresh.sh +33 -0
  100. package/runtime/claude/bin/contribute-guard.sh +79 -0
  101. package/runtime/claude/bin/corpus_map.py +806 -0
  102. package/runtime/claude/bin/corpus_survey.py +304 -0
  103. package/runtime/claude/bin/dashboard-data.sh +18 -0
  104. package/runtime/claude/bin/github-auth.sh +121 -0
  105. package/runtime/claude/bin/graph-batch.sh +86 -0
  106. package/runtime/claude/bin/graph-op.sh +1106 -0
  107. package/runtime/claude/bin/graph-projection.sh +54 -0
  108. package/runtime/claude/bin/graph-wal.sh +242 -0
  109. package/runtime/claude/bin/graph.sh +267 -0
  110. package/runtime/claude/bin/handoff-lifecycle-job.sh +146 -0
  111. package/runtime/claude/bin/handoff-lifecycle.sh +280 -0
  112. package/runtime/claude/bin/handoff-pr-backfill.sh +86 -0
  113. package/runtime/claude/bin/handoff-preview.sh +169 -0
  114. package/runtime/claude/bin/handoff-run.sh +692 -0
  115. package/runtime/claude/bin/handoff-save-egregore.sh +224 -0
  116. package/runtime/claude/bin/index-handoff.sh +322 -0
  117. package/runtime/claude/bin/ingest-graph.sh +86 -0
  118. package/runtime/claude/bin/ingest-item.sh +10 -0
  119. package/runtime/claude/bin/ingest-surface.html +495 -0
  120. package/runtime/claude/bin/ingest.py +1011 -0
  121. package/runtime/claude/bin/ingest.sh +308 -0
  122. package/runtime/claude/bin/ingest_graph.py +856 -0
  123. package/runtime/claude/bin/ingest_surface.py +544 -0
  124. package/runtime/claude/bin/invite.sh +6 -0
  125. package/runtime/claude/bin/issue.sh +10 -0
  126. package/runtime/claude/bin/knowledge.sh +10 -0
  127. package/runtime/claude/bin/lib/artifact-id.sh +46 -0
  128. package/runtime/claude/bin/lib/config.sh +136 -0
  129. package/runtime/claude/bin/lib/context.sh +642 -0
  130. package/runtime/claude/bin/lib/dashboard-artifact.sh +242 -0
  131. package/runtime/claude/bin/lib/ensure-develop.sh +183 -0
  132. package/runtime/claude/bin/lib/git-message.sh +37 -0
  133. package/runtime/claude/bin/lib/git-sync.sh +494 -0
  134. package/runtime/claude/bin/lib/greeting.sh +548 -0
  135. package/runtime/claude/bin/lib/handoff-meta.sh +97 -0
  136. package/runtime/claude/bin/lib/hash.sh +13 -0
  137. package/runtime/claude/bin/lib/identity.sh +213 -0
  138. package/runtime/claude/bin/lib/metrics.sh +374 -0
  139. package/runtime/claude/bin/lib/noncode.sh +31 -0
  140. package/runtime/claude/bin/lib/permissions.sh +106 -0
  141. package/runtime/claude/bin/lib/time.sh +56 -0
  142. package/runtime/claude/bin/lib/worktree-links.sh +85 -0
  143. package/runtime/claude/bin/node-run.sh +42 -0
  144. package/runtime/claude/bin/notification.sh +6 -0
  145. package/runtime/claude/bin/notify.sh +636 -0
  146. package/runtime/claude/bin/observe-context.sh +14 -0
  147. package/runtime/claude/bin/onboarding.sh +6 -0
  148. package/runtime/claude/bin/person.py +634 -0
  149. package/runtime/claude/bin/person.sh +481 -0
  150. package/runtime/claude/bin/pi-render-spec.mjs +140 -0
  151. package/runtime/claude/bin/post-update.sh +37 -0
  152. package/runtime/claude/bin/prime-render-spec.mjs +147 -0
  153. package/runtime/claude/bin/publish-artifact.sh +324 -0
  154. package/runtime/claude/bin/publish-references.sh +144 -0
  155. package/runtime/claude/bin/quest.sh +9 -0
  156. package/runtime/claude/bin/question.sh +8 -0
  157. package/runtime/claude/bin/repo-state.sh +96 -0
  158. package/runtime/claude/bin/research-ingest.sh +7 -0
  159. package/runtime/claude/bin/restore-owned-skills.sh +67 -0
  160. package/runtime/claude/bin/retrieval_eval.py +385 -0
  161. package/runtime/claude/bin/runtime-identity.sh +70 -0
  162. package/runtime/claude/bin/runtime-upgrade.sh +43 -0
  163. package/runtime/claude/bin/scroll.sh +9 -0
  164. package/runtime/claude/bin/search.sh +318 -0
  165. package/runtime/claude/bin/session-autosave.sh +220 -0
  166. package/runtime/claude/bin/session-log.sh +199 -0
  167. package/runtime/claude/bin/session-start.sh +696 -0
  168. package/runtime/claude/bin/settings.sh +532 -0
  169. package/runtime/claude/bin/startup-check.sh +136 -0
  170. package/runtime/claude/bin/statusline.sh +41 -0
  171. package/runtime/claude/bin/telemetry.sh +666 -0
  172. package/runtime/claude/bin/test-changes.sh +344 -0
  173. package/runtime/claude/bin/thread.sh +9 -0
  174. package/runtime/claude/bin/todo.sh +9 -0
  175. package/runtime/claude/bin/view-render.sh +126 -0
  176. package/runtime/claude/bin/worktree-create.sh +145 -0
  177. package/runtime/claude/bin/worktree-remove.sh +50 -0
  178. package/runtime/claude/bin/worktree.sh +198 -0
  179. package/runtime/claude/egregore_runtime/__init__.py +228 -0
  180. package/runtime/claude/egregore_runtime/adapters/__init__.py +5 -0
  181. package/runtime/claude/egregore_runtime/adapters/invitation.py +237 -0
  182. package/runtime/claude/egregore_runtime/adapters/notification.py +59 -0
  183. package/runtime/claude/egregore_runtime/adapters/qmd.py +1701 -0
  184. package/runtime/claude/egregore_runtime/adapters/qmd_cli.py +210 -0
  185. package/runtime/claude/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
  186. package/runtime/claude/egregore_runtime/admin_gate.py +102 -0
  187. package/runtime/claude/egregore_runtime/artifacts.py +523 -0
  188. package/runtime/claude/egregore_runtime/contracts.py +499 -0
  189. package/runtime/claude/egregore_runtime/entitlements.py +85 -0
  190. package/runtime/claude/egregore_runtime/errors.py +10 -0
  191. package/runtime/claude/egregore_runtime/graph.py +356 -0
  192. package/runtime/claude/egregore_runtime/handoff_lookup.py +196 -0
  193. package/runtime/claude/egregore_runtime/harness_cli.py +1221 -0
  194. package/runtime/claude/egregore_runtime/identity.py +734 -0
  195. package/runtime/claude/egregore_runtime/identity_cli.py +91 -0
  196. package/runtime/claude/egregore_runtime/ingest.py +575 -0
  197. package/runtime/claude/egregore_runtime/ingest_cli.py +254 -0
  198. package/runtime/claude/egregore_runtime/ingest_index_cli.py +147 -0
  199. package/runtime/claude/egregore_runtime/invitation_cli.py +51 -0
  200. package/runtime/claude/egregore_runtime/invitations.py +116 -0
  201. package/runtime/claude/egregore_runtime/issue_cli.py +305 -0
  202. package/runtime/claude/egregore_runtime/issues.py +359 -0
  203. package/runtime/claude/egregore_runtime/knowledge.py +376 -0
  204. package/runtime/claude/egregore_runtime/knowledge_cli.py +173 -0
  205. package/runtime/claude/egregore_runtime/lifecycle.py +888 -0
  206. package/runtime/claude/egregore_runtime/lifecycle_cli.py +269 -0
  207. package/runtime/claude/egregore_runtime/notification_cli.py +125 -0
  208. package/runtime/claude/egregore_runtime/notifications.py +230 -0
  209. package/runtime/claude/egregore_runtime/observe.py +342 -0
  210. package/runtime/claude/egregore_runtime/onboarding.py +290 -0
  211. package/runtime/claude/egregore_runtime/onboarding_cli.py +110 -0
  212. package/runtime/claude/egregore_runtime/policy.py +144 -0
  213. package/runtime/claude/egregore_runtime/profile.py +193 -0
  214. package/runtime/claude/egregore_runtime/quest_cli.py +216 -0
  215. package/runtime/claude/egregore_runtime/question_cli.py +180 -0
  216. package/runtime/claude/egregore_runtime/questions.py +304 -0
  217. package/runtime/claude/egregore_runtime/quests.py +419 -0
  218. package/runtime/claude/egregore_runtime/readiness.py +379 -0
  219. package/runtime/claude/egregore_runtime/research_ingest.py +256 -0
  220. package/runtime/claude/egregore_runtime/research_ingest_cli.py +44 -0
  221. package/runtime/claude/egregore_runtime/runtime.py +464 -0
  222. package/runtime/claude/egregore_runtime/scroll_cli.py +185 -0
  223. package/runtime/claude/egregore_runtime/scrolls.py +726 -0
  224. package/runtime/claude/egregore_runtime/status.py +412 -0
  225. package/runtime/claude/egregore_runtime/status_cli.py +174 -0
  226. package/runtime/claude/egregore_runtime/sync.py +218 -0
  227. package/runtime/claude/egregore_runtime/teamsync.py +139 -0
  228. package/runtime/claude/egregore_runtime/telemetry.py +640 -0
  229. package/runtime/claude/egregore_runtime/thread_cli.py +173 -0
  230. package/runtime/claude/egregore_runtime/threads.py +517 -0
  231. package/runtime/claude/egregore_runtime/todo_cli.py +142 -0
  232. package/runtime/claude/egregore_runtime/todos.py +244 -0
  233. package/runtime/claude/egregore_runtime/upgrade.py +770 -0
  234. package/runtime/claude/egregore_runtime/upgrade_worker.py +53 -0
  235. package/runtime/claude/egregore_runtime/writeback.py +391 -0
  236. package/runtime/claude/egregore_runtime/writeback_cli.py +114 -0
  237. package/runtime/claude/manifest.json +284 -0
  238. package/runtime/codex/.codex/hooks/search-hint.js +21 -52
  239. package/runtime/codex/.codex/hooks.json +3 -3
  240. package/runtime/codex/.codex/skills/activity/SKILL.md +23 -66
  241. package/runtime/codex/.codex/skills/announce/SKILL.md +27 -50
  242. package/runtime/codex/.codex/skills/archive/SKILL.md +21 -36
  243. package/runtime/codex/.codex/skills/checkup/SKILL.md +77 -36
  244. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +1 -1
  245. package/runtime/codex/.codex/skills/dashboard/SKILL.md +17 -47
  246. package/runtime/codex/.codex/skills/handoff/SKILL.md +84 -133
  247. package/runtime/codex/.codex/skills/harvest/SKILL.md +32 -117
  248. package/runtime/codex/.codex/skills/ingest/SKILL.md +47 -25
  249. package/runtime/codex/.codex/skills/ingest-user-interview/SKILL.md +18 -36
  250. package/runtime/codex/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
  251. package/runtime/codex/.codex/skills/invite/SKILL.md +21 -35
  252. package/runtime/codex/.codex/skills/issue/SKILL.md +60 -41
  253. package/runtime/codex/.codex/skills/me/SKILL.md +1 -1
  254. package/runtime/codex/.codex/skills/meeting/SKILL.md +16 -36
  255. package/runtime/codex/.codex/skills/meeting/references/analysis-contract.md +48 -0
  256. package/runtime/codex/.codex/skills/note/SKILL.md +30 -25
  257. package/runtime/codex/.codex/skills/onboarding/SKILL.md +36 -41
  258. package/runtime/codex/.codex/skills/pull/SKILL.md +1 -1
  259. package/runtime/codex/.codex/skills/quest/SKILL.md +27 -66
  260. package/runtime/codex/.codex/skills/reflect/SKILL.md +22 -36
  261. package/runtime/codex/.codex/skills/save/SKILL.md +2 -2
  262. package/runtime/codex/.codex/skills/telemetry/SKILL.md +1 -1
  263. package/runtime/codex/.codex/skills/test/SKILL.md +1 -1
  264. package/runtime/codex/.codex/skills/tutorial/SKILL.md +40 -41
  265. package/runtime/codex/.codex/skills/view/SKILL.md +115 -111
  266. package/runtime/codex/.codex/skills/wrap/SKILL.md +45 -41
  267. package/runtime/codex/.codex/spec-manifest.json +9 -9
  268. package/runtime/codex/AGENTS.md +62 -58
  269. package/runtime/codex/bin/activity-action.sh +92 -0
  270. package/runtime/codex/bin/activity-data.sh +12 -233
  271. package/runtime/codex/bin/agent.sh +84 -100
  272. package/runtime/codex/bin/artifact-register.sh +164 -0
  273. package/runtime/codex/bin/artifact-writeback.sh +53 -0
  274. package/runtime/codex/bin/artifacts.sh +288 -0
  275. package/runtime/codex/bin/build-sealed-dataroom.mjs +722 -0
  276. package/runtime/codex/bin/capture-run.sh +18 -2
  277. package/runtime/codex/bin/codex-render-spec.mjs +41 -16
  278. package/runtime/codex/bin/codex-session-start.sh +14 -4
  279. package/runtime/codex/bin/codex-skill-render.mjs +9 -7
  280. package/runtime/codex/bin/corpus_map.py +806 -0
  281. package/runtime/codex/bin/corpus_survey.py +304 -0
  282. package/runtime/codex/bin/dashboard-data.sh +11 -171
  283. package/runtime/codex/bin/github-auth.sh +121 -0
  284. package/runtime/codex/bin/graph-batch.sh +7 -0
  285. package/runtime/codex/bin/graph-op.sh +4 -0
  286. package/runtime/codex/bin/graph-projection.sh +54 -0
  287. package/runtime/codex/bin/graph-wal.sh +13 -0
  288. package/runtime/codex/bin/graph.sh +44 -0
  289. package/runtime/codex/bin/handoff-lifecycle-job.sh +146 -0
  290. package/runtime/codex/bin/handoff-lifecycle.sh +280 -0
  291. package/runtime/codex/bin/handoff-preview.sh +169 -0
  292. package/runtime/codex/bin/handoff-run.sh +42 -26
  293. package/runtime/codex/bin/ingest-graph.sh +6 -0
  294. package/runtime/codex/bin/ingest-item.sh +10 -0
  295. package/runtime/codex/bin/ingest.sh +59 -30
  296. package/runtime/codex/bin/invite.sh +6 -0
  297. package/runtime/codex/bin/issue.sh +10 -0
  298. package/runtime/codex/bin/knowledge.sh +10 -0
  299. package/runtime/codex/bin/lib/config.sh +20 -0
  300. package/runtime/codex/bin/lib/context.sh +46 -25
  301. package/runtime/codex/bin/lib/git-sync.sh +70 -6
  302. package/runtime/codex/bin/lib/greeting.sh +34 -5
  303. package/runtime/codex/bin/lib/handoff-meta.sh +97 -0
  304. package/runtime/codex/bin/lib/worktree-links.sh +6 -0
  305. package/runtime/codex/bin/node-run.sh +42 -0
  306. package/runtime/codex/bin/notification.sh +6 -0
  307. package/runtime/codex/bin/observe-context.sh +14 -0
  308. package/runtime/codex/bin/onboarding.sh +6 -0
  309. package/runtime/codex/bin/person.py +4 -0
  310. package/runtime/codex/bin/person.sh +43 -10
  311. package/runtime/codex/bin/pi-render-spec.mjs +140 -0
  312. package/runtime/codex/bin/post-update.sh +37 -0
  313. package/runtime/codex/bin/prime-render-spec.mjs +147 -0
  314. package/runtime/codex/bin/publish-artifact.sh +55 -2
  315. package/runtime/codex/bin/quest.sh +9 -0
  316. package/runtime/codex/bin/question.sh +8 -0
  317. package/runtime/codex/bin/research-ingest.sh +7 -0
  318. package/runtime/codex/bin/restore-owned-skills.sh +67 -0
  319. package/runtime/codex/bin/retrieval_eval.py +385 -0
  320. package/runtime/codex/bin/runtime-identity.sh +70 -0
  321. package/runtime/codex/bin/runtime-upgrade.sh +43 -0
  322. package/runtime/codex/bin/scroll.sh +9 -0
  323. package/runtime/codex/bin/search.sh +231 -195
  324. package/runtime/codex/bin/settings.sh +174 -5
  325. package/runtime/codex/bin/startup-check.sh +136 -0
  326. package/runtime/codex/bin/telemetry.sh +522 -247
  327. package/runtime/codex/bin/test-changes.sh +344 -0
  328. package/runtime/codex/bin/thread.sh +9 -0
  329. package/runtime/codex/bin/todo.sh +9 -0
  330. package/runtime/codex/bin/view-render.sh +126 -0
  331. package/runtime/codex/bin/worktree-create.sh +19 -5
  332. package/runtime/codex/egregore_runtime/__init__.py +228 -0
  333. package/runtime/codex/egregore_runtime/adapters/__init__.py +5 -0
  334. package/runtime/codex/egregore_runtime/adapters/invitation.py +237 -0
  335. package/runtime/codex/egregore_runtime/adapters/notification.py +59 -0
  336. package/runtime/codex/egregore_runtime/adapters/qmd.py +1701 -0
  337. package/runtime/codex/egregore_runtime/adapters/qmd_cli.py +210 -0
  338. package/runtime/codex/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
  339. package/runtime/codex/egregore_runtime/admin_gate.py +102 -0
  340. package/runtime/codex/egregore_runtime/artifacts.py +523 -0
  341. package/runtime/codex/egregore_runtime/contracts.py +499 -0
  342. package/runtime/codex/egregore_runtime/entitlements.py +85 -0
  343. package/runtime/codex/egregore_runtime/errors.py +10 -0
  344. package/runtime/codex/egregore_runtime/graph.py +356 -0
  345. package/runtime/codex/egregore_runtime/handoff_lookup.py +196 -0
  346. package/runtime/codex/egregore_runtime/harness_cli.py +1221 -0
  347. package/runtime/codex/egregore_runtime/identity.py +734 -0
  348. package/runtime/codex/egregore_runtime/identity_cli.py +91 -0
  349. package/runtime/codex/egregore_runtime/ingest.py +575 -0
  350. package/runtime/codex/egregore_runtime/ingest_cli.py +254 -0
  351. package/runtime/codex/egregore_runtime/ingest_index_cli.py +147 -0
  352. package/runtime/codex/egregore_runtime/invitation_cli.py +51 -0
  353. package/runtime/codex/egregore_runtime/invitations.py +116 -0
  354. package/runtime/codex/egregore_runtime/issue_cli.py +305 -0
  355. package/runtime/codex/egregore_runtime/issues.py +359 -0
  356. package/runtime/codex/egregore_runtime/knowledge.py +376 -0
  357. package/runtime/codex/egregore_runtime/knowledge_cli.py +173 -0
  358. package/runtime/codex/egregore_runtime/lifecycle.py +888 -0
  359. package/runtime/codex/egregore_runtime/lifecycle_cli.py +269 -0
  360. package/runtime/codex/egregore_runtime/notification_cli.py +125 -0
  361. package/runtime/codex/egregore_runtime/notifications.py +230 -0
  362. package/runtime/codex/egregore_runtime/observe.py +342 -0
  363. package/runtime/codex/egregore_runtime/onboarding.py +290 -0
  364. package/runtime/codex/egregore_runtime/onboarding_cli.py +110 -0
  365. package/runtime/codex/egregore_runtime/policy.py +144 -0
  366. package/runtime/codex/egregore_runtime/profile.py +193 -0
  367. package/runtime/codex/egregore_runtime/quest_cli.py +216 -0
  368. package/runtime/codex/egregore_runtime/question_cli.py +180 -0
  369. package/runtime/codex/egregore_runtime/questions.py +304 -0
  370. package/runtime/codex/egregore_runtime/quests.py +419 -0
  371. package/runtime/codex/egregore_runtime/readiness.py +379 -0
  372. package/runtime/codex/egregore_runtime/research_ingest.py +256 -0
  373. package/runtime/codex/egregore_runtime/research_ingest_cli.py +44 -0
  374. package/runtime/codex/egregore_runtime/runtime.py +464 -0
  375. package/runtime/codex/egregore_runtime/scroll_cli.py +185 -0
  376. package/runtime/codex/egregore_runtime/scrolls.py +726 -0
  377. package/runtime/codex/egregore_runtime/status.py +412 -0
  378. package/runtime/codex/egregore_runtime/status_cli.py +174 -0
  379. package/runtime/codex/egregore_runtime/sync.py +218 -0
  380. package/runtime/codex/egregore_runtime/teamsync.py +139 -0
  381. package/runtime/codex/egregore_runtime/telemetry.py +640 -0
  382. package/runtime/codex/egregore_runtime/thread_cli.py +173 -0
  383. package/runtime/codex/egregore_runtime/threads.py +517 -0
  384. package/runtime/codex/egregore_runtime/todo_cli.py +142 -0
  385. package/runtime/codex/egregore_runtime/todos.py +244 -0
  386. package/runtime/codex/egregore_runtime/upgrade.py +770 -0
  387. package/runtime/codex/egregore_runtime/upgrade_worker.py +53 -0
  388. package/runtime/codex/egregore_runtime/writeback.py +391 -0
  389. package/runtime/codex/egregore_runtime/writeback_cli.py +114 -0
  390. package/runtime/codex/manifest.json +98 -1
  391. package/runtime/pi/.codex/hooks/search-hint.js +21 -52
  392. package/runtime/pi/.codex/hooks.json +3 -3
  393. package/runtime/pi/.codex/skills/activity/SKILL.md +23 -66
  394. package/runtime/pi/.codex/skills/announce/SKILL.md +27 -50
  395. package/runtime/pi/.codex/skills/archive/SKILL.md +21 -36
  396. package/runtime/pi/.codex/skills/checkup/SKILL.md +77 -36
  397. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +1 -1
  398. package/runtime/pi/.codex/skills/dashboard/SKILL.md +17 -47
  399. package/runtime/pi/.codex/skills/handoff/SKILL.md +84 -133
  400. package/runtime/pi/.codex/skills/harvest/SKILL.md +32 -117
  401. package/runtime/pi/.codex/skills/ingest/SKILL.md +47 -25
  402. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +18 -36
  403. package/runtime/pi/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
  404. package/runtime/pi/.codex/skills/invite/SKILL.md +21 -35
  405. package/runtime/pi/.codex/skills/issue/SKILL.md +60 -41
  406. package/runtime/pi/.codex/skills/me/SKILL.md +1 -1
  407. package/runtime/pi/.codex/skills/meeting/SKILL.md +16 -36
  408. package/runtime/pi/.codex/skills/meeting/references/analysis-contract.md +48 -0
  409. package/runtime/pi/.codex/skills/note/SKILL.md +30 -25
  410. package/runtime/pi/.codex/skills/onboarding/SKILL.md +36 -41
  411. package/runtime/pi/.codex/skills/pull/SKILL.md +1 -1
  412. package/runtime/pi/.codex/skills/quest/SKILL.md +27 -66
  413. package/runtime/pi/.codex/skills/reflect/SKILL.md +22 -36
  414. package/runtime/pi/.codex/skills/save/SKILL.md +2 -2
  415. package/runtime/pi/.codex/skills/telemetry/SKILL.md +1 -1
  416. package/runtime/pi/.codex/skills/test/SKILL.md +1 -1
  417. package/runtime/pi/.codex/skills/tutorial/SKILL.md +40 -41
  418. package/runtime/pi/.codex/skills/view/SKILL.md +115 -111
  419. package/runtime/pi/.codex/skills/wrap/SKILL.md +45 -41
  420. package/runtime/pi/.codex/spec-manifest.json +9 -9
  421. package/runtime/pi/.pi/APPEND_SYSTEM.md +57 -48
  422. package/runtime/pi/.pi/extensions/egregore.ts +7 -0
  423. package/runtime/pi/.pi/spec-manifest.json +2 -2
  424. package/runtime/pi/AGENTS.md +62 -58
  425. package/runtime/pi/bin/activity-action.sh +92 -0
  426. package/runtime/pi/bin/activity-data.sh +12 -233
  427. package/runtime/pi/bin/agent.sh +84 -100
  428. package/runtime/pi/bin/artifact-register.sh +164 -0
  429. package/runtime/pi/bin/artifact-writeback.sh +53 -0
  430. package/runtime/pi/bin/artifacts.sh +288 -0
  431. package/runtime/pi/bin/build-sealed-dataroom.mjs +722 -0
  432. package/runtime/pi/bin/capture-run.sh +18 -2
  433. package/runtime/pi/bin/codex-render-spec.mjs +41 -16
  434. package/runtime/pi/bin/codex-session-start.sh +14 -4
  435. package/runtime/pi/bin/codex-skill-render.mjs +9 -7
  436. package/runtime/pi/bin/corpus_map.py +806 -0
  437. package/runtime/pi/bin/corpus_survey.py +304 -0
  438. package/runtime/pi/bin/dashboard-data.sh +11 -171
  439. package/runtime/pi/bin/github-auth.sh +121 -0
  440. package/runtime/pi/bin/graph-batch.sh +7 -0
  441. package/runtime/pi/bin/graph-op.sh +4 -0
  442. package/runtime/pi/bin/graph-projection.sh +54 -0
  443. package/runtime/pi/bin/graph-wal.sh +13 -0
  444. package/runtime/pi/bin/graph.sh +44 -0
  445. package/runtime/pi/bin/handoff-lifecycle-job.sh +146 -0
  446. package/runtime/pi/bin/handoff-lifecycle.sh +280 -0
  447. package/runtime/pi/bin/handoff-preview.sh +169 -0
  448. package/runtime/pi/bin/handoff-run.sh +42 -26
  449. package/runtime/pi/bin/ingest-graph.sh +6 -0
  450. package/runtime/pi/bin/ingest-item.sh +10 -0
  451. package/runtime/pi/bin/ingest.sh +59 -30
  452. package/runtime/pi/bin/invite.sh +6 -0
  453. package/runtime/pi/bin/issue.sh +10 -0
  454. package/runtime/pi/bin/knowledge.sh +10 -0
  455. package/runtime/pi/bin/lib/config.sh +20 -0
  456. package/runtime/pi/bin/lib/context.sh +46 -25
  457. package/runtime/pi/bin/lib/git-sync.sh +70 -6
  458. package/runtime/pi/bin/lib/greeting.sh +34 -5
  459. package/runtime/pi/bin/lib/handoff-meta.sh +97 -0
  460. package/runtime/pi/bin/lib/worktree-links.sh +6 -0
  461. package/runtime/pi/bin/node-run.sh +42 -0
  462. package/runtime/pi/bin/notification.sh +6 -0
  463. package/runtime/pi/bin/observe-context.sh +14 -0
  464. package/runtime/pi/bin/onboarding.sh +6 -0
  465. package/runtime/pi/bin/person.py +4 -0
  466. package/runtime/pi/bin/person.sh +43 -10
  467. package/runtime/pi/bin/pi-observe-context.mjs +93 -0
  468. package/runtime/pi/bin/pi-product-workflows.mjs +1 -1
  469. package/runtime/pi/bin/pi-render-spec.mjs +1 -1
  470. package/runtime/pi/bin/post-update.sh +37 -0
  471. package/runtime/pi/bin/prime-render-spec.mjs +147 -0
  472. package/runtime/pi/bin/publish-artifact.sh +55 -2
  473. package/runtime/pi/bin/quest.sh +9 -0
  474. package/runtime/pi/bin/question.sh +8 -0
  475. package/runtime/pi/bin/research-ingest.sh +7 -0
  476. package/runtime/pi/bin/restore-owned-skills.sh +67 -0
  477. package/runtime/pi/bin/retrieval_eval.py +385 -0
  478. package/runtime/pi/bin/runtime-identity.sh +70 -0
  479. package/runtime/pi/bin/runtime-upgrade.sh +43 -0
  480. package/runtime/pi/bin/scroll.sh +9 -0
  481. package/runtime/pi/bin/search.sh +231 -195
  482. package/runtime/pi/bin/settings.sh +174 -5
  483. package/runtime/pi/bin/startup-check.sh +136 -0
  484. package/runtime/pi/bin/telemetry.sh +522 -247
  485. package/runtime/pi/bin/test-changes.sh +344 -0
  486. package/runtime/pi/bin/thread.sh +9 -0
  487. package/runtime/pi/bin/todo.sh +9 -0
  488. package/runtime/pi/bin/view-render.sh +126 -0
  489. package/runtime/pi/bin/worktree-create.sh +19 -5
  490. package/runtime/pi/egregore_runtime/__init__.py +228 -0
  491. package/runtime/pi/egregore_runtime/adapters/__init__.py +5 -0
  492. package/runtime/pi/egregore_runtime/adapters/invitation.py +237 -0
  493. package/runtime/pi/egregore_runtime/adapters/notification.py +59 -0
  494. package/runtime/pi/egregore_runtime/adapters/qmd.py +1701 -0
  495. package/runtime/pi/egregore_runtime/adapters/qmd_cli.py +210 -0
  496. package/runtime/pi/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
  497. package/runtime/pi/egregore_runtime/admin_gate.py +102 -0
  498. package/runtime/pi/egregore_runtime/artifacts.py +523 -0
  499. package/runtime/pi/egregore_runtime/contracts.py +499 -0
  500. package/runtime/pi/egregore_runtime/entitlements.py +85 -0
  501. package/runtime/pi/egregore_runtime/errors.py +10 -0
  502. package/runtime/pi/egregore_runtime/graph.py +356 -0
  503. package/runtime/pi/egregore_runtime/handoff_lookup.py +196 -0
  504. package/runtime/pi/egregore_runtime/harness_cli.py +1221 -0
  505. package/runtime/pi/egregore_runtime/identity.py +734 -0
  506. package/runtime/pi/egregore_runtime/identity_cli.py +91 -0
  507. package/runtime/pi/egregore_runtime/ingest.py +575 -0
  508. package/runtime/pi/egregore_runtime/ingest_cli.py +254 -0
  509. package/runtime/pi/egregore_runtime/ingest_index_cli.py +147 -0
  510. package/runtime/pi/egregore_runtime/invitation_cli.py +51 -0
  511. package/runtime/pi/egregore_runtime/invitations.py +116 -0
  512. package/runtime/pi/egregore_runtime/issue_cli.py +305 -0
  513. package/runtime/pi/egregore_runtime/issues.py +359 -0
  514. package/runtime/pi/egregore_runtime/knowledge.py +376 -0
  515. package/runtime/pi/egregore_runtime/knowledge_cli.py +173 -0
  516. package/runtime/pi/egregore_runtime/lifecycle.py +888 -0
  517. package/runtime/pi/egregore_runtime/lifecycle_cli.py +269 -0
  518. package/runtime/pi/egregore_runtime/notification_cli.py +125 -0
  519. package/runtime/pi/egregore_runtime/notifications.py +230 -0
  520. package/runtime/pi/egregore_runtime/observe.py +342 -0
  521. package/runtime/pi/egregore_runtime/onboarding.py +290 -0
  522. package/runtime/pi/egregore_runtime/onboarding_cli.py +110 -0
  523. package/runtime/pi/egregore_runtime/policy.py +144 -0
  524. package/runtime/pi/egregore_runtime/profile.py +193 -0
  525. package/runtime/pi/egregore_runtime/quest_cli.py +216 -0
  526. package/runtime/pi/egregore_runtime/question_cli.py +180 -0
  527. package/runtime/pi/egregore_runtime/questions.py +304 -0
  528. package/runtime/pi/egregore_runtime/quests.py +419 -0
  529. package/runtime/pi/egregore_runtime/readiness.py +379 -0
  530. package/runtime/pi/egregore_runtime/research_ingest.py +256 -0
  531. package/runtime/pi/egregore_runtime/research_ingest_cli.py +44 -0
  532. package/runtime/pi/egregore_runtime/runtime.py +464 -0
  533. package/runtime/pi/egregore_runtime/scroll_cli.py +185 -0
  534. package/runtime/pi/egregore_runtime/scrolls.py +726 -0
  535. package/runtime/pi/egregore_runtime/status.py +412 -0
  536. package/runtime/pi/egregore_runtime/status_cli.py +174 -0
  537. package/runtime/pi/egregore_runtime/sync.py +218 -0
  538. package/runtime/pi/egregore_runtime/teamsync.py +139 -0
  539. package/runtime/pi/egregore_runtime/telemetry.py +640 -0
  540. package/runtime/pi/egregore_runtime/thread_cli.py +173 -0
  541. package/runtime/pi/egregore_runtime/threads.py +517 -0
  542. package/runtime/pi/egregore_runtime/todo_cli.py +142 -0
  543. package/runtime/pi/egregore_runtime/todos.py +244 -0
  544. package/runtime/pi/egregore_runtime/upgrade.py +770 -0
  545. package/runtime/pi/egregore_runtime/upgrade_worker.py +53 -0
  546. package/runtime/pi/egregore_runtime/writeback.py +391 -0
  547. package/runtime/pi/egregore_runtime/writeback_cli.py +114 -0
  548. package/runtime/pi/manifest.json +98 -1
  549. package/runtime/prime/.codex/hooks/search-hint.js +21 -52
  550. package/runtime/prime/.codex/hooks.json +3 -3
  551. package/runtime/prime/.codex/skills/activity/SKILL.md +23 -66
  552. package/runtime/prime/.codex/skills/announce/SKILL.md +27 -50
  553. package/runtime/prime/.codex/skills/archive/SKILL.md +21 -36
  554. package/runtime/prime/.codex/skills/checkup/SKILL.md +77 -36
  555. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +1 -1
  556. package/runtime/prime/.codex/skills/dashboard/SKILL.md +17 -47
  557. package/runtime/prime/.codex/skills/handoff/SKILL.md +84 -133
  558. package/runtime/prime/.codex/skills/harvest/SKILL.md +32 -117
  559. package/runtime/prime/.codex/skills/ingest/SKILL.md +47 -25
  560. package/runtime/prime/.codex/skills/ingest-user-interview/SKILL.md +18 -36
  561. package/runtime/prime/.codex/skills/ingest-user-interview/references/analysis-contract.md +42 -0
  562. package/runtime/prime/.codex/skills/invite/SKILL.md +21 -35
  563. package/runtime/prime/.codex/skills/issue/SKILL.md +60 -41
  564. package/runtime/prime/.codex/skills/me/SKILL.md +1 -1
  565. package/runtime/prime/.codex/skills/meeting/SKILL.md +16 -36
  566. package/runtime/prime/.codex/skills/meeting/references/analysis-contract.md +48 -0
  567. package/runtime/prime/.codex/skills/note/SKILL.md +30 -25
  568. package/runtime/prime/.codex/skills/onboarding/SKILL.md +36 -41
  569. package/runtime/prime/.codex/skills/pull/SKILL.md +1 -1
  570. package/runtime/prime/.codex/skills/quest/SKILL.md +27 -66
  571. package/runtime/prime/.codex/skills/reflect/SKILL.md +22 -36
  572. package/runtime/prime/.codex/skills/save/SKILL.md +2 -2
  573. package/runtime/prime/.codex/skills/telemetry/SKILL.md +1 -1
  574. package/runtime/prime/.codex/skills/test/SKILL.md +1 -1
  575. package/runtime/prime/.codex/skills/tutorial/SKILL.md +40 -41
  576. package/runtime/prime/.codex/skills/view/SKILL.md +115 -111
  577. package/runtime/prime/.codex/skills/wrap/SKILL.md +45 -41
  578. package/runtime/prime/.codex/spec-manifest.json +9 -9
  579. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +57 -48
  580. package/runtime/prime/.prime/agent/extensions/egregore.ts +7 -0
  581. package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
  582. package/runtime/prime/AGENTS.md +62 -58
  583. package/runtime/prime/bin/activity-action.sh +92 -0
  584. package/runtime/prime/bin/activity-data.sh +12 -233
  585. package/runtime/prime/bin/agent.sh +84 -100
  586. package/runtime/prime/bin/artifact-register.sh +164 -0
  587. package/runtime/prime/bin/artifact-writeback.sh +53 -0
  588. package/runtime/prime/bin/artifacts.sh +288 -0
  589. package/runtime/prime/bin/build-sealed-dataroom.mjs +722 -0
  590. package/runtime/prime/bin/capture-run.sh +18 -2
  591. package/runtime/prime/bin/codex-render-spec.mjs +41 -16
  592. package/runtime/prime/bin/codex-session-start.sh +14 -4
  593. package/runtime/prime/bin/codex-skill-render.mjs +9 -7
  594. package/runtime/prime/bin/corpus_map.py +806 -0
  595. package/runtime/prime/bin/corpus_survey.py +304 -0
  596. package/runtime/prime/bin/dashboard-data.sh +11 -171
  597. package/runtime/prime/bin/github-auth.sh +121 -0
  598. package/runtime/prime/bin/graph-batch.sh +7 -0
  599. package/runtime/prime/bin/graph-op.sh +4 -0
  600. package/runtime/prime/bin/graph-projection.sh +54 -0
  601. package/runtime/prime/bin/graph-wal.sh +13 -0
  602. package/runtime/prime/bin/graph.sh +44 -0
  603. package/runtime/prime/bin/handoff-lifecycle-job.sh +146 -0
  604. package/runtime/prime/bin/handoff-lifecycle.sh +280 -0
  605. package/runtime/prime/bin/handoff-preview.sh +169 -0
  606. package/runtime/prime/bin/handoff-run.sh +42 -26
  607. package/runtime/prime/bin/ingest-graph.sh +6 -0
  608. package/runtime/prime/bin/ingest-item.sh +10 -0
  609. package/runtime/prime/bin/ingest.sh +59 -30
  610. package/runtime/prime/bin/invite.sh +6 -0
  611. package/runtime/prime/bin/issue.sh +10 -0
  612. package/runtime/prime/bin/knowledge.sh +10 -0
  613. package/runtime/prime/bin/lib/config.sh +20 -0
  614. package/runtime/prime/bin/lib/context.sh +46 -25
  615. package/runtime/prime/bin/lib/git-sync.sh +70 -6
  616. package/runtime/prime/bin/lib/greeting.sh +34 -5
  617. package/runtime/prime/bin/lib/handoff-meta.sh +97 -0
  618. package/runtime/prime/bin/lib/worktree-links.sh +6 -0
  619. package/runtime/prime/bin/node-run.sh +42 -0
  620. package/runtime/prime/bin/notification.sh +6 -0
  621. package/runtime/prime/bin/observe-context.sh +14 -0
  622. package/runtime/prime/bin/onboarding.sh +6 -0
  623. package/runtime/prime/bin/person.py +4 -0
  624. package/runtime/prime/bin/person.sh +43 -10
  625. package/runtime/prime/bin/pi-observe-context.mjs +93 -0
  626. package/runtime/prime/bin/pi-product-workflows.mjs +1 -1
  627. package/runtime/prime/bin/pi-render-spec.mjs +140 -0
  628. package/runtime/prime/bin/post-update.sh +37 -0
  629. package/runtime/prime/bin/prime-render-spec.mjs +1 -1
  630. package/runtime/prime/bin/publish-artifact.sh +55 -2
  631. package/runtime/prime/bin/quest.sh +9 -0
  632. package/runtime/prime/bin/question.sh +8 -0
  633. package/runtime/prime/bin/research-ingest.sh +7 -0
  634. package/runtime/prime/bin/restore-owned-skills.sh +67 -0
  635. package/runtime/prime/bin/retrieval_eval.py +385 -0
  636. package/runtime/prime/bin/runtime-identity.sh +70 -0
  637. package/runtime/prime/bin/runtime-upgrade.sh +43 -0
  638. package/runtime/prime/bin/scroll.sh +9 -0
  639. package/runtime/prime/bin/search.sh +231 -195
  640. package/runtime/prime/bin/settings.sh +174 -5
  641. package/runtime/prime/bin/startup-check.sh +136 -0
  642. package/runtime/prime/bin/telemetry.sh +522 -247
  643. package/runtime/prime/bin/test-changes.sh +344 -0
  644. package/runtime/prime/bin/thread.sh +9 -0
  645. package/runtime/prime/bin/todo.sh +9 -0
  646. package/runtime/prime/bin/view-render.sh +126 -0
  647. package/runtime/prime/bin/worktree-create.sh +19 -5
  648. package/runtime/prime/egregore_runtime/__init__.py +228 -0
  649. package/runtime/prime/egregore_runtime/adapters/__init__.py +5 -0
  650. package/runtime/prime/egregore_runtime/adapters/invitation.py +237 -0
  651. package/runtime/prime/egregore_runtime/adapters/notification.py +59 -0
  652. package/runtime/prime/egregore_runtime/adapters/qmd.py +1701 -0
  653. package/runtime/prime/egregore_runtime/adapters/qmd_cli.py +210 -0
  654. package/runtime/prime/egregore_runtime/adapters/qmd_embed_worker.py +59 -0
  655. package/runtime/prime/egregore_runtime/admin_gate.py +102 -0
  656. package/runtime/prime/egregore_runtime/artifacts.py +523 -0
  657. package/runtime/prime/egregore_runtime/contracts.py +499 -0
  658. package/runtime/prime/egregore_runtime/entitlements.py +85 -0
  659. package/runtime/prime/egregore_runtime/errors.py +10 -0
  660. package/runtime/prime/egregore_runtime/graph.py +356 -0
  661. package/runtime/prime/egregore_runtime/handoff_lookup.py +196 -0
  662. package/runtime/prime/egregore_runtime/harness_cli.py +1221 -0
  663. package/runtime/prime/egregore_runtime/identity.py +734 -0
  664. package/runtime/prime/egregore_runtime/identity_cli.py +91 -0
  665. package/runtime/prime/egregore_runtime/ingest.py +575 -0
  666. package/runtime/prime/egregore_runtime/ingest_cli.py +254 -0
  667. package/runtime/prime/egregore_runtime/ingest_index_cli.py +147 -0
  668. package/runtime/prime/egregore_runtime/invitation_cli.py +51 -0
  669. package/runtime/prime/egregore_runtime/invitations.py +116 -0
  670. package/runtime/prime/egregore_runtime/issue_cli.py +305 -0
  671. package/runtime/prime/egregore_runtime/issues.py +359 -0
  672. package/runtime/prime/egregore_runtime/knowledge.py +376 -0
  673. package/runtime/prime/egregore_runtime/knowledge_cli.py +173 -0
  674. package/runtime/prime/egregore_runtime/lifecycle.py +888 -0
  675. package/runtime/prime/egregore_runtime/lifecycle_cli.py +269 -0
  676. package/runtime/prime/egregore_runtime/notification_cli.py +125 -0
  677. package/runtime/prime/egregore_runtime/notifications.py +230 -0
  678. package/runtime/prime/egregore_runtime/observe.py +342 -0
  679. package/runtime/prime/egregore_runtime/onboarding.py +290 -0
  680. package/runtime/prime/egregore_runtime/onboarding_cli.py +110 -0
  681. package/runtime/prime/egregore_runtime/policy.py +144 -0
  682. package/runtime/prime/egregore_runtime/profile.py +193 -0
  683. package/runtime/prime/egregore_runtime/quest_cli.py +216 -0
  684. package/runtime/prime/egregore_runtime/question_cli.py +180 -0
  685. package/runtime/prime/egregore_runtime/questions.py +304 -0
  686. package/runtime/prime/egregore_runtime/quests.py +419 -0
  687. package/runtime/prime/egregore_runtime/readiness.py +379 -0
  688. package/runtime/prime/egregore_runtime/research_ingest.py +256 -0
  689. package/runtime/prime/egregore_runtime/research_ingest_cli.py +44 -0
  690. package/runtime/prime/egregore_runtime/runtime.py +464 -0
  691. package/runtime/prime/egregore_runtime/scroll_cli.py +185 -0
  692. package/runtime/prime/egregore_runtime/scrolls.py +726 -0
  693. package/runtime/prime/egregore_runtime/status.py +412 -0
  694. package/runtime/prime/egregore_runtime/status_cli.py +174 -0
  695. package/runtime/prime/egregore_runtime/sync.py +218 -0
  696. package/runtime/prime/egregore_runtime/teamsync.py +139 -0
  697. package/runtime/prime/egregore_runtime/telemetry.py +640 -0
  698. package/runtime/prime/egregore_runtime/thread_cli.py +173 -0
  699. package/runtime/prime/egregore_runtime/threads.py +517 -0
  700. package/runtime/prime/egregore_runtime/todo_cli.py +142 -0
  701. package/runtime/prime/egregore_runtime/todos.py +244 -0
  702. package/runtime/prime/egregore_runtime/upgrade.py +770 -0
  703. package/runtime/prime/egregore_runtime/upgrade_worker.py +53 -0
  704. package/runtime/prime/egregore_runtime/writeback.py +391 -0
  705. package/runtime/prime/egregore_runtime/writeback_cli.py +114 -0
  706. package/runtime/prime/manifest.json +98 -1
@@ -0,0 +1,1701 @@
1
+ """Local QMD implementation of the Egregore ``Retriever`` contract.
2
+
3
+ QMD-specific commands, query syntax, output parsing, model detection, and
4
+ daemon lifecycle are deliberately confined to this module. Callers submit
5
+ typed domain requests and receive domain results.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import os
13
+ import re
14
+ import shutil
15
+ import socket
16
+ import subprocess
17
+ import sys
18
+ import time
19
+ import urllib.error
20
+ import urllib.parse
21
+ import urllib.request
22
+ import uuid
23
+ from contextlib import contextmanager
24
+ from dataclasses import dataclass, replace
25
+ from datetime import UTC, datetime
26
+ from pathlib import Path
27
+ from typing import Any, Mapping, Sequence
28
+
29
+ import fcntl
30
+
31
+ from egregore_runtime.contracts import (
32
+ RetrievalHit,
33
+ RetrievalMode,
34
+ RetrievalRequest,
35
+ RetrievalResult,
36
+ RetrieverHealth,
37
+ )
38
+ from egregore_runtime.artifacts import (
39
+ ArtifactSchemaError,
40
+ canonical_artifact_id,
41
+ split_frontmatter,
42
+ )
43
+
44
+
45
+ QMD_VERSION = "2.8.3"
46
+ INDEX_SPEC_VERSION = "egregore-qmd-local/v2"
47
+ RUNTIME_SCHEMA_VERSION = "egregore-qmd-runtime/v1"
48
+ EMBEDDING_MODEL = "EmbeddingGemma"
49
+ MAX_AUTHORIZED_CANDIDATES = 2_000
50
+ # Every query this adapter issues — daemon and CLI — disables model
51
+ # reranking; results come from typed lex/vec rank fusion only. Readiness
52
+ # surfaces report this posture from here rather than restating it.
53
+ RERANKING_ENABLED = False
54
+
55
+
56
+ class QmdAdapterError(RuntimeError):
57
+ """Raised when the pinned local retrieval adapter cannot execute safely."""
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class _DocumentMetadata:
62
+ artifact_id: str
63
+ title: str | None = None
64
+ artifact_type: str | None = None
65
+ workstream: str | None = None
66
+ revision: str | None = None
67
+ content_hash: str | None = None
68
+ observed_at: datetime | None = None
69
+
70
+
71
+ @dataclass(frozen=True, slots=True)
72
+ class QmdRuntimeMetadata:
73
+ schema_version: str
74
+ instance_id: str
75
+ generation: str
76
+ pid: int
77
+ port: int
78
+ endpoint: str
79
+ qmd_version: str
80
+ index_spec_version: str
81
+ index_name: str
82
+ index_path: str
83
+ collection: str
84
+ memory_root: str
85
+ started_at: str
86
+ process_started_at: str
87
+
88
+ @classmethod
89
+ def from_mapping(cls, value: Mapping[str, Any]) -> "QmdRuntimeMetadata":
90
+ return cls(
91
+ schema_version=str(value["schema_version"]),
92
+ instance_id=str(value["instance_id"]),
93
+ generation=str(value["generation"]),
94
+ pid=int(value["pid"]),
95
+ port=int(value["port"]),
96
+ endpoint=str(value["endpoint"]),
97
+ qmd_version=str(value["qmd_version"]),
98
+ index_spec_version=str(value["index_spec_version"]),
99
+ index_name=str(value["index_name"]),
100
+ index_path=str(value["index_path"]),
101
+ collection=str(value["collection"]),
102
+ memory_root=str(value["memory_root"]),
103
+ started_at=str(value["started_at"]),
104
+ process_started_at=str(value["process_started_at"]),
105
+ )
106
+
107
+ def to_dict(self) -> dict[str, str | int]:
108
+ return {
109
+ "schema_version": self.schema_version,
110
+ "instance_id": self.instance_id,
111
+ "generation": self.generation,
112
+ "pid": self.pid,
113
+ "port": self.port,
114
+ "endpoint": self.endpoint,
115
+ "qmd_version": self.qmd_version,
116
+ "index_spec_version": self.index_spec_version,
117
+ "index_name": self.index_name,
118
+ "index_path": self.index_path,
119
+ "collection": self.collection,
120
+ "memory_root": self.memory_root,
121
+ "started_at": self.started_at,
122
+ "process_started_at": self.process_started_at,
123
+ }
124
+
125
+
126
+ class QmdLocalRetriever:
127
+ """QMD 2.8.3-backed local retriever with explicit typed rank fusion.
128
+
129
+ The persistent loopback daemon is preferred because it keeps local models
130
+ warm. Every daemon request supplies ``searches`` (never QMD's untyped
131
+ expansion input) and disables reranking. The exact same typed query is
132
+ used by the pinned CLI fallback.
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ *,
138
+ repository_root: Path,
139
+ memory_root: Path | None = None,
140
+ collection: str | None = None,
141
+ collection_purpose: str = "memory",
142
+ qmd_command: Sequence[str] | None = None,
143
+ environment: Mapping[str, str] | None = None,
144
+ ) -> None:
145
+ self.repository_root = repository_root.resolve()
146
+ configured_memory = memory_root or self.repository_root / "memory"
147
+ self.memory_root = configured_memory.resolve()
148
+ self.environment = dict(os.environ if environment is None else environment)
149
+ self.collection_purpose = re.sub(
150
+ r"[^A-Za-z0-9_.-]", "-", collection_purpose.strip()
151
+ ) or "memory"
152
+ self.collection = collection or self._default_collection()
153
+ self.index_name = self.environment.get("EGREGORE_QMD_INDEX_NAME") or (
154
+ f"egregore-{self.collection_purpose}-{self._instance_hash()[:12]}-v2"
155
+ )
156
+ self._configured_command = tuple(qmd_command) if qmd_command else None
157
+ self._resolved_command: tuple[str, ...] | None = None
158
+ self._version_checked = False
159
+ self._daemon_available = False
160
+ self._source_revision_cache: str | None = None
161
+ self._configure_isolated_environment()
162
+ self._prefer_usable_node_runtime()
163
+
164
+ def _configure_isolated_environment(self) -> None:
165
+ """Give QMD its own database, config, PID files, logs, and models."""
166
+
167
+ runtime = self._runtime_root()
168
+ cache = runtime / "cache"
169
+ config = runtime / "config"
170
+ index = self._index_path()
171
+ self.environment["INDEX_PATH"] = str(index)
172
+ self.environment["QMD_CONFIG_DIR"] = str(config)
173
+ self.environment["XDG_CACHE_HOME"] = str(cache)
174
+ self.environment["XDG_CONFIG_HOME"] = str(config)
175
+
176
+ def _prefer_usable_node_runtime(self) -> None:
177
+ """Put a working Node installation ahead of broken version-manager shims.
178
+
179
+ Harnesses do not always inherit the interactive shell's fully expanded
180
+ PATH. In particular, Volta's shim directory can remain visible while
181
+ the selected Node image directory is absent, causing every QMD/npm/npx
182
+ shebang to fail with ``Node is not available``. Validate candidates
183
+ instead of trusting their presence, then prepend the first usable
184
+ runtime without changing the parent process or the user's global Node
185
+ configuration.
186
+ """
187
+
188
+ configured = self.environment.get("EGREGORE_QMD_NODE_BIN")
189
+ path_value = self.environment.get("PATH", "")
190
+ candidates: list[Path] = []
191
+ if configured:
192
+ candidates.append(Path(configured).expanduser())
193
+ resolved = shutil.which("node", path=path_value)
194
+ if resolved:
195
+ candidates.append(Path(resolved))
196
+
197
+ home = Path(self.environment.get("HOME", str(Path.home()))).expanduser()
198
+ volta_images = home / ".volta" / "tools" / "image" / "node"
199
+ if volta_images.is_dir():
200
+ candidates.extend(
201
+ sorted(
202
+ volta_images.glob("*/bin/node"),
203
+ key=lambda candidate: self._node_version_key(candidate),
204
+ reverse=True,
205
+ )
206
+ )
207
+ candidates.extend((Path("/opt/homebrew/bin/node"), Path("/usr/local/bin/node")))
208
+
209
+ seen: set[str] = set()
210
+ for candidate in candidates:
211
+ try:
212
+ executable = candidate.resolve()
213
+ except OSError:
214
+ continue
215
+ key = str(executable)
216
+ if key in seen or not executable.is_file() or not os.access(executable, os.X_OK):
217
+ continue
218
+ seen.add(key)
219
+ try:
220
+ checked = subprocess.run(
221
+ (str(executable), "--version"),
222
+ cwd=self.repository_root,
223
+ env=self.environment,
224
+ text=True,
225
+ capture_output=True,
226
+ timeout=10,
227
+ check=False,
228
+ umask=0o077,
229
+ )
230
+ except (OSError, subprocess.TimeoutExpired):
231
+ continue
232
+ if checked.returncode != 0 or not re.search(r"^v\d+\.\d+\.\d+", checked.stdout.strip()):
233
+ continue
234
+ node_dir = str(executable.parent)
235
+ entries = [entry for entry in path_value.split(os.pathsep) if entry]
236
+ entries = [entry for entry in entries if Path(entry).expanduser() != executable.parent]
237
+ self.environment["PATH"] = os.pathsep.join((node_dir, *entries))
238
+ self.environment["EGREGORE_QMD_NODE_BIN"] = str(executable)
239
+ return
240
+
241
+ @staticmethod
242
+ def _node_version_key(candidate: Path) -> tuple[int, ...]:
243
+ try:
244
+ return tuple(int(part) for part in candidate.parents[1].name.split("."))
245
+ except (ValueError, IndexError):
246
+ return ()
247
+
248
+ def _ensure_runtime_layout(self) -> None:
249
+ runtime = self._runtime_root()
250
+ for directory in (
251
+ runtime,
252
+ runtime / "cache",
253
+ runtime / "config",
254
+ self._index_path().parent,
255
+ self._models_path(),
256
+ ):
257
+ directory.mkdir(parents=True, exist_ok=True, mode=0o700)
258
+ os.chmod(directory, 0o700)
259
+ for private_file in (
260
+ self._index_path(),
261
+ self._runtime_state_path(),
262
+ self._index_state_path(),
263
+ self._semantic_state_path(),
264
+ ):
265
+ if private_file.exists():
266
+ os.chmod(private_file, 0o600)
267
+ self._reuse_embedding_model()
268
+
269
+ def _reuse_embedding_model(self) -> None:
270
+ if self._embedding_model_available():
271
+ return
272
+ configured = (
273
+ self.environment.get("EGREGORE_QMD_REUSE_EMBEDDING_MODEL")
274
+ or self.environment.get("EGREGORE_QMD_MODELS_PATH")
275
+ )
276
+ candidates: list[Path] = []
277
+ if configured:
278
+ configured_path = Path(configured).expanduser()
279
+ if configured_path.is_dir():
280
+ candidates.extend(sorted(configured_path.glob("*embeddinggemma*")))
281
+ else:
282
+ candidates.append(configured_path)
283
+ home = Path(self.environment.get("HOME", str(Path.home()))).expanduser()
284
+ candidates.extend(
285
+ sorted((home / ".cache" / "qmd" / "models").glob("*embeddinggemma*"))
286
+ )
287
+ target_dir = self._models_path()
288
+ for source in candidates:
289
+ if not source.is_file():
290
+ continue
291
+ target = target_dir / source.name
292
+ if not target.exists():
293
+ target.symlink_to(source.resolve())
294
+ return
295
+
296
+ def _runtime_root(self) -> Path:
297
+ override = self.environment.get("EGREGORE_QMD_RUNTIME_DIR")
298
+ if override:
299
+ return Path(override).expanduser().resolve()
300
+ home = Path(self.environment.get("HOME", str(Path.home()))).expanduser()
301
+ return home / ".egregore" / "runtime" / "qmd" / self._instance_hash()
302
+
303
+ def _runtime_state_path(self) -> Path:
304
+ return self._runtime_root() / "runtime.json"
305
+
306
+ def _lifecycle_lock_path(self) -> Path:
307
+ return self._runtime_root() / "lifecycle.lock"
308
+
309
+ @contextmanager
310
+ def _lifecycle_lock(self):
311
+ self._ensure_runtime_layout()
312
+ path = self._lifecycle_lock_path()
313
+ with path.open("a+", encoding="utf-8") as handle:
314
+ os.chmod(path, 0o600)
315
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
316
+ try:
317
+ yield
318
+ finally:
319
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
320
+
321
+ # -- Retriever contract -------------------------------------------------
322
+
323
+ def health(self) -> RetrieverHealth:
324
+ warnings: list[str] = []
325
+ canonical_ready = self.memory_root.is_dir()
326
+ qmd_available = True
327
+ if not canonical_ready:
328
+ warnings.append(f"canonical memory is unavailable: {self.memory_root}")
329
+
330
+ try:
331
+ self._ensure_runtime_layout()
332
+ self._ensure_version()
333
+ except QmdAdapterError as exc:
334
+ qmd_available = False
335
+ warnings.append(str(exc))
336
+
337
+ status = ""
338
+ status_ok = False
339
+ if canonical_ready and qmd_available:
340
+ completed = self._run(("status",), timeout=20)
341
+ if completed.returncode == 0:
342
+ status = completed.stdout
343
+ status_ok = True
344
+ else:
345
+ warnings.append(self._failure_message("qmd status", completed))
346
+
347
+ counts = self._status_counts(status)
348
+ # BM25 readiness is usability, mirroring the semantic split: a
349
+ # canonical write leaves the index trailing, not unusable — queries
350
+ # refresh it inline. Only an identity mismatch makes it stale.
351
+ bm25_ready = bool(
352
+ status_ok
353
+ and counts["documents"] > 0
354
+ and self._index_state_usable()
355
+ )
356
+ semantic_ready = bool(
357
+ bm25_ready
358
+ and counts["vectors"] > 0
359
+ and counts["pending"] == 0
360
+ and self._semantic_state_current()
361
+ and not self._live_lock(self._embed_lock_path())
362
+ )
363
+ embedding_state = self._embedding_state(status, semantic_ready=semantic_ready)
364
+ if embedding_state == "missing_model":
365
+ warnings.append(
366
+ f"{EMBEDDING_MODEL} is not installed; hybrid requests degrade to lexical retrieval while embedding warms"
367
+ )
368
+
369
+ runtime = self._owned_runtime()
370
+ if not canonical_ready or not qmd_available or not status_ok:
371
+ runtime_state = "runtime_unavailable"
372
+ elif self._persistent_enabled() and runtime is None:
373
+ runtime_state = "runtime_unavailable"
374
+ elif not self._index_state_usable() and self._index_path().exists():
375
+ runtime_state = "runtime_stale"
376
+ elif not bm25_ready:
377
+ runtime_state = "canonical_state_ready"
378
+ elif not semantic_ready:
379
+ runtime_state = "semantic_index_building"
380
+ else:
381
+ runtime_state = "semantic_ready"
382
+
383
+ return RetrieverHealth(
384
+ available=canonical_ready and qmd_available and status_ok,
385
+ adapter="qmd-local",
386
+ adapter_version=QMD_VERSION,
387
+ index_spec_version=INDEX_SPEC_VERSION,
388
+ index_revision=self._index_revision(),
389
+ source_revision=self._source_revision(),
390
+ embedding_state=embedding_state,
391
+ warnings=tuple(warnings),
392
+ canonical_state_ready=canonical_ready,
393
+ bm25_ready=bm25_ready,
394
+ semantic_ready=semantic_ready,
395
+ runtime_state=runtime_state,
396
+ runtime_pid=runtime.pid if runtime else None,
397
+ runtime_endpoint=runtime.endpoint if runtime else None,
398
+ index_path=str(self._index_path()),
399
+ collection=self.collection,
400
+ index_source_revision=self._indexed_source_revision(),
401
+ semantic_source_revision=self._semantic_source_revision(),
402
+ )
403
+
404
+ def start(self) -> RetrieverHealth:
405
+ """Bring BM25 online, start/reuse the owned worker, then embed async."""
406
+
407
+ warnings: list[str] = []
408
+ self._source_revision_cache = None
409
+ self._ensure_ready(warnings)
410
+ if not self._ensure_daemon():
411
+ warnings.append("instance-owned QMD worker is unavailable; CLI fallback remains active")
412
+ if not self._semantic_state_current():
413
+ self._schedule_embed()
414
+ health = self.health()
415
+ if not warnings:
416
+ return health
417
+ return replace(health, warnings=tuple((*health.warnings, *warnings)))
418
+
419
+ def shutdown(self) -> RetrieverHealth:
420
+ """Stop only a worker whose full instance/process identity is proven."""
421
+
422
+ with self._lifecycle_lock():
423
+ runtime = self._owned_runtime()
424
+ if runtime is not None:
425
+ stopped = self._run(("mcp", "stop"), timeout=20)
426
+ if stopped.returncode != 0 and self._pid_alive(runtime.pid):
427
+ raise QmdAdapterError(
428
+ self._failure_message("owned qmd worker shutdown", stopped)
429
+ )
430
+ self._runtime_state_path().unlink(missing_ok=True)
431
+ self._daemon_available = False
432
+ return self.health()
433
+
434
+ def _diagnostic_trace(self, phase: str, milliseconds: int, **extra: Any) -> None:
435
+ """Local-only latency diagnostics, independent of telemetry consent.
436
+
437
+ EGREGORE_RETRIEVAL_TRACE=1 appends one JSON line per phase to
438
+ retrieval-trace.jsonl inside the instance runtime dir. Content-free
439
+ by construction: phases, timings, counts — never queries or text.
440
+ """
441
+
442
+ if self.environment.get("EGREGORE_RETRIEVAL_TRACE") != "1":
443
+ return
444
+ try:
445
+ line = json.dumps(
446
+ {
447
+ "at": datetime.now(UTC).isoformat(),
448
+ "phase": phase,
449
+ "ms": milliseconds,
450
+ **extra,
451
+ },
452
+ sort_keys=True,
453
+ )
454
+ trace_path = self._runtime_root() / "retrieval-trace.jsonl"
455
+ with trace_path.open("a", encoding="utf-8") as handle:
456
+ handle.write(line + "\n")
457
+ os.chmod(trace_path, 0o600)
458
+ except OSError:
459
+ pass
460
+
461
+ def retrieve(self, request: RetrievalRequest) -> RetrievalResult:
462
+ started = time.monotonic()
463
+ warnings: list[str] = []
464
+ # A long-lived runtime may observe a new canonical Git revision between
465
+ # requests. Cache only within one retrieval operation.
466
+ self._source_revision_cache = None
467
+ self._ensure_ready(warnings)
468
+
469
+ requested_mode = request.mode
470
+ executed_mode = requested_mode
471
+ if requested_mode in (RetrievalMode.HYBRID, RetrievalMode.VEC):
472
+ # Hybrid retrieval stays available across canonical writes: the
473
+ # last complete vectors keep serving alongside current BM25 while
474
+ # a newer embedding builds. Lexical fallback happens only when no
475
+ # usable vector build exists at all.
476
+ if not self._semantic_state_usable():
477
+ executed_mode = RetrievalMode.LEX
478
+ warnings.append(
479
+ "no usable vector index exists yet; request used lexical fallback and embedding was scheduled"
480
+ )
481
+
482
+ searches = self._typed_searches(request, executed_mode)
483
+ if not searches:
484
+ raise QmdAdapterError(
485
+ f"retrieval request {request.request_id!r} has no {executed_mode.value} query"
486
+ )
487
+
488
+ candidate_limit = max(request.top_k * 8, 64)
489
+ raw_hits: list[dict[str, Any]]
490
+ try:
491
+ raw_hits = self._query(searches, candidate_limit, request.task)
492
+ except QmdAdapterError as exc:
493
+ if executed_mode is RetrievalMode.LEX:
494
+ raise
495
+ warnings.append(f"typed {executed_mode.value} retrieval failed: {exc}; used lexical fallback")
496
+ executed_mode = RetrievalMode.LEX
497
+ searches = self._typed_searches(request, executed_mode)
498
+ if not searches:
499
+ raise
500
+ raw_hits = self._query(searches, candidate_limit, request.task)
501
+
502
+ hits = self._authorized_hits(raw_hits, request, executed_mode)
503
+ while (
504
+ len(hits) < request.top_k
505
+ and len(raw_hits) >= candidate_limit
506
+ and candidate_limit < MAX_AUTHORIZED_CANDIDATES
507
+ ):
508
+ candidate_limit = min(candidate_limit * 4, MAX_AUTHORIZED_CANDIDATES)
509
+ raw_hits = self._query(searches, candidate_limit, request.task)
510
+ hits = self._authorized_hits(raw_hits, request, executed_mode)
511
+ if len(hits) < request.top_k and len(raw_hits) >= MAX_AUTHORIZED_CANDIDATES:
512
+ warnings.append(
513
+ "authorized candidates may be incomplete after bounded policy-safe overfetch"
514
+ )
515
+ self._schedule_embed()
516
+ latency_ms = round((time.monotonic() - started) * 1000)
517
+ self._diagnostic_trace(
518
+ "retrieve",
519
+ latency_ms,
520
+ mode=str(requested_mode),
521
+ hits=len(hits[: request.top_k]),
522
+ degraded=executed_mode is not requested_mode or bool(warnings),
523
+ )
524
+ return RetrievalResult(
525
+ request_id=request.request_id,
526
+ mode=requested_mode,
527
+ hits=tuple(hits[: request.top_k]),
528
+ index_spec_version=INDEX_SPEC_VERSION,
529
+ index_revision=self._index_revision(),
530
+ source_revision=self._source_revision(),
531
+ latency_ms=latency_ms,
532
+ degraded=executed_mode is not requested_mode or bool(warnings),
533
+ warnings=tuple(warnings),
534
+ semantic_source_revision=(
535
+ self._semantic_source_revision()
536
+ if executed_mode in (RetrievalMode.HYBRID, RetrievalMode.VEC)
537
+ else None
538
+ ),
539
+ )
540
+
541
+ def open_source(self, hit: RetrievalHit) -> str:
542
+ started = time.monotonic()
543
+ source = self._source_path(hit.canonical_path)
544
+ if not source.is_file():
545
+ raise QmdAdapterError(f"canonical source is unavailable: {hit.canonical_path}")
546
+ content = source.read_text(encoding="utf-8")
547
+ self._diagnostic_trace(
548
+ "open_source",
549
+ round((time.monotonic() - started) * 1000),
550
+ bytes=len(content),
551
+ )
552
+ return content
553
+
554
+ def update(self, paths: Sequence[str] = ()) -> RetrieverHealth:
555
+ # QMD's update is already incremental. Its v2.8.3 CLI scans registered
556
+ # collections rather than accepting individual paths, so ``paths`` is a
557
+ # writeback hint rather than a different command shape.
558
+ del paths
559
+ self._source_revision_cache = None
560
+ if self._index_state_current():
561
+ # The index already matches the exact canonical revision — there
562
+ # is nothing to scan. Boot-path syncs stop paying QMD subprocess
563
+ # spawns for an unchanged corpus; any revision change (including
564
+ # a dirty-tree hash change) still takes the full update path.
565
+ return self.health()
566
+ # A canonical write makes the semantic build stale, never unusable:
567
+ # staleness is expressed by revision inequality alone, and the last
568
+ # complete vector receipt persists so hybrid keeps serving while the
569
+ # next embedding builds.
570
+ self._ensure_collection(force=True)
571
+ completed = self._run(("update",), timeout=300)
572
+ if completed.returncode != 0:
573
+ raise QmdAdapterError(self._failure_message("qmd update", completed))
574
+ self._write_index_state()
575
+ return self.health()
576
+
577
+ def embed_background(self) -> RetrieverHealth:
578
+ # Snapshot health before starting the worker. Once QMD begins embedding
579
+ # it may hold SQLite's writer lock, so a status call made afterwards can
580
+ # turn this nominally background operation into a foreground wait.
581
+ health = self.health()
582
+ if not self._schedule_embed():
583
+ return health
584
+ return replace(
585
+ health,
586
+ embedding_state="running",
587
+ semantic_ready=False,
588
+ runtime_state="semantic_index_building",
589
+ )
590
+
591
+ def install_managed(self) -> Path:
592
+ """Provision the exact Local runtime for later offline use."""
593
+
594
+ npm = shutil.which("npm", path=self.environment.get("PATH"))
595
+ if not npm:
596
+ raise QmdAdapterError(f"cannot install QMD {QMD_VERSION}: npm is unavailable")
597
+ prefix = self._managed_prefix()
598
+ prefix.mkdir(parents=True, exist_ok=True, mode=0o700)
599
+ completed = self._run_raw(
600
+ (
601
+ npm,
602
+ "install",
603
+ "--prefix",
604
+ str(prefix),
605
+ "--no-save",
606
+ "--no-package-lock",
607
+ f"@tobilu/qmd@{QMD_VERSION}",
608
+ ),
609
+ timeout=900,
610
+ )
611
+ if completed.returncode != 0:
612
+ raise QmdAdapterError(self._failure_message("managed qmd install", completed))
613
+ binary = self._managed_binary()
614
+ if not binary.is_file() or not os.access(binary, os.X_OK):
615
+ raise QmdAdapterError("managed QMD install completed without an executable")
616
+ self._resolved_command = (str(binary),)
617
+ self._version_checked = False
618
+ self._ensure_version()
619
+ return binary
620
+
621
+ def _schedule_embed(self) -> bool:
622
+ if self.environment.get("EGREGORE_SEARCH_NO_WARM", "0") == "1":
623
+ return False
624
+ if self._semantic_state_current():
625
+ return False
626
+
627
+ with self._lifecycle_lock():
628
+ lock = self._embed_lock_path()
629
+ if self._live_lock(lock):
630
+ return True
631
+ lock.parent.mkdir(parents=True, exist_ok=True)
632
+ source_revision = self._source_revision()
633
+ command = [
634
+ sys.executable,
635
+ "-m",
636
+ "egregore_runtime.adapters.qmd_embed_worker",
637
+ "--repository-root",
638
+ str(self.repository_root),
639
+ "--memory-root",
640
+ str(self.memory_root),
641
+ "--collection",
642
+ self.collection,
643
+ "--collection-purpose",
644
+ self.collection_purpose,
645
+ "--source-revision",
646
+ source_revision,
647
+ ]
648
+ worker_environment = dict(self.environment)
649
+ package_root = str(Path(__file__).resolve().parents[2])
650
+ existing_pythonpath = worker_environment.get("PYTHONPATH")
651
+ worker_environment["PYTHONPATH"] = (
652
+ f"{package_root}{os.pathsep}{existing_pythonpath}"
653
+ if existing_pythonpath
654
+ else package_root
655
+ )
656
+ try:
657
+ process = subprocess.Popen(
658
+ command,
659
+ cwd=self.repository_root,
660
+ env=worker_environment,
661
+ stdin=subprocess.DEVNULL,
662
+ stdout=subprocess.DEVNULL,
663
+ stderr=subprocess.DEVNULL,
664
+ start_new_session=True,
665
+ umask=0o077,
666
+ )
667
+ except OSError as exc:
668
+ raise QmdAdapterError(f"could not start background embedding: {exc}") from exc
669
+ self._atomic_json(
670
+ lock,
671
+ {
672
+ "pid": process.pid,
673
+ "started_at": int(time.time()),
674
+ "source_revision": source_revision,
675
+ "index_spec_version": INDEX_SPEC_VERSION,
676
+ },
677
+ )
678
+ return True
679
+
680
+ # Explicit maintenance operation used by the compatibility shell adapter.
681
+ def embed_foreground(self) -> RetrieverHealth:
682
+ self._ensure_collection(force=True)
683
+ # Read the covered revision before embedding: the vectors will cover
684
+ # at least everything indexed now. Under-claiming is safe (the next
685
+ # cycle re-embeds); claiming a revision the vectors do not cover
686
+ # would silently strand newer documents without embeddings.
687
+ covered_revision = self._indexed_source_revision()
688
+ started = time.monotonic()
689
+ completed = self._run(("embed", "-c", self.collection), timeout=3600)
690
+ log = self._runtime_root() / "embedding.log"
691
+ log.write_text(
692
+ f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}\n",
693
+ encoding="utf-8",
694
+ )
695
+ os.chmod(log, 0o600)
696
+ if completed.returncode != 0:
697
+ raise QmdAdapterError(self._failure_message("qmd embed", completed))
698
+ self._source_revision_cache = None
699
+ if covered_revision != "unknown":
700
+ # Atomic advance: the semantic revision moves to what the vectors
701
+ # actually represent — never to a newer canonical revision that
702
+ # changed mid-embed. A stale-but-complete receipt keeps hybrid
703
+ # serving until the next build completes.
704
+ self._write_semantic_state(
705
+ source_revision=covered_revision,
706
+ duration_seconds=time.monotonic() - started,
707
+ )
708
+ return self.health()
709
+
710
+ # -- Query execution ----------------------------------------------------
711
+
712
+ def _query(
713
+ self,
714
+ searches: list[dict[str, str]],
715
+ limit: int,
716
+ intent: str,
717
+ ) -> list[dict[str, Any]]:
718
+ if self._persistent_enabled() and self._ensure_daemon():
719
+ payload = {
720
+ "searches": searches,
721
+ "collections": [self.collection],
722
+ "limit": limit,
723
+ "candidateLimit": max(limit, 40),
724
+ "rerank": False,
725
+ "intent": intent,
726
+ }
727
+ try:
728
+ request = urllib.request.Request(
729
+ f"{self._daemon_url()}/query",
730
+ data=json.dumps(payload).encode("utf-8"),
731
+ headers={"Content-Type": "application/json"},
732
+ method="POST",
733
+ )
734
+ with urllib.request.urlopen(request, timeout=30) as response:
735
+ body = json.loads(response.read().decode("utf-8"))
736
+ results = body.get("results") if isinstance(body, dict) else None
737
+ if isinstance(results, list):
738
+ return [item for item in results if isinstance(item, dict)]
739
+ except (OSError, ValueError, urllib.error.URLError) as exc:
740
+ self._daemon_available = False
741
+ # A healthy daemon is only an accelerator; the pinned CLI is
742
+ # the safe compatibility fallback.
743
+ daemon_error = str(exc)
744
+ else:
745
+ daemon_error = "daemon returned an invalid result payload"
746
+ else:
747
+ daemon_error = "daemon unavailable"
748
+
749
+ query_document = "\n".join(
750
+ f"{item['type']}: {self._single_line(item['query'])}" for item in searches
751
+ )
752
+ completed = self._run(
753
+ (
754
+ "query",
755
+ query_document,
756
+ "-c",
757
+ self.collection,
758
+ "-n",
759
+ str(limit),
760
+ "--no-rerank",
761
+ "--format",
762
+ "json",
763
+ "--full-path",
764
+ ),
765
+ timeout=60,
766
+ )
767
+ if completed.returncode != 0:
768
+ message = self._failure_message("typed qmd query", completed)
769
+ if daemon_error != "daemon unavailable":
770
+ message = f"{message} (persistent path: {daemon_error})"
771
+ raise QmdAdapterError(message)
772
+ return self._parse_cli_results(completed.stdout)
773
+
774
+ def _typed_searches(
775
+ self, request: RetrievalRequest, mode: RetrievalMode
776
+ ) -> list[dict[str, str]]:
777
+ if mode is RetrievalMode.LEX:
778
+ terms = request.lex or request.vec
779
+ return [{"type": "lex", "query": term} for term in terms if term.strip()]
780
+ if mode is RetrievalMode.VEC:
781
+ terms = request.vec or request.lex
782
+ return [{"type": "vec", "query": term} for term in terms if term.strip()]
783
+ searches = [
784
+ {"type": "lex", "query": term} for term in request.lex if term.strip()
785
+ ]
786
+ searches.extend(
787
+ {"type": "vec", "query": term} for term in request.vec if term.strip()
788
+ )
789
+ return searches
790
+
791
+ def _authorized_hits(
792
+ self,
793
+ raw_hits: Sequence[Mapping[str, Any]],
794
+ request: RetrievalRequest,
795
+ executed_mode: RetrievalMode,
796
+ ) -> list[RetrievalHit]:
797
+ accepted: list[tuple[Mapping[str, Any], str, _DocumentMetadata]] = []
798
+ for raw in raw_hits:
799
+ canonical_path = self._canonical_path(str(raw.get("file", "")))
800
+ if canonical_path is None or not self._scope_allows(canonical_path, request):
801
+ continue
802
+ metadata = self._metadata(canonical_path)
803
+ if request.allowed_artifact_ids and metadata.artifact_id not in request.allowed_artifact_ids:
804
+ continue
805
+ if request.artifact_types and metadata.artifact_type not in request.artifact_types:
806
+ continue
807
+ if request.workstream and metadata.workstream != request.workstream:
808
+ continue
809
+ if request.not_before is not None:
810
+ if metadata.observed_at is None:
811
+ continue
812
+ if metadata.observed_at < request.not_before.astimezone(UTC):
813
+ continue
814
+ accepted.append((raw, canonical_path, metadata))
815
+
816
+ # Exact canonical names are source-selection metadata, not generated
817
+ # query terms or a model reranker. If the user names a returned title
818
+ # verbatim, prefer that source while preserving QMD/RRF order otherwise.
819
+ accepted.sort(
820
+ key=lambda candidate: (
821
+ 0 if self._task_names_document(request.task, candidate[2]) else 1
822
+ )
823
+ )
824
+
825
+ retrieval_types = {
826
+ RetrievalMode.LEX: (RetrievalMode.LEX,),
827
+ RetrievalMode.VEC: (RetrievalMode.VEC,),
828
+ RetrievalMode.HYBRID: (RetrievalMode.LEX, RetrievalMode.VEC),
829
+ }[executed_mode]
830
+ return [
831
+ RetrievalHit(
832
+ artifact_id=metadata.artifact_id,
833
+ canonical_path=canonical_path,
834
+ rank=rank,
835
+ score=float(raw.get("score") or 0),
836
+ retrieval_types=retrieval_types,
837
+ passage=self._passage(raw),
838
+ passage_id=(
839
+ f"{metadata.artifact_id}:L{raw['line']}" if raw.get("line") else None
840
+ ),
841
+ revision=metadata.revision,
842
+ content_hash=metadata.content_hash,
843
+ observed_at=metadata.observed_at,
844
+ )
845
+ for rank, (raw, canonical_path, metadata) in enumerate(accepted, start=1)
846
+ ]
847
+
848
+ # -- QMD lifecycle ------------------------------------------------------
849
+
850
+ def _ensure_ready(self, warnings: list[str]) -> None:
851
+ if not self.memory_root.is_dir():
852
+ raise QmdAdapterError(f"canonical memory is unavailable: {self.memory_root}")
853
+ self._ensure_version()
854
+ if self._index_state_current():
855
+ return
856
+ self._ensure_collection(force=True)
857
+ completed = self._run(("update",), timeout=300)
858
+ if completed.returncode != 0:
859
+ warnings.append(
860
+ self._failure_message(
861
+ "incremental qmd update",
862
+ completed,
863
+ )
864
+ + "; results may omit recent canonical writes"
865
+ )
866
+ else:
867
+ self._write_index_state()
868
+
869
+ def _ensure_collection(self, *, force: bool = False) -> None:
870
+ self._ensure_version()
871
+ if not force and self._index_state_current():
872
+ return
873
+ listed = self._run(("collection", "list"), timeout=30)
874
+ names = self._collection_names(listed.stdout) if listed.returncode == 0 else ()
875
+ for name in names:
876
+ registered_path = self._collection_path(name)
877
+ if name == self.collection:
878
+ if registered_path == self.memory_root:
879
+ return
880
+ raise QmdAdapterError(
881
+ f"QMD collection {name!r} belongs to {registered_path or 'an unknown path'}; "
882
+ "refusing to reuse a foreign corpus"
883
+ )
884
+ if registered_path == self.memory_root:
885
+ # QMD permits only one collection registration per path. Keep
886
+ # the existing disposable index and migrate its legacy name to
887
+ # the instance-scoped name instead of deleting/re-embedding.
888
+ renamed = self._run(
889
+ ("collection", "rename", name, self.collection), timeout=60
890
+ )
891
+ if renamed.returncode != 0:
892
+ raise QmdAdapterError(
893
+ self._failure_message("qmd collection name migration", renamed)
894
+ )
895
+ return
896
+ added = self._run(
897
+ ("collection", "add", str(self.memory_root), "--name", self.collection),
898
+ timeout=60,
899
+ )
900
+ if added.returncode != 0:
901
+ raise QmdAdapterError(self._failure_message("qmd collection add", added))
902
+
903
+ @staticmethod
904
+ def _collection_names(output: str) -> tuple[str, ...]:
905
+ return tuple(
906
+ match.group(1)
907
+ for match in re.finditer(
908
+ r"^\s*([A-Za-z0-9_.-]+)\s+\(qmd://[^)]+/\)\s*$",
909
+ output,
910
+ re.MULTILINE,
911
+ )
912
+ )
913
+
914
+ def _collection_path(self, name: str) -> Path | None:
915
+ shown = self._run(("collection", "show", name), timeout=30)
916
+ if shown.returncode != 0:
917
+ return None
918
+ match = re.search(r"^\s*Path:\s+(.+?)\s*$", shown.stdout, re.MULTILINE)
919
+ if not match:
920
+ return None
921
+ return Path(match.group(1)).expanduser().resolve()
922
+
923
+ def _ensure_version(self) -> None:
924
+ if self._version_checked:
925
+ return
926
+ command = self._command()
927
+ if self._version_checked:
928
+ return
929
+ completed = self._run_raw((*command, "--version"), timeout=20)
930
+ output = f"{completed.stdout}\n{completed.stderr}"
931
+ if completed.returncode != 0 or not re.search(
932
+ rf"\b{re.escape(QMD_VERSION)}\b", output
933
+ ):
934
+ actual = output.strip() or f"exit {completed.returncode}"
935
+ raise QmdAdapterError(f"QMD {QMD_VERSION} is required; adapter resolved {actual!r}")
936
+ self._version_checked = True
937
+
938
+ def _command(self) -> tuple[str, ...]:
939
+ if self._resolved_command is not None:
940
+ return self._resolved_command
941
+ if self._configured_command:
942
+ self._resolved_command = self._configured_command
943
+ return self._resolved_command
944
+
945
+ override = self.environment.get("EGREGORE_QMD_BIN")
946
+ if override:
947
+ self._resolved_command = (override,)
948
+ return self._resolved_command
949
+
950
+ managed = self._managed_binary()
951
+ if managed.is_file() and os.access(managed, os.X_OK):
952
+ self._resolved_command = (str(managed),)
953
+ # The managed prefix embeds the exact pinned version in its path
954
+ # (qmd-<QMD_VERSION>/) and install_managed verified the binary at
955
+ # provisioning time — the same assertion the npx selector makes.
956
+ # Skipping the per-process --version spawn saves a Node startup
957
+ # on every boot-path health/status call.
958
+ self._version_checked = True
959
+ return self._resolved_command
960
+
961
+ # Never a PATH global and never npx: those resolve through machine
962
+ # state the Runtime does not own (a stale global or npx cache built
963
+ # against another Node ABI produced exactly the dlopen failure the
964
+ # Codex black-box hit). A missing managed install is provisioned now
965
+ # into the instance-owned prefix; if that fails, retrieval fails as a
966
+ # Runtime failure — degrading to unmanaged binaries is never correct.
967
+ binary = self.install_managed()
968
+ self._resolved_command = (str(binary),)
969
+ self._version_checked = True
970
+ return self._resolved_command
971
+
972
+ def _managed_prefix(self) -> Path:
973
+ """Install target for the managed QMD — instance-owned by default.
974
+
975
+ A shared install couples every instance to whichever Node built its
976
+ native modules; scoping the prefix to the instance runtime root keeps
977
+ upgrades, ABI compatibility, and removal per-instance. An explicit
978
+ EGREGORE_RUNTIME_ROOT keeps the historical shared layout for callers
979
+ that opt into it.
980
+ """
981
+ runtime_root = self.environment.get("EGREGORE_RUNTIME_ROOT")
982
+ if runtime_root:
983
+ return Path(runtime_root).expanduser() / f"qmd-{QMD_VERSION}"
984
+ return self._runtime_root() / f"qmd-{QMD_VERSION}"
985
+
986
+ def _managed_binary(self) -> Path:
987
+ instance = self._managed_prefix() / "node_modules" / ".bin" / "qmd"
988
+ if instance.is_file():
989
+ return instance
990
+ # Existing installs provisioned the shared, version-addressed prefix;
991
+ # keep resolving it so an upgrade is adoption, never breakage.
992
+ legacy = Path.home() / ".egregore" / "runtime" / f"qmd-{QMD_VERSION}"
993
+ legacy_binary = legacy / "node_modules" / ".bin" / "qmd"
994
+ if legacy_binary.is_file():
995
+ return legacy_binary
996
+ return instance
997
+
998
+ def _persistent_enabled(self) -> bool:
999
+ return self.environment.get("EGREGORE_QMD_PERSISTENT", "1") != "0"
1000
+
1001
+ def _ensure_daemon(self) -> bool:
1002
+ if not self._persistent_enabled():
1003
+ return False
1004
+ with self._lifecycle_lock():
1005
+ if self._owned_runtime() is not None:
1006
+ self._daemon_available = True
1007
+ return True
1008
+
1009
+ stale = self._read_runtime_metadata()
1010
+ if stale and self._static_runtime_matches(stale):
1011
+ # Signal only a process whose complete QMD command identity is
1012
+ # still ours. Foreign listeners and recycled PIDs are untouched.
1013
+ if (
1014
+ self._process_matches(stale.pid, stale.port)
1015
+ and self._process_start_identity(stale.pid)
1016
+ == stale.process_started_at
1017
+ ):
1018
+ self._run(("mcp", "stop"), timeout=20)
1019
+ self._cleanup_stale_runtime_files()
1020
+
1021
+ preferred = self._preferred_port()
1022
+ port = preferred if self._port_available(preferred) else self._allocate_port()
1023
+ started = self._run(
1024
+ ("mcp", "--http", "--daemon", "--port", str(port)),
1025
+ timeout=20,
1026
+ )
1027
+ if started.returncode != 0:
1028
+ # A bind race must not cause adoption of the process that won.
1029
+ port = self._allocate_port()
1030
+ started = self._run(
1031
+ ("mcp", "--http", "--daemon", "--port", str(port)),
1032
+ timeout=20,
1033
+ )
1034
+ if started.returncode != 0:
1035
+ return False
1036
+ pid = self._started_pid(started.stdout)
1037
+ if pid is None:
1038
+ return False
1039
+ for _ in range(100):
1040
+ if self._process_matches(pid, port) and self._daemon_health(port):
1041
+ self._write_runtime_metadata(pid=pid, port=port)
1042
+ self._daemon_available = True
1043
+ return True
1044
+ time.sleep(0.1)
1045
+ return False
1046
+
1047
+ def _daemon_health(self, port: int) -> bool:
1048
+ try:
1049
+ with urllib.request.urlopen(
1050
+ f"http://localhost:{port}/health", timeout=0.3
1051
+ ) as response:
1052
+ payload = json.loads(response.read().decode("utf-8"))
1053
+ return (
1054
+ response.status == 200
1055
+ and isinstance(payload, dict)
1056
+ and payload.get("status") == "ok"
1057
+ )
1058
+ except (OSError, ValueError, urllib.error.URLError):
1059
+ return False
1060
+
1061
+ def _preferred_port(self) -> int:
1062
+ default_port = 20_000 + int(self._instance_hash()[:8], 16) % 20_000
1063
+ if default_port == 8181:
1064
+ default_port += 1
1065
+ raw = self.environment.get("EGREGORE_QMD_PORT", str(default_port))
1066
+ try:
1067
+ port = int(raw)
1068
+ except ValueError as exc:
1069
+ raise QmdAdapterError(f"invalid EGREGORE_QMD_PORT: {raw!r}") from exc
1070
+ if not 1 <= port <= 65535 or port == 8181:
1071
+ raise QmdAdapterError(f"invalid EGREGORE_QMD_PORT: {raw!r}")
1072
+ return port
1073
+
1074
+ def _daemon_url(self) -> str:
1075
+ runtime = self._owned_runtime()
1076
+ if runtime is None:
1077
+ raise QmdAdapterError("instance-owned QMD worker is unavailable")
1078
+ return runtime.endpoint
1079
+
1080
+ def _read_runtime_metadata(self) -> QmdRuntimeMetadata | None:
1081
+ try:
1082
+ value = json.loads(self._runtime_state_path().read_text(encoding="utf-8"))
1083
+ return QmdRuntimeMetadata.from_mapping(value)
1084
+ except (OSError, ValueError, KeyError, TypeError):
1085
+ return None
1086
+
1087
+ def _static_runtime_matches(self, runtime: QmdRuntimeMetadata) -> bool:
1088
+ return (
1089
+ runtime.schema_version == RUNTIME_SCHEMA_VERSION
1090
+ and runtime.instance_id == self._instance_hash()
1091
+ and runtime.qmd_version == QMD_VERSION
1092
+ and runtime.index_spec_version == INDEX_SPEC_VERSION
1093
+ and runtime.index_name == self.index_name
1094
+ and Path(runtime.index_path).resolve() == self._index_path()
1095
+ and runtime.collection == self.collection
1096
+ and Path(runtime.memory_root).resolve() == self.memory_root
1097
+ and runtime.endpoint == f"http://localhost:{runtime.port}"
1098
+ and runtime.port != 8181
1099
+ )
1100
+
1101
+ def _owned_runtime(self) -> QmdRuntimeMetadata | None:
1102
+ runtime = self._read_runtime_metadata()
1103
+ if runtime is None or not self._static_runtime_matches(runtime):
1104
+ return None
1105
+ if not self._process_matches(runtime.pid, runtime.port):
1106
+ return None
1107
+ if self._process_start_identity(runtime.pid) != runtime.process_started_at:
1108
+ return None
1109
+ if not self._daemon_health(runtime.port):
1110
+ return None
1111
+ return runtime
1112
+
1113
+ def _process_matches(self, pid: int, port: int) -> bool:
1114
+ if not self._pid_alive(pid):
1115
+ return False
1116
+ completed = self._run_raw(("ps", "-ww", "-p", str(pid), "-o", "command="), timeout=5)
1117
+ if completed.returncode != 0:
1118
+ return False
1119
+ command = completed.stdout.strip()
1120
+ return (
1121
+ "qmd" in command.lower()
1122
+ and "mcp" in command
1123
+ and f"--index {self.index_name}" in command
1124
+ and f"--port {port}" in command
1125
+ )
1126
+
1127
+ def _process_start_identity(self, pid: int) -> str | None:
1128
+ completed = self._run_raw(
1129
+ ("ps", "-ww", "-p", str(pid), "-o", "lstart="), timeout=5
1130
+ )
1131
+ if completed.returncode != 0 or not completed.stdout.strip():
1132
+ return None
1133
+ return " ".join(completed.stdout.split())
1134
+
1135
+ @staticmethod
1136
+ def _pid_alive(pid: int) -> bool:
1137
+ try:
1138
+ os.kill(pid, 0)
1139
+ return True
1140
+ except (OSError, ValueError):
1141
+ return False
1142
+
1143
+ @staticmethod
1144
+ def _port_available(port: int) -> bool:
1145
+ if port == 8181:
1146
+ return False
1147
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1148
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1149
+ try:
1150
+ probe.bind(("127.0.0.1", port))
1151
+ return True
1152
+ except OSError:
1153
+ return False
1154
+
1155
+ def _allocate_port(self) -> int:
1156
+ for _ in range(20):
1157
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
1158
+ probe.bind(("127.0.0.1", 0))
1159
+ port = int(probe.getsockname()[1])
1160
+ if port != 8181 and self._port_available(port):
1161
+ return port
1162
+ raise QmdAdapterError("could not allocate an instance-owned QMD port")
1163
+
1164
+ def _started_pid(self, output: str) -> int | None:
1165
+ if match := re.search(r"\bPID\s+(\d+)\b", output):
1166
+ return int(match.group(1))
1167
+ candidates = (
1168
+ self._runtime_root() / "cache" / "qmd" / f"mcp-{self.index_name}.pid",
1169
+ self._runtime_root() / "cache" / "qmd" / "mcp.pid",
1170
+ )
1171
+ for candidate in candidates:
1172
+ try:
1173
+ return int(candidate.read_text(encoding="utf-8").strip())
1174
+ except (OSError, ValueError):
1175
+ continue
1176
+ return None
1177
+
1178
+ def _write_runtime_metadata(self, *, pid: int, port: int) -> None:
1179
+ process_started_at = self._process_start_identity(pid)
1180
+ if process_started_at is None:
1181
+ raise QmdAdapterError("could not verify the QMD worker process birth identity")
1182
+ runtime = QmdRuntimeMetadata(
1183
+ schema_version=RUNTIME_SCHEMA_VERSION,
1184
+ instance_id=self._instance_hash(),
1185
+ generation=uuid.uuid4().hex,
1186
+ pid=pid,
1187
+ port=port,
1188
+ endpoint=f"http://localhost:{port}",
1189
+ qmd_version=QMD_VERSION,
1190
+ index_spec_version=INDEX_SPEC_VERSION,
1191
+ index_name=self.index_name,
1192
+ index_path=str(self._index_path()),
1193
+ collection=self.collection,
1194
+ memory_root=str(self.memory_root),
1195
+ started_at=datetime.now(UTC).isoformat(),
1196
+ process_started_at=process_started_at,
1197
+ )
1198
+ self._atomic_json(self._runtime_state_path(), runtime.to_dict())
1199
+
1200
+ def _cleanup_stale_runtime_files(self) -> None:
1201
+ self._runtime_state_path().unlink(missing_ok=True)
1202
+ for candidate in (
1203
+ self._runtime_root() / "cache" / "qmd" / f"mcp-{self.index_name}.pid",
1204
+ self._runtime_root() / "cache" / "qmd" / "mcp.pid",
1205
+ ):
1206
+ candidate.unlink(missing_ok=True)
1207
+
1208
+ # -- Canonical path, metadata, and revision helpers --------------------
1209
+
1210
+ def _canonical_path(self, raw: str) -> str | None:
1211
+ raw = raw.strip().strip("`")
1212
+ uri_prefix = f"qmd://{self.collection}/"
1213
+ if raw.startswith(uri_prefix):
1214
+ relative = urllib.parse.unquote(raw[len(uri_prefix) :])
1215
+ else:
1216
+ candidate = Path(raw)
1217
+ if not candidate.is_absolute():
1218
+ candidate = (self.repository_root / candidate).resolve()
1219
+ else:
1220
+ candidate = candidate.resolve()
1221
+ try:
1222
+ relative = str(candidate.relative_to(self.memory_root))
1223
+ except ValueError:
1224
+ return None
1225
+ relative = relative.replace("\\", "/").lstrip("/")
1226
+ if not relative or ".." in Path(relative).parts:
1227
+ return None
1228
+ return f"memory/{relative}"
1229
+
1230
+ def _source_path(self, canonical_path: str) -> Path:
1231
+ prefix = "memory/"
1232
+ if not canonical_path.startswith(prefix):
1233
+ raise QmdAdapterError(f"source is outside canonical memory: {canonical_path}")
1234
+ source = (self.memory_root / canonical_path[len(prefix) :]).resolve()
1235
+ try:
1236
+ source.relative_to(self.memory_root)
1237
+ except ValueError as exc:
1238
+ raise QmdAdapterError(f"source is outside canonical memory: {canonical_path}") from exc
1239
+ return source
1240
+
1241
+ def _metadata(self, canonical_path: str) -> _DocumentMetadata:
1242
+ source = self._source_path(canonical_path)
1243
+ fallback_id = canonical_artifact_id("", canonical_path)
1244
+ if not source.is_file():
1245
+ return _DocumentMetadata(artifact_id=fallback_id)
1246
+ body = source.read_text(encoding="utf-8", errors="replace")
1247
+ try:
1248
+ frontmatter, markdown_body = split_frontmatter(body)
1249
+ except ArtifactSchemaError:
1250
+ frontmatter, markdown_body = {}, body
1251
+ title = next(
1252
+ (
1253
+ str(frontmatter[field]).strip()
1254
+ for field in ("title", "topic", "name")
1255
+ if frontmatter.get(field)
1256
+ ),
1257
+ None,
1258
+ )
1259
+ if title is None:
1260
+ heading = re.search(r"^#\s+(.+?)\s*$", markdown_body, re.MULTILINE)
1261
+ if heading:
1262
+ title = heading.group(1).strip()
1263
+ return _DocumentMetadata(
1264
+ artifact_id=canonical_artifact_id(body, canonical_path),
1265
+ title=title,
1266
+ artifact_type=(
1267
+ str(frontmatter.get("artifact_type") or frontmatter.get("type"))
1268
+ if frontmatter.get("artifact_type") or frontmatter.get("type")
1269
+ else None
1270
+ ),
1271
+ workstream=(
1272
+ str(frontmatter.get("workstream") or frontmatter.get("topic"))
1273
+ if frontmatter.get("workstream") or frontmatter.get("topic")
1274
+ else None
1275
+ ),
1276
+ revision=str(frontmatter.get("revision") or self._source_revision()),
1277
+ content_hash=hashlib.sha256(body.encode("utf-8")).hexdigest(),
1278
+ observed_at=self._observed_at(frontmatter, canonical_path),
1279
+ )
1280
+
1281
+ @staticmethod
1282
+ def _observed_at(
1283
+ frontmatter: Mapping[str, Any], canonical_path: str
1284
+ ) -> datetime | None:
1285
+ for field in (
1286
+ "occurred_at",
1287
+ "created_at",
1288
+ "updated_at",
1289
+ "date",
1290
+ "created",
1291
+ "started",
1292
+ "ingested_at",
1293
+ ):
1294
+ raw = frontmatter.get(field)
1295
+ if raw in (None, ""):
1296
+ continue
1297
+ value = str(raw).strip()
1298
+ try:
1299
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
1300
+ except ValueError:
1301
+ continue
1302
+ return (
1303
+ parsed.replace(tzinfo=UTC)
1304
+ if parsed.tzinfo is None
1305
+ else parsed.astimezone(UTC)
1306
+ )
1307
+
1308
+ match = re.search(r"(?:^|/)(20\d{2})-(\d{2})(?:/|-)(\d{2})(?:[-/]|$)", canonical_path)
1309
+ if match:
1310
+ try:
1311
+ return datetime(
1312
+ int(match.group(1)),
1313
+ int(match.group(2)),
1314
+ int(match.group(3)),
1315
+ tzinfo=UTC,
1316
+ )
1317
+ except ValueError:
1318
+ return None
1319
+ return None
1320
+
1321
+ @staticmethod
1322
+ def _task_names_document(task: str, metadata: _DocumentMetadata) -> bool:
1323
+ if not metadata.title:
1324
+ return False
1325
+
1326
+ def normalized(value: str) -> str:
1327
+ return " ".join(re.findall(r"[a-z0-9]+", value.lower()))
1328
+
1329
+ title = normalized(metadata.title)
1330
+ return len(title) >= 8 and title in normalized(task)
1331
+
1332
+ def _scope_allows(self, canonical_path: str, request: RetrievalRequest) -> bool:
1333
+ # Fail closed: a request that carries no authorized scope gets no
1334
+ # hits. Observe always sets scopes; a direct caller without them is
1335
+ # unauthorized by construction, not universally entitled.
1336
+ if not request.authorized_scopes:
1337
+ return False
1338
+ for raw_scope in request.authorized_scopes:
1339
+ scope = raw_scope.strip().replace("\\", "/").rstrip("/")
1340
+ if scope == "memory":
1341
+ return True
1342
+ if not scope.startswith("memory/"):
1343
+ scope = f"memory/{scope.lstrip('/')}"
1344
+ if canonical_path == scope or canonical_path.startswith(f"{scope}/"):
1345
+ return True
1346
+ return False
1347
+
1348
+ def _source_revision(self) -> str:
1349
+ if self._source_revision_cache is not None:
1350
+ return self._source_revision_cache
1351
+ completed = self._run_raw(
1352
+ ("git", "-C", str(self.memory_root), "rev-parse", "HEAD"), timeout=10
1353
+ )
1354
+ if completed.returncode == 0 and completed.stdout.strip():
1355
+ head = completed.stdout.strip()
1356
+ changes = self._run_raw(
1357
+ (
1358
+ "git", "-C", str(self.memory_root), "status", "--porcelain=v1",
1359
+ "--untracked-files=all", "--", "*.md",
1360
+ ),
1361
+ timeout=30,
1362
+ )
1363
+ if changes.returncode == 0 and not changes.stdout.strip():
1364
+ revision = f"git:{head}"
1365
+ else:
1366
+ revision = f"git:{head}+dirty:{self._markdown_manifest_hash()}"
1367
+ else:
1368
+ revision = f"filesystem:{self._markdown_manifest_hash()}"
1369
+ self._source_revision_cache = revision
1370
+ return revision
1371
+
1372
+ def _markdown_manifest_hash(self) -> str:
1373
+ digest = hashlib.sha256()
1374
+ for source in sorted(self.memory_root.rglob("*.md")):
1375
+ if not source.is_file():
1376
+ continue
1377
+ relative = source.relative_to(self.memory_root).as_posix()
1378
+ digest.update(relative.encode("utf-8"))
1379
+ digest.update(b"\0")
1380
+ try:
1381
+ digest.update(source.read_bytes())
1382
+ except OSError:
1383
+ digest.update(b"<unreadable>")
1384
+ digest.update(b"\0")
1385
+ return digest.hexdigest()[:20]
1386
+
1387
+ def _index_revision(self) -> str:
1388
+ index = self._index_path()
1389
+ stat_material = "missing"
1390
+ try:
1391
+ stat = index.stat()
1392
+ stat_material = f"{stat.st_mtime_ns}:{stat.st_size}"
1393
+ except OSError:
1394
+ pass
1395
+ material = (
1396
+ f"{INDEX_SPEC_VERSION}:{self.collection}:"
1397
+ f"{self._indexed_source_revision()}:{stat_material}"
1398
+ )
1399
+ return "qmd:" + hashlib.sha256(material.encode()).hexdigest()[:20]
1400
+
1401
+ def _index_path(self) -> Path:
1402
+ override = self.environment.get("EGREGORE_QMD_INDEX_PATH")
1403
+ if override:
1404
+ return Path(override).expanduser().resolve()
1405
+ return self._runtime_root() / "index.sqlite"
1406
+
1407
+ def _models_path(self) -> Path:
1408
+ return self._runtime_root() / "cache" / "qmd" / "models"
1409
+
1410
+ def _index_state_path(self) -> Path:
1411
+ safe_collection = re.sub(r"[^A-Za-z0-9_.-]", "-", self.collection)
1412
+ return self._index_path().parent / f"egregore-index-{safe_collection}.json"
1413
+
1414
+ def _index_state_current(self) -> bool:
1415
+ if not self._index_path().is_file():
1416
+ return False
1417
+ try:
1418
+ state = json.loads(self._index_state_path().read_text(encoding="utf-8"))
1419
+ except (OSError, ValueError, TypeError):
1420
+ return False
1421
+ return state == {
1422
+ "adapter_version": QMD_VERSION,
1423
+ "collection": self.collection,
1424
+ "index_name": self.index_name,
1425
+ "index_path": str(self._index_path()),
1426
+ "instance_id": self._instance_hash(),
1427
+ "index_spec_version": INDEX_SPEC_VERSION,
1428
+ "memory_root": str(self.memory_root),
1429
+ "source_revision": self._source_revision(),
1430
+ }
1431
+
1432
+ def _index_state_usable(self) -> bool:
1433
+ """The BM25 index exists for this exact identity, revision aside.
1434
+
1435
+ A canonical write leaves the index trailing the source revision, not
1436
+ unusable — retrieval refreshes it inline before querying. Usability
1437
+ breaks only on a missing index or a different index identity.
1438
+ """
1439
+ if not self._index_path().is_file():
1440
+ return False
1441
+ try:
1442
+ state = json.loads(self._index_state_path().read_text(encoding="utf-8"))
1443
+ except (OSError, ValueError, TypeError):
1444
+ return False
1445
+ expected = {
1446
+ "adapter_version": QMD_VERSION,
1447
+ "collection": self.collection,
1448
+ "index_name": self.index_name,
1449
+ "index_path": str(self._index_path()),
1450
+ "instance_id": self._instance_hash(),
1451
+ "index_spec_version": INDEX_SPEC_VERSION,
1452
+ "memory_root": str(self.memory_root),
1453
+ }
1454
+ return isinstance(state, dict) and all(
1455
+ state.get(key) == value for key, value in expected.items()
1456
+ )
1457
+
1458
+ def _indexed_source_revision(self) -> str:
1459
+ try:
1460
+ state = json.loads(self._index_state_path().read_text(encoding="utf-8"))
1461
+ revision = state.get("source_revision")
1462
+ return str(revision) if revision else "unknown"
1463
+ except (OSError, ValueError, TypeError):
1464
+ return "unknown"
1465
+
1466
+ def _write_index_state(self) -> None:
1467
+ state_path = self._index_state_path()
1468
+ state_path.parent.mkdir(parents=True, exist_ok=True)
1469
+ state = {
1470
+ "adapter_version": QMD_VERSION,
1471
+ "collection": self.collection,
1472
+ "index_name": self.index_name,
1473
+ "index_path": str(self._index_path()),
1474
+ "instance_id": self._instance_hash(),
1475
+ "index_spec_version": INDEX_SPEC_VERSION,
1476
+ "memory_root": str(self.memory_root),
1477
+ "source_revision": self._source_revision(),
1478
+ }
1479
+ self._atomic_json(state_path, state)
1480
+
1481
+ def _embedding_model_available(self) -> bool:
1482
+ models = self._models_path()
1483
+ return any(models.glob("*embeddinggemma*")) if models.is_dir() else False
1484
+
1485
+ @staticmethod
1486
+ def _status_counts(status: str) -> dict[str, int]:
1487
+ def count(pattern: str) -> int:
1488
+ match = re.search(pattern, status, re.IGNORECASE)
1489
+ return int(match.group(1)) if match else 0
1490
+
1491
+ return {
1492
+ "documents": count(r"Total:\s*(\d+)\s+files?\s+indexed"),
1493
+ "vectors": count(r"Vectors:\s*(\d+)\s+embedded"),
1494
+ "pending": count(r"Pending:\s*(\d+)"),
1495
+ }
1496
+
1497
+ def _semantic_state_path(self) -> Path:
1498
+ return self._runtime_root() / "semantic-state.json"
1499
+
1500
+ def _semantic_state_current(self) -> bool:
1501
+ if not self._index_state_current():
1502
+ return False
1503
+ try:
1504
+ state = json.loads(self._semantic_state_path().read_text(encoding="utf-8"))
1505
+ except (OSError, ValueError, TypeError):
1506
+ return False
1507
+ expected = {
1508
+ "collection": self.collection,
1509
+ "index_name": self.index_name,
1510
+ "index_spec_version": INDEX_SPEC_VERSION,
1511
+ "source_revision": self._source_revision(),
1512
+ }
1513
+ return all(state.get(key) == value for key, value in expected.items())
1514
+
1515
+ def _semantic_state_usable(self) -> bool:
1516
+ """A complete vector build exists for this exact index identity.
1517
+
1518
+ Unlike currentness, usability survives canonical writes: the last
1519
+ complete vectors keep serving hybrid queries alongside current BM25
1520
+ while a newer embedding builds. Only an absent index, an absent
1521
+ receipt, or a different index identity makes vectors unusable.
1522
+ """
1523
+ if not self._index_path().is_file():
1524
+ return False
1525
+ try:
1526
+ state = json.loads(self._semantic_state_path().read_text(encoding="utf-8"))
1527
+ except (OSError, ValueError, TypeError):
1528
+ return False
1529
+ identity = {
1530
+ "collection": self.collection,
1531
+ "index_name": self.index_name,
1532
+ "index_spec_version": INDEX_SPEC_VERSION,
1533
+ }
1534
+ return all(
1535
+ state.get(key) == value for key, value in identity.items()
1536
+ ) and bool(state.get("source_revision"))
1537
+
1538
+ def _semantic_source_revision(self) -> str | None:
1539
+ """The canonical revision the last complete vector build represents."""
1540
+ try:
1541
+ state = json.loads(self._semantic_state_path().read_text(encoding="utf-8"))
1542
+ except (OSError, ValueError, TypeError):
1543
+ return None
1544
+ revision = state.get("source_revision")
1545
+ return str(revision) if revision else None
1546
+
1547
+ def _write_semantic_state(
1548
+ self,
1549
+ *,
1550
+ source_revision: str,
1551
+ duration_seconds: float | None = None,
1552
+ ) -> None:
1553
+ self._atomic_json(
1554
+ self._semantic_state_path(),
1555
+ {
1556
+ "schema_version": "egregore-qmd-semantic/v1",
1557
+ "collection": self.collection,
1558
+ "index_name": self.index_name,
1559
+ "index_spec_version": INDEX_SPEC_VERSION,
1560
+ "source_revision": source_revision,
1561
+ "completed_at": datetime.now(UTC).isoformat(),
1562
+ "duration_seconds": round(duration_seconds, 3)
1563
+ if duration_seconds is not None
1564
+ else None,
1565
+ },
1566
+ )
1567
+
1568
+ def _embedding_state(self, status: str, *, semantic_ready: bool = False) -> str:
1569
+ if self._live_lock(self._embed_lock_path()):
1570
+ return "running"
1571
+ if semantic_ready:
1572
+ return "ready"
1573
+ if not self._embedding_model_available():
1574
+ return "missing_model"
1575
+ counts = self._status_counts(status)
1576
+ if counts["pending"] > 0 or counts["documents"] > 0:
1577
+ return "pending"
1578
+ return "not_built"
1579
+
1580
+ def _embed_lock_path(self) -> Path:
1581
+ safe_collection = re.sub(r"[^A-Za-z0-9_.-]", "-", self.collection)
1582
+ cache = self._index_path().parent
1583
+ return cache / f"egregore-embed-{safe_collection}.json"
1584
+
1585
+ def _live_lock(self, lock: Path) -> bool:
1586
+ # Liveness is process identity, not revision equality: a worker whose
1587
+ # target revision was outrun by a canonical write is still embedding
1588
+ # and converges through its own retry loop. Treating it as dead would
1589
+ # spawn a second concurrent embedder against the same SQLite index.
1590
+ try:
1591
+ data = json.loads(lock.read_text(encoding="utf-8"))
1592
+ pid = int(data["pid"])
1593
+ index_spec_version = str(data["index_spec_version"])
1594
+ except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
1595
+ return False
1596
+ if index_spec_version != INDEX_SPEC_VERSION or not self._pid_alive(pid):
1597
+ lock.unlink(missing_ok=True)
1598
+ return False
1599
+ return True
1600
+
1601
+ @staticmethod
1602
+ def _atomic_json(path: Path, value: Mapping[str, Any]) -> None:
1603
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
1604
+ os.chmod(path.parent, 0o700)
1605
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
1606
+ temporary.write_text(json.dumps(value, sort_keys=True) + "\n", encoding="utf-8")
1607
+ os.chmod(temporary, 0o600)
1608
+ temporary.replace(path)
1609
+ os.chmod(path, 0o600)
1610
+
1611
+ def _config_slug(self) -> str:
1612
+ try:
1613
+ config = json.loads((self.repository_root / "egregore.json").read_text())
1614
+ slug = str(config.get("slug") or "egregore").strip()
1615
+ return slug or "egregore"
1616
+ except (OSError, ValueError, TypeError):
1617
+ return "egregore"
1618
+
1619
+ def _instance_hash(self) -> str:
1620
+ # Worktrees for one Egregore share the same canonical memory checkout.
1621
+ # Namespace by that stable root so they reuse one disposable index and
1622
+ # daemon instead of renaming the collection on every branch switch.
1623
+ material = str(self.memory_root)
1624
+ return hashlib.sha256(material.encode()).hexdigest()[:16]
1625
+
1626
+ def _default_collection(self) -> str:
1627
+ slug = re.sub(r"[^A-Za-z0-9_.-]", "-", self._config_slug())
1628
+ return f"{slug}-{self.collection_purpose}-{self._instance_hash()[:12]}"
1629
+
1630
+ # -- Process and output helpers ----------------------------------------
1631
+
1632
+ def _run(self, arguments: Sequence[str], *, timeout: int) -> subprocess.CompletedProcess[str]:
1633
+ self._ensure_runtime_layout()
1634
+ self._ensure_version()
1635
+ return self._run_raw(
1636
+ (*self._command(), "--index", self.index_name, *arguments),
1637
+ timeout=timeout,
1638
+ )
1639
+
1640
+ def _run_raw(
1641
+ self, command: Sequence[str], *, timeout: int
1642
+ ) -> subprocess.CompletedProcess[str]:
1643
+ try:
1644
+ return subprocess.run(
1645
+ command,
1646
+ cwd=self.repository_root,
1647
+ env=self.environment,
1648
+ text=True,
1649
+ capture_output=True,
1650
+ timeout=timeout,
1651
+ check=False,
1652
+ umask=0o077,
1653
+ )
1654
+ except (OSError, subprocess.TimeoutExpired) as exc:
1655
+ return subprocess.CompletedProcess(command, 124, "", str(exc))
1656
+
1657
+ @staticmethod
1658
+ def _parse_cli_results(output: str) -> list[dict[str, Any]]:
1659
+ start = output.find("[")
1660
+ end = output.rfind("]")
1661
+ if start == -1 or end < start:
1662
+ if "No results" in output or not output.strip():
1663
+ return []
1664
+ raise QmdAdapterError("typed qmd query returned non-JSON output")
1665
+ try:
1666
+ payload = json.loads(output[start : end + 1])
1667
+ except json.JSONDecodeError as exc:
1668
+ raise QmdAdapterError("typed qmd query returned invalid JSON") from exc
1669
+ if not isinstance(payload, list):
1670
+ raise QmdAdapterError("typed qmd query returned an invalid result shape")
1671
+ return [item for item in payload if isinstance(item, dict)]
1672
+
1673
+ @staticmethod
1674
+ def _passage(raw: Mapping[str, Any]) -> str | None:
1675
+ snippet = raw.get("snippet")
1676
+ if not isinstance(snippet, str) or not snippet.strip():
1677
+ return None
1678
+ return re.sub(r"^\s*\d+:\s?", "", snippet.strip(), flags=re.MULTILINE)
1679
+
1680
+ @staticmethod
1681
+ def _single_line(value: str) -> str:
1682
+ return " ".join(value.splitlines()).strip()
1683
+
1684
+ @staticmethod
1685
+ def _failure_message(
1686
+ operation: str, completed: subprocess.CompletedProcess[str]
1687
+ ) -> str:
1688
+ detail = completed.stderr.strip() or completed.stdout.strip() or "unknown failure"
1689
+ return f"{operation} failed ({completed.returncode}): {detail[:400]}"
1690
+
1691
+
1692
+ __all__ = [
1693
+ "EMBEDDING_MODEL",
1694
+ "INDEX_SPEC_VERSION",
1695
+ "QMD_VERSION",
1696
+ "RERANKING_ENABLED",
1697
+ "RUNTIME_SCHEMA_VERSION",
1698
+ "QmdAdapterError",
1699
+ "QmdLocalRetriever",
1700
+ "QmdRuntimeMetadata",
1701
+ ]