create-egregore 0.21.6 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (719) hide show
  1. package/assets/egregore-launcher.js +58 -6
  2. package/assets/egregore-launcher.sh +15 -2
  3. package/lib/claude-runtime.js +2 -0
  4. package/lib/codex-runtime.js +2 -0
  5. package/lib/setup.js +20 -3
  6. package/package.json +2 -2
  7. package/runtime/claude/.claude/context/commit-format.md +7 -4
  8. package/runtime/claude/.claude/context/handoff-claiming.md +32 -0
  9. package/runtime/claude/.claude/context/notification-consent.md +12 -9
  10. package/runtime/claude/.claude/hooks/boundary-check.sh +14 -331
  11. package/runtime/claude/.claude/hooks/memory-read-guard.sh +9 -1
  12. package/runtime/claude/.claude/rules/voice-bedrock.md +6 -2
  13. package/runtime/claude/.claude/settings.json +1 -1
  14. package/runtime/claude/.claude/skills/commit/SKILL.md +5 -3
  15. package/runtime/claude/.claude/skills/dark-mode/SKILL.md +48 -369
  16. package/runtime/claude/.claude/skills/records/SKILL.md +158 -0
  17. package/runtime/claude/.claude/skills/reflect/SKILL.md +50 -21
  18. package/runtime/claude/.claude/skills/review-pr/SKILL.md +4 -2
  19. package/runtime/claude/.claude/skills/save/SKILL.md +9 -3
  20. package/runtime/claude/.claude/skills/summon/SKILL.md +138 -207
  21. package/runtime/claude/.claude/skills/tui-design/SKILL.md +1 -1
  22. package/runtime/claude/.claude/skills/update/SKILL.md +1 -1
  23. package/runtime/claude/.claude/skills/view/SKILL.md +52 -16
  24. package/runtime/claude/.claude/skills/view/compose-scaffold.html +19 -13
  25. package/runtime/claude/CLAUDE.md +59 -95
  26. package/runtime/claude/bin/agent.sh +231 -7
  27. package/runtime/claude/bin/api-call.sh +44 -23
  28. package/runtime/claude/bin/artifact-register.sh +7 -1
  29. package/runtime/claude/bin/artifact-writeback.sh +1 -1
  30. package/runtime/claude/bin/artifacts.sh +66 -265
  31. package/runtime/claude/bin/attendant.sh +4 -1
  32. package/runtime/claude/bin/boundary-guard.cjs +1848 -0
  33. package/runtime/claude/bin/boundary-hook.sh +41 -0
  34. package/runtime/claude/bin/boundary.sh +15 -155
  35. package/runtime/claude/bin/capture-run.sh +32 -2
  36. package/runtime/claude/bin/codex-render-spec.mjs +24 -46
  37. package/runtime/claude/bin/codex-session-start.sh +19 -0
  38. package/runtime/claude/bin/codex-skill-render.mjs +18 -0
  39. package/runtime/claude/bin/config-get.sh +10 -1
  40. package/runtime/claude/bin/connect-refresh.sh +16 -0
  41. package/runtime/claude/bin/corpus_map.py +5 -0
  42. package/runtime/claude/bin/data/capabilities/claude.json +1480 -0
  43. package/runtime/claude/bin/genui.sh +19 -0
  44. package/runtime/claude/bin/graph-batch.sh +1 -1
  45. package/runtime/claude/bin/graph-op.sh +14 -0
  46. package/runtime/claude/bin/graph.sh +1 -1
  47. package/runtime/claude/bin/handoff-pr-backfill.sh +1 -1
  48. package/runtime/claude/bin/handoff-run.sh +11 -6
  49. package/runtime/claude/bin/ingest-item.sh +1 -1
  50. package/runtime/claude/bin/ingest-surface.html +22 -4
  51. package/runtime/claude/bin/issue.sh +1 -1
  52. package/runtime/claude/bin/knowledge.sh +1 -1
  53. package/runtime/claude/bin/lib/card-delivery.sh +329 -0
  54. package/runtime/claude/bin/lib/config.sh +148 -3
  55. package/runtime/claude/bin/lib/context.sh +38 -67
  56. package/runtime/claude/bin/lib/genui-entry.py +36 -0
  57. package/runtime/claude/bin/lib/git-message.sh +25 -6
  58. package/runtime/claude/bin/lib/git-sync.sh +12 -4
  59. package/runtime/claude/bin/lib/greeting.sh +255 -28
  60. package/runtime/claude/bin/lib/handoff_meta.py +294 -0
  61. package/runtime/claude/bin/lib/health-footer.sh +83 -2
  62. package/runtime/claude/bin/lib/identity.sh +31 -10
  63. package/runtime/claude/bin/lib/instance-registry.sh +71 -0
  64. package/runtime/claude/bin/lib/local_ci_receipt.py +249 -0
  65. package/runtime/claude/bin/lib/metrics.sh +121 -8
  66. package/runtime/claude/bin/lib/notices.sh +36 -0
  67. package/runtime/claude/bin/lib/record_outcomes.py +729 -0
  68. package/runtime/claude/bin/lib/reflexes.py +267 -0
  69. package/runtime/claude/bin/lib/retrieval_scorecard.py +624 -0
  70. package/runtime/claude/bin/lib/session-record-prompt.txt +18 -0
  71. package/runtime/claude/bin/lib/session_record.py +3747 -0
  72. package/runtime/claude/bin/lib/session_trail.py +1983 -0
  73. package/runtime/claude/bin/lib/settings-drift.sh +8 -5
  74. package/runtime/claude/bin/lib/startup-restart.sh +41 -0
  75. package/runtime/claude/bin/lib/worktree-links.sh +92 -0
  76. package/runtime/claude/bin/memory-push.sh +6 -0
  77. package/runtime/claude/bin/merge-gate.sh +498 -0
  78. package/runtime/claude/bin/notify.sh +5 -2
  79. package/runtime/claude/bin/page-access.sh +103 -0
  80. package/runtime/claude/bin/person.sh +13 -2
  81. package/runtime/claude/bin/pi-render-spec.mjs +0 -1
  82. package/runtime/claude/bin/pi-spirit-discovery.mjs +34 -0
  83. package/runtime/claude/bin/post-update.sh +181 -0
  84. package/runtime/claude/bin/prime-render-spec.mjs +0 -1
  85. package/runtime/claude/bin/publish-artifact.sh +89 -42
  86. package/runtime/claude/bin/qa-inventory.sh +2 -2
  87. package/runtime/claude/bin/quest.sh +1 -1
  88. package/runtime/claude/bin/records.sh +11 -0
  89. package/runtime/claude/bin/reflexes/05-vitals.sh +9 -0
  90. package/runtime/claude/bin/reflexes/10-open-items.sh +12 -0
  91. package/runtime/claude/bin/reflexes/15-retrieval.sh +8 -0
  92. package/runtime/claude/bin/reflexes/20-nightly.sh +100 -0
  93. package/runtime/claude/bin/reflexes/20-sandbox.sh +85 -0
  94. package/runtime/claude/bin/reflexes/30-cost.py +116 -0
  95. package/runtime/claude/bin/reflexes/40-shells.py +51 -0
  96. package/runtime/claude/bin/reflexes/README.md +61 -0
  97. package/runtime/claude/bin/reflexes.sh +7 -0
  98. package/runtime/claude/bin/runtime-identity.sh +140 -42
  99. package/runtime/claude/bin/screenshot.sh +77 -0
  100. package/runtime/claude/bin/scroll.sh +1 -1
  101. package/runtime/claude/bin/search.sh +31 -5
  102. package/runtime/claude/bin/session-autosave.sh +6 -3
  103. package/runtime/claude/bin/session-id.sh +147 -0
  104. package/runtime/claude/bin/session-log.sh +19 -0
  105. package/runtime/claude/bin/session-record.sh +26 -0
  106. package/runtime/claude/bin/session-start.sh +121 -151
  107. package/runtime/claude/bin/settings.sh +13 -8
  108. package/runtime/claude/bin/spirit-discovery-claude.sh +4 -0
  109. package/runtime/claude/bin/spirit-discovery-codex.sh +4 -0
  110. package/runtime/claude/bin/spirit-discovery-pi.sh +4 -0
  111. package/runtime/claude/bin/spirit-discovery-prime.sh +4 -0
  112. package/runtime/claude/bin/spirit-discovery.sh +4 -0
  113. package/runtime/claude/bin/startup-check.sh +1 -1
  114. package/runtime/claude/bin/telemetry.sh +11 -4
  115. package/runtime/claude/bin/test-baseline.sh +33 -5
  116. package/runtime/claude/bin/todo.sh +1 -1
  117. package/runtime/claude/bin/worktree-create.sh +15 -45
  118. package/runtime/claude/bin/worktree.sh +6 -0
  119. package/runtime/claude/docs/spirit-discovery.md +96 -0
  120. package/runtime/claude/egregore_runtime/__init__.py +18 -12
  121. package/runtime/claude/egregore_runtime/access.py +608 -0
  122. package/runtime/claude/egregore_runtime/access_cli.py +190 -0
  123. package/runtime/claude/egregore_runtime/adapters/invitation.py +5 -3
  124. package/runtime/claude/egregore_runtime/adapters/qmd.py +521 -47
  125. package/runtime/claude/egregore_runtime/admin_entry.py +56 -0
  126. package/runtime/claude/egregore_runtime/artifacts.py +222 -16
  127. package/runtime/claude/egregore_runtime/contracts.py +27 -0
  128. package/runtime/claude/egregore_runtime/eligibility.py +5 -12
  129. package/runtime/claude/egregore_runtime/episode_binding.py +16 -5
  130. package/runtime/claude/egregore_runtime/github_issues.py +2 -2
  131. package/runtime/claude/egregore_runtime/handoff_lookup.py +9 -20
  132. package/runtime/claude/egregore_runtime/harness_cli.py +674 -149
  133. package/runtime/claude/egregore_runtime/held.py +2436 -0
  134. package/runtime/claude/egregore_runtime/hosted_api.py +137 -0
  135. package/runtime/claude/egregore_runtime/identity.py +196 -65
  136. package/runtime/claude/egregore_runtime/identity_transfer.py +6 -2
  137. package/runtime/claude/egregore_runtime/ingest_cli.py +2 -2
  138. package/runtime/claude/egregore_runtime/ingest_index_cli.py +4 -16
  139. package/runtime/claude/egregore_runtime/installation.py +119 -0
  140. package/runtime/claude/egregore_runtime/investigation.py +64 -41
  141. package/runtime/claude/egregore_runtime/investigation_guidance.py +16 -11
  142. package/runtime/claude/egregore_runtime/issue_cli.py +51 -32
  143. package/runtime/claude/egregore_runtime/issue_reporting.py +4 -3
  144. package/runtime/claude/egregore_runtime/issue_triage_service.py +2 -2
  145. package/runtime/claude/egregore_runtime/issues.py +12 -6
  146. package/runtime/claude/egregore_runtime/knowledge.py +331 -5
  147. package/runtime/claude/egregore_runtime/knowledge_cli.py +57 -17
  148. package/runtime/claude/egregore_runtime/lifecycle.py +88 -22
  149. package/runtime/claude/egregore_runtime/lifecycle_cli.py +21 -13
  150. package/runtime/claude/egregore_runtime/line_ids.py +10 -0
  151. package/runtime/claude/egregore_runtime/memory_push.py +349 -0
  152. package/runtime/claude/egregore_runtime/observe.py +64 -25
  153. package/runtime/claude/egregore_runtime/observers.py +19 -0
  154. package/runtime/claude/egregore_runtime/optional_modules.py +13 -0
  155. package/runtime/claude/egregore_runtime/outputs_cli.py +321 -0
  156. package/runtime/claude/egregore_runtime/policy.py +651 -24
  157. package/runtime/claude/egregore_runtime/private_result.py +2 -1
  158. package/runtime/claude/egregore_runtime/projection.py +946 -0
  159. package/runtime/claude/egregore_runtime/quest_cli.py +20 -23
  160. package/runtime/claude/egregore_runtime/question_cli.py +15 -11
  161. package/runtime/claude/egregore_runtime/questions.py +10 -10
  162. package/runtime/claude/egregore_runtime/quests.py +10 -10
  163. package/runtime/claude/egregore_runtime/record_publish.py +548 -0
  164. package/runtime/claude/egregore_runtime/record_subject.py +564 -0
  165. package/runtime/claude/egregore_runtime/research_ingest.py +2 -4
  166. package/runtime/claude/egregore_runtime/runtime.py +169 -60
  167. package/runtime/claude/egregore_runtime/sandbox.py +194 -0
  168. package/runtime/claude/egregore_runtime/scroll_cli.py +22 -32
  169. package/runtime/claude/egregore_runtime/scrolls.py +23 -15
  170. package/runtime/claude/egregore_runtime/shadow.py +262 -0
  171. package/runtime/claude/egregore_runtime/spirit_discovery.py +487 -0
  172. package/runtime/claude/egregore_runtime/spirit_discovery_cli.py +25 -0
  173. package/runtime/claude/egregore_runtime/spirit_discovery_transport.py +18 -0
  174. package/runtime/claude/egregore_runtime/status.py +101 -44
  175. package/runtime/claude/egregore_runtime/status_cli.py +13 -5
  176. package/runtime/claude/egregore_runtime/sync.py +39 -21
  177. package/runtime/claude/egregore_runtime/taste.py +934 -0
  178. package/runtime/claude/egregore_runtime/taste_cli.py +242 -0
  179. package/runtime/claude/egregore_runtime/taste_rules.py +91 -0
  180. package/runtime/claude/egregore_runtime/telemetry.py +26 -3
  181. package/runtime/claude/egregore_runtime/todo_cli.py +15 -12
  182. package/runtime/claude/egregore_runtime/todos.py +3 -2
  183. package/runtime/claude/egregore_runtime/upgrade.py +85 -58
  184. package/runtime/claude/egregore_runtime/writeback.py +330 -24
  185. package/runtime/claude/manifest.json +65 -5
  186. package/runtime/codex/.claude/context/handoff-claiming.md +32 -0
  187. package/runtime/codex/.claude/context/notification-consent.md +12 -9
  188. package/runtime/codex/.claude/skills/view/SKILL.md +438 -0
  189. package/runtime/codex/.codex/hooks.json +11 -0
  190. package/runtime/codex/.codex/skills/dark-mode/SKILL.md +1 -1
  191. package/runtime/codex/.codex/skills/records/SKILL.md +29 -0
  192. package/runtime/codex/.codex/skills/summon/SKILL.md +1 -1
  193. package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +19 -13
  194. package/runtime/codex/.codex/spec-manifest.json +13 -19
  195. package/runtime/codex/AGENTS.md +54 -105
  196. package/runtime/codex/bin/agent.sh +231 -7
  197. package/runtime/codex/bin/api-call.sh +44 -23
  198. package/runtime/codex/bin/artifact-register.sh +7 -1
  199. package/runtime/codex/bin/artifact-writeback.sh +1 -1
  200. package/runtime/codex/bin/artifacts.sh +66 -265
  201. package/runtime/codex/bin/attendant.sh +4 -1
  202. package/runtime/codex/bin/boundary-guard.cjs +1848 -0
  203. package/runtime/codex/bin/boundary-hook.sh +41 -0
  204. package/runtime/codex/bin/capture-run.sh +32 -2
  205. package/runtime/codex/bin/codex-render-spec.mjs +24 -46
  206. package/runtime/codex/bin/codex-session-start.sh +19 -0
  207. package/runtime/codex/bin/codex-skill-render.mjs +18 -0
  208. package/runtime/codex/bin/config-get.sh +10 -1
  209. package/runtime/codex/bin/connect-refresh.sh +16 -0
  210. package/runtime/codex/bin/corpus_map.py +5 -0
  211. package/runtime/codex/bin/data/capabilities/codex.json +1458 -0
  212. package/runtime/codex/bin/genui.sh +19 -0
  213. package/runtime/codex/bin/graph-batch.sh +1 -1
  214. package/runtime/codex/bin/graph-op.sh +14 -0
  215. package/runtime/codex/bin/graph.sh +1 -1
  216. package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
  217. package/runtime/codex/bin/handoff-run.sh +11 -6
  218. package/runtime/codex/bin/ingest-item.sh +1 -1
  219. package/runtime/codex/bin/ingest-surface.html +22 -4
  220. package/runtime/codex/bin/issue.sh +1 -1
  221. package/runtime/codex/bin/knowledge.sh +1 -1
  222. package/runtime/codex/bin/lib/card-delivery.sh +329 -0
  223. package/runtime/codex/bin/lib/config.sh +148 -3
  224. package/runtime/codex/bin/lib/context.sh +38 -67
  225. package/runtime/codex/bin/lib/genui-entry.py +36 -0
  226. package/runtime/codex/bin/lib/git-message.sh +25 -6
  227. package/runtime/codex/bin/lib/git-sync.sh +12 -4
  228. package/runtime/codex/bin/lib/greeting.sh +255 -28
  229. package/runtime/codex/bin/lib/handoff_meta.py +294 -0
  230. package/runtime/codex/bin/lib/health-footer.sh +83 -2
  231. package/runtime/codex/bin/lib/identity.sh +31 -10
  232. package/runtime/codex/bin/lib/instance-registry.sh +71 -0
  233. package/runtime/codex/bin/lib/local_ci_receipt.py +249 -0
  234. package/runtime/codex/bin/lib/metrics.sh +121 -8
  235. package/runtime/codex/bin/lib/notices.sh +36 -0
  236. package/runtime/codex/bin/lib/record_outcomes.py +729 -0
  237. package/runtime/codex/bin/lib/reflexes.py +267 -0
  238. package/runtime/codex/bin/lib/retrieval_scorecard.py +624 -0
  239. package/runtime/codex/bin/lib/session-record-prompt.txt +18 -0
  240. package/runtime/codex/bin/lib/session_record.py +3747 -0
  241. package/runtime/codex/bin/lib/session_trail.py +1983 -0
  242. package/runtime/codex/bin/lib/settings-drift.sh +8 -5
  243. package/runtime/codex/bin/lib/startup-restart.sh +41 -0
  244. package/runtime/codex/bin/lib/worktree-links.sh +92 -0
  245. package/runtime/codex/bin/memory-push.sh +6 -0
  246. package/runtime/codex/bin/merge-gate.sh +498 -0
  247. package/runtime/codex/bin/notify.sh +5 -2
  248. package/runtime/codex/bin/page-access.sh +103 -0
  249. package/runtime/codex/bin/person.sh +13 -2
  250. package/runtime/codex/bin/pi-render-spec.mjs +0 -1
  251. package/runtime/codex/bin/pi-spirit-discovery.mjs +34 -0
  252. package/runtime/codex/bin/post-update.sh +181 -0
  253. package/runtime/codex/bin/prime-render-spec.mjs +0 -1
  254. package/runtime/codex/bin/publish-artifact.sh +89 -42
  255. package/runtime/codex/bin/qa-inventory.sh +2 -2
  256. package/runtime/codex/bin/quest.sh +1 -1
  257. package/runtime/codex/bin/records.sh +11 -0
  258. package/runtime/codex/bin/reflexes/05-vitals.sh +9 -0
  259. package/runtime/codex/bin/reflexes/10-open-items.sh +12 -0
  260. package/runtime/codex/bin/reflexes/15-retrieval.sh +8 -0
  261. package/runtime/codex/bin/reflexes/20-nightly.sh +100 -0
  262. package/runtime/codex/bin/reflexes/20-sandbox.sh +85 -0
  263. package/runtime/codex/bin/reflexes/30-cost.py +116 -0
  264. package/runtime/codex/bin/reflexes/40-shells.py +51 -0
  265. package/runtime/codex/bin/reflexes/README.md +61 -0
  266. package/runtime/codex/bin/reflexes.sh +7 -0
  267. package/runtime/codex/bin/runtime-identity.sh +140 -42
  268. package/runtime/codex/bin/screenshot.sh +77 -0
  269. package/runtime/codex/bin/scroll.sh +1 -1
  270. package/runtime/codex/bin/search.sh +31 -5
  271. package/runtime/codex/bin/session-autosave.sh +6 -3
  272. package/runtime/codex/bin/session-id.sh +147 -0
  273. package/runtime/codex/bin/session-log.sh +19 -0
  274. package/runtime/codex/bin/session-record.sh +26 -0
  275. package/runtime/codex/bin/settings.sh +13 -8
  276. package/runtime/codex/bin/spirit-discovery-claude.sh +4 -0
  277. package/runtime/codex/bin/spirit-discovery-codex.sh +4 -0
  278. package/runtime/codex/bin/spirit-discovery-pi.sh +4 -0
  279. package/runtime/codex/bin/spirit-discovery-prime.sh +4 -0
  280. package/runtime/codex/bin/spirit-discovery.sh +4 -0
  281. package/runtime/codex/bin/startup-check.sh +1 -1
  282. package/runtime/codex/bin/telemetry.sh +11 -4
  283. package/runtime/codex/bin/test-baseline.sh +33 -5
  284. package/runtime/codex/bin/todo.sh +1 -1
  285. package/runtime/codex/bin/worktree-create.sh +15 -45
  286. package/runtime/codex/bin/worktree.sh +6 -0
  287. package/runtime/codex/docs/spirit-discovery.md +96 -0
  288. package/runtime/codex/egregore_runtime/__init__.py +18 -12
  289. package/runtime/codex/egregore_runtime/access.py +608 -0
  290. package/runtime/codex/egregore_runtime/access_cli.py +190 -0
  291. package/runtime/codex/egregore_runtime/adapters/invitation.py +5 -3
  292. package/runtime/codex/egregore_runtime/adapters/qmd.py +521 -47
  293. package/runtime/codex/egregore_runtime/admin_entry.py +56 -0
  294. package/runtime/codex/egregore_runtime/artifacts.py +222 -16
  295. package/runtime/codex/egregore_runtime/contracts.py +27 -0
  296. package/runtime/codex/egregore_runtime/eligibility.py +5 -12
  297. package/runtime/codex/egregore_runtime/episode_binding.py +16 -5
  298. package/runtime/codex/egregore_runtime/github_issues.py +2 -2
  299. package/runtime/codex/egregore_runtime/handoff_lookup.py +9 -20
  300. package/runtime/codex/egregore_runtime/harness_cli.py +674 -149
  301. package/runtime/codex/egregore_runtime/held.py +2436 -0
  302. package/runtime/codex/egregore_runtime/hosted_api.py +137 -0
  303. package/runtime/codex/egregore_runtime/identity.py +196 -65
  304. package/runtime/codex/egregore_runtime/identity_transfer.py +6 -2
  305. package/runtime/codex/egregore_runtime/ingest_cli.py +2 -2
  306. package/runtime/codex/egregore_runtime/ingest_index_cli.py +4 -16
  307. package/runtime/codex/egregore_runtime/installation.py +119 -0
  308. package/runtime/codex/egregore_runtime/investigation.py +64 -41
  309. package/runtime/codex/egregore_runtime/investigation_guidance.py +16 -11
  310. package/runtime/codex/egregore_runtime/issue_cli.py +51 -32
  311. package/runtime/codex/egregore_runtime/issue_reporting.py +4 -3
  312. package/runtime/codex/egregore_runtime/issue_triage_service.py +2 -2
  313. package/runtime/codex/egregore_runtime/issues.py +12 -6
  314. package/runtime/codex/egregore_runtime/knowledge.py +331 -5
  315. package/runtime/codex/egregore_runtime/knowledge_cli.py +57 -17
  316. package/runtime/codex/egregore_runtime/lifecycle.py +88 -22
  317. package/runtime/codex/egregore_runtime/lifecycle_cli.py +21 -13
  318. package/runtime/codex/egregore_runtime/line_ids.py +10 -0
  319. package/runtime/codex/egregore_runtime/memory_push.py +349 -0
  320. package/runtime/codex/egregore_runtime/observe.py +64 -25
  321. package/runtime/codex/egregore_runtime/observers.py +19 -0
  322. package/runtime/codex/egregore_runtime/optional_modules.py +13 -0
  323. package/runtime/codex/egregore_runtime/outputs_cli.py +321 -0
  324. package/runtime/codex/egregore_runtime/policy.py +651 -24
  325. package/runtime/codex/egregore_runtime/private_result.py +2 -1
  326. package/runtime/codex/egregore_runtime/projection.py +946 -0
  327. package/runtime/codex/egregore_runtime/quest_cli.py +20 -23
  328. package/runtime/codex/egregore_runtime/question_cli.py +15 -11
  329. package/runtime/codex/egregore_runtime/questions.py +10 -10
  330. package/runtime/codex/egregore_runtime/quests.py +10 -10
  331. package/runtime/codex/egregore_runtime/record_publish.py +548 -0
  332. package/runtime/codex/egregore_runtime/record_subject.py +564 -0
  333. package/runtime/codex/egregore_runtime/research_ingest.py +2 -4
  334. package/runtime/codex/egregore_runtime/runtime.py +169 -60
  335. package/runtime/codex/egregore_runtime/sandbox.py +194 -0
  336. package/runtime/codex/egregore_runtime/scroll_cli.py +22 -32
  337. package/runtime/codex/egregore_runtime/scrolls.py +23 -15
  338. package/runtime/codex/egregore_runtime/shadow.py +262 -0
  339. package/runtime/codex/egregore_runtime/spirit_discovery.py +487 -0
  340. package/runtime/codex/egregore_runtime/spirit_discovery_cli.py +25 -0
  341. package/runtime/codex/egregore_runtime/spirit_discovery_transport.py +18 -0
  342. package/runtime/codex/egregore_runtime/status.py +101 -44
  343. package/runtime/codex/egregore_runtime/status_cli.py +13 -5
  344. package/runtime/codex/egregore_runtime/sync.py +39 -21
  345. package/runtime/codex/egregore_runtime/taste.py +934 -0
  346. package/runtime/codex/egregore_runtime/taste_cli.py +242 -0
  347. package/runtime/codex/egregore_runtime/taste_rules.py +91 -0
  348. package/runtime/codex/egregore_runtime/telemetry.py +26 -3
  349. package/runtime/codex/egregore_runtime/todo_cli.py +15 -12
  350. package/runtime/codex/egregore_runtime/todos.py +3 -2
  351. package/runtime/codex/egregore_runtime/upgrade.py +85 -58
  352. package/runtime/codex/egregore_runtime/writeback.py +330 -24
  353. package/runtime/codex/manifest.json +66 -5
  354. package/runtime/pi/.claude/context/handoff-claiming.md +32 -0
  355. package/runtime/pi/.claude/context/notification-consent.md +12 -9
  356. package/runtime/pi/.claude/skills/view/SKILL.md +438 -0
  357. package/runtime/pi/.codex/hooks.json +11 -0
  358. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +1 -1
  359. package/runtime/pi/.codex/skills/records/SKILL.md +29 -0
  360. package/runtime/pi/.codex/skills/summon/SKILL.md +1 -1
  361. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +19 -13
  362. package/runtime/pi/.codex/spec-manifest.json +13 -19
  363. package/runtime/pi/.pi/APPEND_SYSTEM.md +53 -99
  364. package/runtime/pi/.pi/extensions/egregore.ts +13 -1
  365. package/runtime/pi/.pi/spec-manifest.json +2 -2
  366. package/runtime/pi/AGENTS.md +54 -105
  367. package/runtime/pi/bin/agent.sh +231 -7
  368. package/runtime/pi/bin/api-call.sh +44 -23
  369. package/runtime/pi/bin/artifact-register.sh +7 -1
  370. package/runtime/pi/bin/artifact-writeback.sh +1 -1
  371. package/runtime/pi/bin/artifacts.sh +66 -265
  372. package/runtime/pi/bin/attendant.sh +4 -1
  373. package/runtime/pi/bin/boundary-guard.cjs +1848 -0
  374. package/runtime/pi/bin/boundary-hook.sh +41 -0
  375. package/runtime/pi/bin/capture-run.sh +32 -2
  376. package/runtime/pi/bin/codex-render-spec.mjs +24 -46
  377. package/runtime/pi/bin/codex-session-start.sh +19 -0
  378. package/runtime/pi/bin/codex-skill-render.mjs +18 -0
  379. package/runtime/pi/bin/config-get.sh +10 -1
  380. package/runtime/pi/bin/connect-refresh.sh +16 -0
  381. package/runtime/pi/bin/corpus_map.py +5 -0
  382. package/runtime/pi/bin/data/capabilities/pi.json +1458 -0
  383. package/runtime/pi/bin/genui.sh +19 -0
  384. package/runtime/pi/bin/graph-batch.sh +1 -1
  385. package/runtime/pi/bin/graph-op.sh +14 -0
  386. package/runtime/pi/bin/graph.sh +1 -1
  387. package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
  388. package/runtime/pi/bin/handoff-run.sh +11 -6
  389. package/runtime/pi/bin/ingest-item.sh +1 -1
  390. package/runtime/pi/bin/ingest-surface.html +22 -4
  391. package/runtime/pi/bin/issue.sh +1 -1
  392. package/runtime/pi/bin/knowledge.sh +1 -1
  393. package/runtime/pi/bin/lib/card-delivery.sh +329 -0
  394. package/runtime/pi/bin/lib/config.sh +148 -3
  395. package/runtime/pi/bin/lib/context.sh +38 -67
  396. package/runtime/pi/bin/lib/genui-entry.py +36 -0
  397. package/runtime/pi/bin/lib/git-message.sh +25 -6
  398. package/runtime/pi/bin/lib/git-sync.sh +12 -4
  399. package/runtime/pi/bin/lib/greeting.sh +255 -28
  400. package/runtime/pi/bin/lib/handoff_meta.py +294 -0
  401. package/runtime/pi/bin/lib/health-footer.sh +83 -2
  402. package/runtime/pi/bin/lib/identity.sh +31 -10
  403. package/runtime/pi/bin/lib/instance-registry.sh +71 -0
  404. package/runtime/pi/bin/lib/local_ci_receipt.py +249 -0
  405. package/runtime/pi/bin/lib/metrics.sh +121 -8
  406. package/runtime/pi/bin/lib/notices.sh +36 -0
  407. package/runtime/pi/bin/lib/record_outcomes.py +729 -0
  408. package/runtime/pi/bin/lib/reflexes.py +267 -0
  409. package/runtime/pi/bin/lib/retrieval_scorecard.py +624 -0
  410. package/runtime/pi/bin/lib/session-record-prompt.txt +18 -0
  411. package/runtime/pi/bin/lib/session_record.py +3747 -0
  412. package/runtime/pi/bin/lib/session_trail.py +1983 -0
  413. package/runtime/pi/bin/lib/settings-drift.sh +8 -5
  414. package/runtime/pi/bin/lib/startup-restart.sh +41 -0
  415. package/runtime/pi/bin/lib/worktree-links.sh +92 -0
  416. package/runtime/pi/bin/memory-push.sh +6 -0
  417. package/runtime/pi/bin/merge-gate.sh +498 -0
  418. package/runtime/pi/bin/notify.sh +5 -2
  419. package/runtime/pi/bin/page-access.sh +103 -0
  420. package/runtime/pi/bin/person.sh +13 -2
  421. package/runtime/pi/bin/pi-branch-policy.mjs +37 -0
  422. package/runtime/pi/bin/pi-observe-context.mjs +97 -4
  423. package/runtime/pi/bin/pi-render-spec.mjs +0 -1
  424. package/runtime/pi/bin/pi-session-start.sh +4 -1
  425. package/runtime/pi/bin/pi-spirit-discovery.mjs +34 -0
  426. package/runtime/pi/bin/post-update.sh +181 -0
  427. package/runtime/pi/bin/prime-render-spec.mjs +0 -1
  428. package/runtime/pi/bin/publish-artifact.sh +89 -42
  429. package/runtime/pi/bin/qa-inventory.sh +2 -2
  430. package/runtime/pi/bin/quest.sh +1 -1
  431. package/runtime/pi/bin/records.sh +11 -0
  432. package/runtime/pi/bin/reflexes/05-vitals.sh +9 -0
  433. package/runtime/pi/bin/reflexes/10-open-items.sh +12 -0
  434. package/runtime/pi/bin/reflexes/15-retrieval.sh +8 -0
  435. package/runtime/pi/bin/reflexes/20-nightly.sh +100 -0
  436. package/runtime/pi/bin/reflexes/20-sandbox.sh +85 -0
  437. package/runtime/pi/bin/reflexes/30-cost.py +116 -0
  438. package/runtime/pi/bin/reflexes/40-shells.py +51 -0
  439. package/runtime/pi/bin/reflexes/README.md +61 -0
  440. package/runtime/pi/bin/reflexes.sh +7 -0
  441. package/runtime/pi/bin/runtime-identity.sh +140 -42
  442. package/runtime/pi/bin/screenshot.sh +77 -0
  443. package/runtime/pi/bin/scroll.sh +1 -1
  444. package/runtime/pi/bin/search.sh +31 -5
  445. package/runtime/pi/bin/session-autosave.sh +6 -3
  446. package/runtime/pi/bin/session-id.sh +147 -0
  447. package/runtime/pi/bin/session-log.sh +19 -0
  448. package/runtime/pi/bin/session-record.sh +26 -0
  449. package/runtime/pi/bin/settings.sh +13 -8
  450. package/runtime/pi/bin/spirit-discovery-claude.sh +4 -0
  451. package/runtime/pi/bin/spirit-discovery-codex.sh +4 -0
  452. package/runtime/pi/bin/spirit-discovery-pi.sh +4 -0
  453. package/runtime/pi/bin/spirit-discovery-prime.sh +4 -0
  454. package/runtime/pi/bin/spirit-discovery.sh +4 -0
  455. package/runtime/pi/bin/startup-check.sh +1 -1
  456. package/runtime/pi/bin/telemetry.sh +11 -4
  457. package/runtime/pi/bin/test-baseline.sh +33 -5
  458. package/runtime/pi/bin/todo.sh +1 -1
  459. package/runtime/pi/bin/worktree-create.sh +15 -45
  460. package/runtime/pi/bin/worktree.sh +6 -0
  461. package/runtime/pi/docs/spirit-discovery.md +96 -0
  462. package/runtime/pi/egregore_runtime/__init__.py +18 -12
  463. package/runtime/pi/egregore_runtime/access.py +608 -0
  464. package/runtime/pi/egregore_runtime/access_cli.py +190 -0
  465. package/runtime/pi/egregore_runtime/adapters/invitation.py +5 -3
  466. package/runtime/pi/egregore_runtime/adapters/qmd.py +521 -47
  467. package/runtime/pi/egregore_runtime/admin_entry.py +56 -0
  468. package/runtime/pi/egregore_runtime/artifacts.py +222 -16
  469. package/runtime/pi/egregore_runtime/contracts.py +27 -0
  470. package/runtime/pi/egregore_runtime/eligibility.py +5 -12
  471. package/runtime/pi/egregore_runtime/episode_binding.py +16 -5
  472. package/runtime/pi/egregore_runtime/github_issues.py +2 -2
  473. package/runtime/pi/egregore_runtime/handoff_lookup.py +9 -20
  474. package/runtime/pi/egregore_runtime/harness_cli.py +674 -149
  475. package/runtime/pi/egregore_runtime/held.py +2436 -0
  476. package/runtime/pi/egregore_runtime/hosted_api.py +137 -0
  477. package/runtime/pi/egregore_runtime/identity.py +196 -65
  478. package/runtime/pi/egregore_runtime/identity_transfer.py +6 -2
  479. package/runtime/pi/egregore_runtime/ingest_cli.py +2 -2
  480. package/runtime/pi/egregore_runtime/ingest_index_cli.py +4 -16
  481. package/runtime/pi/egregore_runtime/installation.py +119 -0
  482. package/runtime/pi/egregore_runtime/investigation.py +64 -41
  483. package/runtime/pi/egregore_runtime/investigation_guidance.py +16 -11
  484. package/runtime/pi/egregore_runtime/issue_cli.py +51 -32
  485. package/runtime/pi/egregore_runtime/issue_reporting.py +4 -3
  486. package/runtime/pi/egregore_runtime/issue_triage_service.py +2 -2
  487. package/runtime/pi/egregore_runtime/issues.py +12 -6
  488. package/runtime/pi/egregore_runtime/knowledge.py +331 -5
  489. package/runtime/pi/egregore_runtime/knowledge_cli.py +57 -17
  490. package/runtime/pi/egregore_runtime/lifecycle.py +88 -22
  491. package/runtime/pi/egregore_runtime/lifecycle_cli.py +21 -13
  492. package/runtime/pi/egregore_runtime/line_ids.py +10 -0
  493. package/runtime/pi/egregore_runtime/memory_push.py +349 -0
  494. package/runtime/pi/egregore_runtime/observe.py +64 -25
  495. package/runtime/pi/egregore_runtime/observers.py +19 -0
  496. package/runtime/pi/egregore_runtime/optional_modules.py +13 -0
  497. package/runtime/pi/egregore_runtime/outputs_cli.py +321 -0
  498. package/runtime/pi/egregore_runtime/policy.py +651 -24
  499. package/runtime/pi/egregore_runtime/private_result.py +2 -1
  500. package/runtime/pi/egregore_runtime/projection.py +946 -0
  501. package/runtime/pi/egregore_runtime/quest_cli.py +20 -23
  502. package/runtime/pi/egregore_runtime/question_cli.py +15 -11
  503. package/runtime/pi/egregore_runtime/questions.py +10 -10
  504. package/runtime/pi/egregore_runtime/quests.py +10 -10
  505. package/runtime/pi/egregore_runtime/record_publish.py +548 -0
  506. package/runtime/pi/egregore_runtime/record_subject.py +564 -0
  507. package/runtime/pi/egregore_runtime/research_ingest.py +2 -4
  508. package/runtime/pi/egregore_runtime/runtime.py +169 -60
  509. package/runtime/pi/egregore_runtime/sandbox.py +194 -0
  510. package/runtime/pi/egregore_runtime/scroll_cli.py +22 -32
  511. package/runtime/pi/egregore_runtime/scrolls.py +23 -15
  512. package/runtime/pi/egregore_runtime/shadow.py +262 -0
  513. package/runtime/pi/egregore_runtime/spirit_discovery.py +487 -0
  514. package/runtime/pi/egregore_runtime/spirit_discovery_cli.py +25 -0
  515. package/runtime/pi/egregore_runtime/spirit_discovery_transport.py +18 -0
  516. package/runtime/pi/egregore_runtime/status.py +101 -44
  517. package/runtime/pi/egregore_runtime/status_cli.py +13 -5
  518. package/runtime/pi/egregore_runtime/sync.py +39 -21
  519. package/runtime/pi/egregore_runtime/taste.py +934 -0
  520. package/runtime/pi/egregore_runtime/taste_cli.py +242 -0
  521. package/runtime/pi/egregore_runtime/taste_rules.py +91 -0
  522. package/runtime/pi/egregore_runtime/telemetry.py +26 -3
  523. package/runtime/pi/egregore_runtime/todo_cli.py +15 -12
  524. package/runtime/pi/egregore_runtime/todos.py +3 -2
  525. package/runtime/pi/egregore_runtime/upgrade.py +85 -58
  526. package/runtime/pi/egregore_runtime/writeback.py +330 -24
  527. package/runtime/pi/manifest.json +66 -5
  528. package/runtime/prime/.claude/context/handoff-claiming.md +32 -0
  529. package/runtime/prime/.claude/context/notification-consent.md +12 -9
  530. package/runtime/prime/.claude/skills/view/SKILL.md +438 -0
  531. package/runtime/prime/.codex/hooks.json +11 -0
  532. package/runtime/prime/.codex/skills/dark-mode/SKILL.md +1 -1
  533. package/runtime/prime/.codex/skills/records/SKILL.md +29 -0
  534. package/runtime/prime/.codex/skills/summon/SKILL.md +1 -1
  535. package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +19 -13
  536. package/runtime/prime/.codex/spec-manifest.json +13 -19
  537. package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +53 -99
  538. package/runtime/prime/.prime/agent/extensions/egregore.ts +14 -1
  539. package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +14 -6
  540. package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +66 -1
  541. package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
  542. package/runtime/prime/AGENTS.md +54 -105
  543. package/runtime/prime/bin/agent.sh +231 -7
  544. package/runtime/prime/bin/api-call.sh +44 -23
  545. package/runtime/prime/bin/artifact-register.sh +7 -1
  546. package/runtime/prime/bin/artifact-writeback.sh +1 -1
  547. package/runtime/prime/bin/artifacts.sh +66 -265
  548. package/runtime/prime/bin/attendant.sh +4 -1
  549. package/runtime/prime/bin/boundary-guard.cjs +1848 -0
  550. package/runtime/prime/bin/boundary-hook.sh +41 -0
  551. package/runtime/prime/bin/capture-run.sh +32 -2
  552. package/runtime/prime/bin/codex-render-spec.mjs +24 -46
  553. package/runtime/prime/bin/codex-session-start.sh +19 -0
  554. package/runtime/prime/bin/codex-skill-render.mjs +18 -0
  555. package/runtime/prime/bin/config-get.sh +10 -1
  556. package/runtime/prime/bin/connect-refresh.sh +16 -0
  557. package/runtime/prime/bin/corpus_map.py +5 -0
  558. package/runtime/prime/bin/data/capabilities/prime.json +1458 -0
  559. package/runtime/prime/bin/genui.sh +19 -0
  560. package/runtime/prime/bin/graph-batch.sh +1 -1
  561. package/runtime/prime/bin/graph-op.sh +14 -0
  562. package/runtime/prime/bin/graph.sh +1 -1
  563. package/runtime/prime/bin/handoff-pr-backfill.sh +1 -1
  564. package/runtime/prime/bin/handoff-run.sh +11 -6
  565. package/runtime/prime/bin/ingest-item.sh +1 -1
  566. package/runtime/prime/bin/ingest-surface.html +22 -4
  567. package/runtime/prime/bin/issue.sh +1 -1
  568. package/runtime/prime/bin/knowledge.sh +1 -1
  569. package/runtime/prime/bin/lib/card-delivery.sh +329 -0
  570. package/runtime/prime/bin/lib/config.sh +148 -3
  571. package/runtime/prime/bin/lib/context.sh +38 -67
  572. package/runtime/prime/bin/lib/genui-entry.py +36 -0
  573. package/runtime/prime/bin/lib/git-message.sh +25 -6
  574. package/runtime/prime/bin/lib/git-sync.sh +12 -4
  575. package/runtime/prime/bin/lib/greeting.sh +255 -28
  576. package/runtime/prime/bin/lib/handoff_meta.py +294 -0
  577. package/runtime/prime/bin/lib/health-footer.sh +83 -2
  578. package/runtime/prime/bin/lib/identity.sh +31 -10
  579. package/runtime/prime/bin/lib/instance-registry.sh +71 -0
  580. package/runtime/prime/bin/lib/local_ci_receipt.py +249 -0
  581. package/runtime/prime/bin/lib/metrics.sh +121 -8
  582. package/runtime/prime/bin/lib/notices.sh +36 -0
  583. package/runtime/prime/bin/lib/record_outcomes.py +729 -0
  584. package/runtime/prime/bin/lib/reflexes.py +267 -0
  585. package/runtime/prime/bin/lib/retrieval_scorecard.py +624 -0
  586. package/runtime/prime/bin/lib/session-record-prompt.txt +18 -0
  587. package/runtime/prime/bin/lib/session_record.py +3747 -0
  588. package/runtime/prime/bin/lib/session_trail.py +1983 -0
  589. package/runtime/prime/bin/lib/settings-drift.sh +8 -5
  590. package/runtime/prime/bin/lib/startup-restart.sh +41 -0
  591. package/runtime/prime/bin/lib/worktree-links.sh +92 -0
  592. package/runtime/prime/bin/memory-push.sh +6 -0
  593. package/runtime/prime/bin/merge-gate.sh +498 -0
  594. package/runtime/prime/bin/notify.sh +5 -2
  595. package/runtime/prime/bin/page-access.sh +103 -0
  596. package/runtime/prime/bin/person.sh +13 -2
  597. package/runtime/prime/bin/pi-branch-policy.mjs +37 -0
  598. package/runtime/prime/bin/pi-observe-context.mjs +97 -4
  599. package/runtime/prime/bin/pi-render-spec.mjs +0 -1
  600. package/runtime/prime/bin/pi-spirit-discovery.mjs +34 -0
  601. package/runtime/prime/bin/post-update.sh +181 -0
  602. package/runtime/prime/bin/prime-render-spec.mjs +0 -1
  603. package/runtime/prime/bin/prime-session-start.sh +3 -1
  604. package/runtime/prime/bin/publish-artifact.sh +89 -42
  605. package/runtime/prime/bin/qa-inventory.sh +2 -2
  606. package/runtime/prime/bin/quest.sh +1 -1
  607. package/runtime/prime/bin/records.sh +11 -0
  608. package/runtime/prime/bin/reflexes/05-vitals.sh +9 -0
  609. package/runtime/prime/bin/reflexes/10-open-items.sh +12 -0
  610. package/runtime/prime/bin/reflexes/15-retrieval.sh +8 -0
  611. package/runtime/prime/bin/reflexes/20-nightly.sh +100 -0
  612. package/runtime/prime/bin/reflexes/20-sandbox.sh +85 -0
  613. package/runtime/prime/bin/reflexes/30-cost.py +116 -0
  614. package/runtime/prime/bin/reflexes/40-shells.py +51 -0
  615. package/runtime/prime/bin/reflexes/README.md +61 -0
  616. package/runtime/prime/bin/reflexes.sh +7 -0
  617. package/runtime/prime/bin/runtime-identity.sh +140 -42
  618. package/runtime/prime/bin/screenshot.sh +77 -0
  619. package/runtime/prime/bin/scroll.sh +1 -1
  620. package/runtime/prime/bin/search.sh +31 -5
  621. package/runtime/prime/bin/session-autosave.sh +6 -3
  622. package/runtime/prime/bin/session-id.sh +147 -0
  623. package/runtime/prime/bin/session-log.sh +19 -0
  624. package/runtime/prime/bin/session-record.sh +26 -0
  625. package/runtime/prime/bin/settings.sh +13 -8
  626. package/runtime/prime/bin/spirit-discovery-claude.sh +4 -0
  627. package/runtime/prime/bin/spirit-discovery-codex.sh +4 -0
  628. package/runtime/prime/bin/spirit-discovery-pi.sh +4 -0
  629. package/runtime/prime/bin/spirit-discovery-prime.sh +4 -0
  630. package/runtime/prime/bin/spirit-discovery.sh +4 -0
  631. package/runtime/prime/bin/startup-check.sh +1 -1
  632. package/runtime/prime/bin/telemetry.sh +11 -4
  633. package/runtime/prime/bin/test-baseline.sh +33 -5
  634. package/runtime/prime/bin/todo.sh +1 -1
  635. package/runtime/prime/bin/worktree-create.sh +15 -45
  636. package/runtime/prime/bin/worktree.sh +6 -0
  637. package/runtime/prime/docs/spirit-discovery.md +96 -0
  638. package/runtime/prime/egregore_runtime/__init__.py +18 -12
  639. package/runtime/prime/egregore_runtime/access.py +608 -0
  640. package/runtime/prime/egregore_runtime/access_cli.py +190 -0
  641. package/runtime/prime/egregore_runtime/adapters/invitation.py +5 -3
  642. package/runtime/prime/egregore_runtime/adapters/qmd.py +521 -47
  643. package/runtime/prime/egregore_runtime/admin_entry.py +56 -0
  644. package/runtime/prime/egregore_runtime/artifacts.py +222 -16
  645. package/runtime/prime/egregore_runtime/contracts.py +27 -0
  646. package/runtime/prime/egregore_runtime/eligibility.py +5 -12
  647. package/runtime/prime/egregore_runtime/episode_binding.py +16 -5
  648. package/runtime/prime/egregore_runtime/github_issues.py +2 -2
  649. package/runtime/prime/egregore_runtime/handoff_lookup.py +9 -20
  650. package/runtime/prime/egregore_runtime/harness_cli.py +674 -149
  651. package/runtime/prime/egregore_runtime/held.py +2436 -0
  652. package/runtime/prime/egregore_runtime/hosted_api.py +137 -0
  653. package/runtime/prime/egregore_runtime/identity.py +196 -65
  654. package/runtime/prime/egregore_runtime/identity_transfer.py +6 -2
  655. package/runtime/prime/egregore_runtime/ingest_cli.py +2 -2
  656. package/runtime/prime/egregore_runtime/ingest_index_cli.py +4 -16
  657. package/runtime/prime/egregore_runtime/installation.py +119 -0
  658. package/runtime/prime/egregore_runtime/investigation.py +64 -41
  659. package/runtime/prime/egregore_runtime/investigation_guidance.py +16 -11
  660. package/runtime/prime/egregore_runtime/issue_cli.py +51 -32
  661. package/runtime/prime/egregore_runtime/issue_reporting.py +4 -3
  662. package/runtime/prime/egregore_runtime/issue_triage_service.py +2 -2
  663. package/runtime/prime/egregore_runtime/issues.py +12 -6
  664. package/runtime/prime/egregore_runtime/knowledge.py +331 -5
  665. package/runtime/prime/egregore_runtime/knowledge_cli.py +57 -17
  666. package/runtime/prime/egregore_runtime/lifecycle.py +88 -22
  667. package/runtime/prime/egregore_runtime/lifecycle_cli.py +21 -13
  668. package/runtime/prime/egregore_runtime/line_ids.py +10 -0
  669. package/runtime/prime/egregore_runtime/memory_push.py +349 -0
  670. package/runtime/prime/egregore_runtime/observe.py +64 -25
  671. package/runtime/prime/egregore_runtime/observers.py +19 -0
  672. package/runtime/prime/egregore_runtime/optional_modules.py +13 -0
  673. package/runtime/prime/egregore_runtime/outputs_cli.py +321 -0
  674. package/runtime/prime/egregore_runtime/policy.py +651 -24
  675. package/runtime/prime/egregore_runtime/private_result.py +2 -1
  676. package/runtime/prime/egregore_runtime/projection.py +946 -0
  677. package/runtime/prime/egregore_runtime/quest_cli.py +20 -23
  678. package/runtime/prime/egregore_runtime/question_cli.py +15 -11
  679. package/runtime/prime/egregore_runtime/questions.py +10 -10
  680. package/runtime/prime/egregore_runtime/quests.py +10 -10
  681. package/runtime/prime/egregore_runtime/record_publish.py +548 -0
  682. package/runtime/prime/egregore_runtime/record_subject.py +564 -0
  683. package/runtime/prime/egregore_runtime/research_ingest.py +2 -4
  684. package/runtime/prime/egregore_runtime/runtime.py +169 -60
  685. package/runtime/prime/egregore_runtime/sandbox.py +194 -0
  686. package/runtime/prime/egregore_runtime/scroll_cli.py +22 -32
  687. package/runtime/prime/egregore_runtime/scrolls.py +23 -15
  688. package/runtime/prime/egregore_runtime/shadow.py +262 -0
  689. package/runtime/prime/egregore_runtime/spirit_discovery.py +487 -0
  690. package/runtime/prime/egregore_runtime/spirit_discovery_cli.py +25 -0
  691. package/runtime/prime/egregore_runtime/spirit_discovery_transport.py +18 -0
  692. package/runtime/prime/egregore_runtime/status.py +101 -44
  693. package/runtime/prime/egregore_runtime/status_cli.py +13 -5
  694. package/runtime/prime/egregore_runtime/sync.py +39 -21
  695. package/runtime/prime/egregore_runtime/taste.py +934 -0
  696. package/runtime/prime/egregore_runtime/taste_cli.py +242 -0
  697. package/runtime/prime/egregore_runtime/taste_rules.py +91 -0
  698. package/runtime/prime/egregore_runtime/telemetry.py +26 -3
  699. package/runtime/prime/egregore_runtime/todo_cli.py +15 -12
  700. package/runtime/prime/egregore_runtime/todos.py +3 -2
  701. package/runtime/prime/egregore_runtime/upgrade.py +85 -58
  702. package/runtime/prime/egregore_runtime/writeback.py +330 -24
  703. package/runtime/prime/manifest.json +66 -5
  704. package/runtime/claude/bin/thread.sh +0 -9
  705. package/runtime/claude/egregore_runtime/admin_gate.py +0 -102
  706. package/runtime/claude/egregore_runtime/thread_cli.py +0 -173
  707. package/runtime/claude/egregore_runtime/threads.py +0 -521
  708. package/runtime/codex/bin/thread.sh +0 -9
  709. package/runtime/codex/egregore_runtime/admin_gate.py +0 -102
  710. package/runtime/codex/egregore_runtime/thread_cli.py +0 -173
  711. package/runtime/codex/egregore_runtime/threads.py +0 -521
  712. package/runtime/pi/bin/thread.sh +0 -9
  713. package/runtime/pi/egregore_runtime/admin_gate.py +0 -102
  714. package/runtime/pi/egregore_runtime/thread_cli.py +0 -173
  715. package/runtime/pi/egregore_runtime/threads.py +0 -521
  716. package/runtime/prime/bin/thread.sh +0 -9
  717. package/runtime/prime/egregore_runtime/admin_gate.py +0 -102
  718. package/runtime/prime/egregore_runtime/thread_cli.py +0 -173
  719. package/runtime/prime/egregore_runtime/threads.py +0 -521
@@ -10,13 +10,16 @@ from __future__ import annotations
10
10
  import hashlib
11
11
  import hmac
12
12
  import json
13
+ import math
13
14
  import os
14
15
  import re
15
16
  import shutil
16
17
  import signal
17
18
  import secrets
18
19
  import socket
20
+ import sqlite3
19
21
  import stat
22
+ import struct
20
23
  import subprocess
21
24
  import sys
22
25
  import time
@@ -24,7 +27,7 @@ import urllib.error
24
27
  import urllib.parse
25
28
  import urllib.request
26
29
  import uuid
27
- from contextlib import contextmanager
30
+ from contextlib import closing, contextmanager
28
31
  from dataclasses import dataclass, replace
29
32
  from datetime import UTC, datetime
30
33
  from pathlib import Path
@@ -32,6 +35,7 @@ from typing import Any, Mapping, Sequence
32
35
 
33
36
  import fcntl
34
37
 
38
+ from egregore_runtime.policy import within
35
39
  from egregore_runtime.contracts import (
36
40
  RetrievalHit,
37
41
  RetrievalMode,
@@ -44,6 +48,7 @@ from egregore_runtime.artifacts import (
44
48
  canonical_artifact_id,
45
49
  split_frontmatter,
46
50
  )
51
+ from egregore_runtime.sandbox import REFUSED, REMEDY, display, sandboxed, state_directory
47
52
 
48
53
 
49
54
  QMD_VERSION = "2.8.3"
@@ -52,6 +57,10 @@ RUNTIME_SCHEMA_VERSION = "egregore-qmd-runtime/v1"
52
57
  EMBEDDING_MODEL = "EmbeddingGemma"
53
58
  MAX_AUTHORIZED_CANDIDATES = 2_000
54
59
  MAX_SDK_PASSAGE_CHARACTERS = 4_000
60
+ # `qmd status` counts are reused only while the files they count are
61
+ # unchanged, and never for longer than this.
62
+ STATUS_COUNTS_SCHEMA = "egregore-qmd-status-counts/v1"
63
+ STATUS_COUNTS_MAX_AGE_SECONDS = 600
55
64
  # Every query this adapter issues — daemon and CLI — disables model
56
65
  # reranking; results come from typed lex/vec rank fusion only. Readiness
57
66
  # surfaces report this posture from here rather than restating it.
@@ -149,6 +158,9 @@ class QmdLocalRetriever:
149
158
  used by the pinned CLI fallback.
150
159
  """
151
160
 
161
+ # Known without a health check (which runs ``qmd status``): what telemetry names on every source open.
162
+ index_spec_version = INDEX_SPEC_VERSION
163
+
152
164
  def __init__(
153
165
  self,
154
166
  *,
@@ -163,6 +175,12 @@ class QmdLocalRetriever:
163
175
  configured_memory = memory_root or self.repository_root / "memory"
164
176
  self.memory_root = configured_memory.resolve()
165
177
  self.environment = dict(os.environ if environment is None else environment)
178
+ # Resolved once by _runtime_root(). The warning is set when the
179
+ # per-user state refuses writes: with the workspace copy in use, or
180
+ # naming why that copy was refused too.
181
+ self._state_root: Path | None = None
182
+ self._state_fallback = False
183
+ self._state_warning: str | None = None
166
184
  self.collection_purpose = re.sub(
167
185
  r"[^A-Za-z0-9_.-]", "-", collection_purpose.strip()
168
186
  ) or "memory"
@@ -180,7 +198,18 @@ class QmdLocalRetriever:
180
198
  # retrieval is never silent.
181
199
  self._last_query_used_daemon: bool | None = None
182
200
  self._source_revision_cache: str | None = None
201
+ # The memory checkout's Git top level: fixed for the adapter's life.
202
+ self._git_top_level_cache: Path | None = None
203
+ # (source revision, Markdown revision): valid for that source revision only.
204
+ self._markdown_revision_cache: tuple[str, str] | None = None
183
205
  self._configure_isolated_environment()
206
+ if self._state_fallback:
207
+ # The workspace copy belongs to one checkout: keep it lexical and
208
+ # cold, with no query worker or background embedding that would
209
+ # outlive the session or repeat in every worktree. Explicit
210
+ # settings still win.
211
+ self.environment.setdefault("EGREGORE_QMD_PERSISTENT", "0")
212
+ self.environment.setdefault("EGREGORE_SEARCH_NO_WARM", "1")
184
213
  self._prefer_usable_node_runtime()
185
214
 
186
215
  def _configure_isolated_environment(self) -> None:
@@ -327,8 +356,41 @@ class QmdLocalRetriever:
327
356
  override = self.environment.get("EGREGORE_QMD_RUNTIME_DIR")
328
357
  if override:
329
358
  return Path(override).expanduser().resolve()
330
- home = Path(self.environment.get("HOME", str(Path.home()))).expanduser()
331
- return home / ".egregore" / "runtime" / "qmd" / self._instance_hash()
359
+ root = getattr(self, "_state_root", None)
360
+ if root is None:
361
+ # Resolved on first use, so every construction path works: per-user
362
+ # state unless it refuses writes (a sandboxed session), then this
363
+ # checkout's own .egregore/runtime, and a warning. An adapter built
364
+ # for its path rules alone (vectors_receipt) has no workspace, so it
365
+ # neither probes nor falls back.
366
+ preferred = self._user_runtime_root()
367
+ workspace = getattr(self, "repository_root", None)
368
+ warning = None
369
+ if workspace is None:
370
+ root = preferred
371
+ else:
372
+ root, warning = state_directory(
373
+ preferred,
374
+ workspace / ".egregore" / "runtime" / "qmd" / self._instance_hash(),
375
+ workspace=workspace,
376
+ environment=self.environment,
377
+ home=self._home(),
378
+ )
379
+ self._state_root, self._state_warning = root, warning
380
+ self._state_fallback = root != preferred
381
+ return root
382
+
383
+ def _home(self) -> Path:
384
+ return Path(self.environment.get("HOME", str(Path.home()))).expanduser()
385
+
386
+ def _user_runtime_root(self) -> Path:
387
+ return self._home() / ".egregore" / "runtime" / "qmd" / self._instance_hash()
388
+
389
+ def _first_warnings(self) -> list[str]:
390
+ """The state warning, which every health and result names first."""
391
+ self._runtime_root()
392
+ warning = getattr(self, "_state_warning", None)
393
+ return [warning] if warning else []
332
394
 
333
395
  def _runtime_state_path(self) -> Path:
334
396
  return self._runtime_root() / "runtime.json"
@@ -364,11 +426,19 @@ class QmdLocalRetriever:
364
426
  index_spec_version=INDEX_SPEC_VERSION,
365
427
  index_revision="unavailable", source_revision="unavailable",
366
428
  embedding_state="unavailable", runtime_state="runtime_unavailable",
367
- warnings=(self._filesystem_recovery_message(exc),),
429
+ warnings=(*self._first_warnings(), self._filesystem_recovery_message(exc)),
368
430
  )
369
431
 
370
- @staticmethod
371
- def _filesystem_recovery_message(exc: OSError) -> str:
432
+ def _filesystem_recovery_message(self, exc: OSError) -> str:
433
+ if getattr(exc, "errno", None) in REFUSED and sandboxed(self.environment):
434
+ # The cause and the remedy, instead of a lock path.
435
+ if exc.filename:
436
+ return (
437
+ f"Local retrieval storage at {display(self._runtime_root(), self._home())} "
438
+ f"is not writable (sandbox); {REMEDY}."
439
+ )
440
+ return (f"Local retrieval was refused by the sandbox "
441
+ f"({exc.strerror or type(exc).__name__}); {REMEDY}.")
372
442
  return (
373
443
  f"Local retrieval storage is unavailable ({type(exc).__name__}: {exc}). "
374
444
  "Provision the instance cache with private permissions and the host's "
@@ -376,7 +446,7 @@ class QmdLocalRetriever:
376
446
  )
377
447
 
378
448
  def _health(self) -> RetrieverHealth:
379
- warnings: list[str] = []
449
+ warnings = self._first_warnings()
380
450
  canonical_ready = self.memory_root.is_dir()
381
451
  qmd_available = True
382
452
  if not canonical_ready:
@@ -389,17 +459,22 @@ class QmdLocalRetriever:
389
459
  qmd_available = False
390
460
  warnings.append(str(exc))
391
461
 
392
- status = ""
462
+ counts = self._status_counts("")
393
463
  status_ok = False
394
464
  if canonical_ready and qmd_available:
395
- completed = self._run(("status",), timeout=20)
396
- if completed.returncode == 0:
397
- status = completed.stdout
398
- status_ok = True
465
+ remembered = self._remembered_status_counts()
466
+ if remembered is not None:
467
+ counts, status_ok = remembered, True
399
468
  else:
400
- warnings.append(self._failure_message("qmd status", completed))
469
+ observed = self._status_identity()
470
+ completed = self._run(("status",), timeout=20)
471
+ if completed.returncode == 0:
472
+ counts = self._status_counts(completed.stdout)
473
+ status_ok = True
474
+ self._remember_status_counts(observed, counts)
475
+ else:
476
+ warnings.append(self._failure_message("qmd status", completed))
401
477
 
402
- counts = self._status_counts(status)
403
478
  # BM25 readiness is usability, mirroring the semantic split: a
404
479
  # canonical write leaves the index trailing, not unusable — queries
405
480
  # refresh it inline. Only an identity mismatch makes it stale.
@@ -420,7 +495,7 @@ class QmdLocalRetriever:
420
495
  and self._semantic_state_current()
421
496
  and not self._live_lock(self._embed_lock_path())
422
497
  )
423
- embedding_state = self._embedding_state(status, semantic_ready=semantic_ready)
498
+ embedding_state = self._embedding_state(counts, semantic_ready=semantic_ready)
424
499
  if embedding_state == "missing_model":
425
500
  if counts["vectors"] > 0:
426
501
  warnings.append(
@@ -533,12 +608,12 @@ class QmdLocalRetriever:
533
608
  """Canonical metadata for a path already authorized by Runtime."""
534
609
  return self._metadata(canonical_path)
535
610
 
536
- def retrieve_authorized(self, request, *, actor, admin_gate=None, lifecycle=None,
611
+ def retrieve_authorized(self, request, *, actor, decision, lifecycle=None,
537
612
  path_filter=None, metadata_filter=None):
538
613
  from ..eligibility import build_eligibility
539
614
  try:
540
615
  return self._retrieve(request, eligibility_builder=lambda: build_eligibility(
541
- self, request, actor=actor, admin_gate=admin_gate, lifecycle=lifecycle,
616
+ self, request, actor=actor, decision=decision, lifecycle=lifecycle,
542
617
  path_filter=path_filter, metadata_filter=metadata_filter))
543
618
  except OSError as exc:
544
619
  raise QmdAdapterError(self._filesystem_recovery_message(exc)) from exc
@@ -560,7 +635,7 @@ class QmdLocalRetriever:
560
635
 
561
636
  def _retrieve(self, request: RetrievalRequest, *, eligibility_builder=None) -> RetrievalResult:
562
637
  started = time.monotonic()
563
- warnings: list[str] = []
638
+ warnings = self._first_warnings()
564
639
  # A long-lived runtime may observe a new canonical Git revision between
565
640
  # requests. Cache only within one retrieval operation.
566
641
  self._source_revision_cache = None
@@ -690,6 +765,11 @@ class QmdLocalRetriever:
690
765
  # spawns for an unchanged corpus; any revision change (including
691
766
  # a dirty-tree hash change) still takes the full update path.
692
767
  return self.health()
768
+ if not force and self._index_markdown_current():
769
+ # Only files the index never reads changed (a taste event, a
770
+ # YAML registry): record the new revision without a rescan.
771
+ self._write_index_state()
772
+ return self.health()
693
773
  # A canonical write makes the semantic build stale, never unusable:
694
774
  # staleness is expressed by revision inequality alone, and the last
695
775
  # complete vector receipt persists so hybrid keeps serving while the
@@ -941,7 +1021,7 @@ class QmdLocalRetriever:
941
1021
  accepted: list[tuple[Mapping[str, Any], str, _DocumentMetadata]] = []
942
1022
  for raw in raw_hits:
943
1023
  canonical_path = self._canonical_path(str(raw.get("file", "")))
944
- if canonical_path is None or not self._scope_allows(canonical_path, request):
1024
+ if canonical_path is None or not within(canonical_path, request.authorized_scopes, root=self.memory_root):
945
1025
  continue
946
1026
  if metadata_by_path is not None and canonical_path not in metadata_by_path:
947
1027
  raise QmdAdapterError('scoped QMD returned a document outside its eligible set')
@@ -999,6 +1079,9 @@ class QmdLocalRetriever:
999
1079
  self._ensure_version()
1000
1080
  if self._index_state_current():
1001
1081
  return
1082
+ if self._index_markdown_current():
1083
+ self._write_index_state()
1084
+ return
1002
1085
  self._ensure_collection(force=True)
1003
1086
  completed = self._run(("update",), timeout=300)
1004
1087
  if completed.returncode != 0:
@@ -1133,6 +1216,14 @@ class QmdLocalRetriever:
1133
1216
  instance = self._managed_prefix() / "node_modules" / ".bin" / "qmd"
1134
1217
  if instance.is_file():
1135
1218
  return instance
1219
+ if getattr(self, "_state_fallback", False):
1220
+ # The workspace copy runs the per-user install read-only: a
1221
+ # sandbox that refuses writes there also refuses the network an
1222
+ # install needs.
1223
+ provisioned = (self._user_runtime_root() / f"qmd-{QMD_VERSION}"
1224
+ / "node_modules" / ".bin" / "qmd")
1225
+ if provisioned.is_file():
1226
+ return provisioned
1136
1227
  # Existing installs provisioned the shared, version-addressed prefix;
1137
1228
  # keep resolving it so an upgrade is adoption, never breakage.
1138
1229
  legacy = Path.home() / ".egregore" / "runtime" / f"qmd-{QMD_VERSION}"
@@ -1490,7 +1581,7 @@ class QmdLocalRetriever:
1490
1581
  relative = str(candidate.relative_to(self.memory_root))
1491
1582
  except ValueError:
1492
1583
  return None
1493
- relative = relative.replace("\\", "/").lstrip("/")
1584
+ relative = relative.lstrip("/")
1494
1585
  if not relative or ".." in Path(relative).parts:
1495
1586
  return None
1496
1587
  return f"memory/{relative}"
@@ -1605,23 +1696,17 @@ class QmdLocalRetriever:
1605
1696
  title = normalized(metadata.title)
1606
1697
  return len(title) >= 8 and title in normalized(task)
1607
1698
 
1608
- def _scope_allows(self, canonical_path: str, request: RetrievalRequest) -> bool:
1609
- # Fail closed: a request that carries no authorized scope gets no
1610
- # hits. Observe always sets scopes; a direct caller without them is
1611
- # unauthorized by construction, not universally entitled.
1612
- if not request.authorized_scopes:
1613
- return False
1614
- for raw_scope in request.authorized_scopes:
1615
- scope = raw_scope.strip().replace("\\", "/").rstrip("/")
1616
- if scope == "memory":
1617
- return True
1618
- if not scope.startswith("memory/"):
1619
- scope = f"memory/{scope.lstrip('/')}"
1620
- if canonical_path == scope or canonical_path.startswith(f"{scope}/"):
1621
- return True
1622
- return False
1623
-
1624
1699
  def _source_revision(self) -> str:
1700
+ """The complete canonical revision: every cache and receipt keys on it.
1701
+
1702
+ A clean tree is its commit. A dirty tree is the commit plus a digest
1703
+ of the Markdown paths Git reports modified, deleted, renamed,
1704
+ untracked or ignored, with their current contents: the commit already
1705
+ pins every other file, so the digest need not read the whole corpus.
1706
+ Where Git's report cannot see what the index reads (a symbolic link,
1707
+ a submodule) or Git cannot report at all, every Markdown file is
1708
+ hashed instead.
1709
+ """
1625
1710
  if self._source_revision_cache is not None:
1626
1711
  return self._source_revision_cache
1627
1712
  completed = self._run_raw(
@@ -1629,22 +1714,174 @@ class QmdLocalRetriever:
1629
1714
  )
1630
1715
  if completed.returncode == 0 and completed.stdout.strip():
1631
1716
  head = completed.stdout.strip()
1632
- changes = self._run_raw(
1633
- (
1634
- "git", "-C", str(self.memory_root), "status", "--porcelain=v1",
1635
- "--untracked-files=all", "--ignored=matching", "--", "*.md",
1636
- ),
1637
- timeout=30,
1638
- )
1639
- if changes.returncode == 0 and not changes.stdout.strip():
1717
+ changes = self._markdown_changes()
1718
+ if changes is not None and not changes:
1640
1719
  revision = f"git:{head}"
1641
- else:
1720
+ elif changes is None or not self._git_report_covers_markdown():
1642
1721
  revision = f"git:{head}+dirty:{self._markdown_manifest_hash()}"
1722
+ else:
1723
+ revision = f"git:{head}+dirty:{self._changed_markdown_hash(changes)}"
1643
1724
  else:
1644
1725
  revision = f"filesystem:{self._markdown_manifest_hash()}"
1645
1726
  self._source_revision_cache = revision
1646
1727
  return revision
1647
1728
 
1729
+ def _markdown_changes(self) -> tuple[str, ...] | None:
1730
+ """The Markdown paths Git reports changed, untracked or ignored.
1731
+
1732
+ Both sides of a rename or copy are named. A directory Git does not
1733
+ descend into (an ignored folder, a nested repository) keeps its
1734
+ trailing slash. Paths are relative to the repository's top level.
1735
+ None when Git cannot report.
1736
+ """
1737
+ command = (
1738
+ "git", "-C", str(self.memory_root), "status", "--porcelain=v1", "-z",
1739
+ "--untracked-files=all", "--ignored=matching", "--", "*.md",
1740
+ )
1741
+ try:
1742
+ completed = subprocess.run(
1743
+ command,
1744
+ cwd=self.repository_root,
1745
+ env=self.environment,
1746
+ capture_output=True,
1747
+ timeout=30,
1748
+ check=False,
1749
+ umask=0o077,
1750
+ )
1751
+ except (OSError, subprocess.TimeoutExpired):
1752
+ return None
1753
+ if completed.returncode != 0:
1754
+ return None
1755
+ fields = completed.stdout.split(b"\0")
1756
+ paths: list[str] = []
1757
+ position = 0
1758
+ while position < len(fields):
1759
+ entry = fields[position]
1760
+ position += 1
1761
+ if not entry:
1762
+ continue
1763
+ if len(entry) < 4 or entry[2:3] != b" ":
1764
+ return None
1765
+ paths.append(os.fsdecode(entry[3:]))
1766
+ if b"R" in entry[:2] or b"C" in entry[:2]:
1767
+ if position >= len(fields) or not fields[position]:
1768
+ return None
1769
+ paths.append(os.fsdecode(fields[position]))
1770
+ position += 1
1771
+ return tuple(paths)
1772
+
1773
+ def _git_report_covers_markdown(self) -> bool:
1774
+ """Git's change report sees every change to what the index reads:
1775
+ no tracked link and no submodule can hide one."""
1776
+ top = self._git_top_level()
1777
+ if top is None or (top / ".gitmodules").exists():
1778
+ return False
1779
+ return not self._markdown_reads_through_a_link()
1780
+
1781
+ def _git_top_level(self) -> Path | None:
1782
+ cached = getattr(self, "_git_top_level_cache", None)
1783
+ if cached is not None:
1784
+ return cached
1785
+ completed = self._run_raw(
1786
+ ("git", "-C", str(self.memory_root), "rev-parse", "--show-toplevel"), timeout=10
1787
+ )
1788
+ if completed.returncode != 0 or not completed.stdout.strip():
1789
+ return None
1790
+ self._git_top_level_cache = Path(completed.stdout.strip()).resolve()
1791
+ return self._git_top_level_cache
1792
+
1793
+ def _changed_markdown_hash(self, changes: Sequence[str]) -> str:
1794
+ top = self._git_top_level()
1795
+ if top is None:
1796
+ return self._markdown_manifest_hash()
1797
+ reported: dict[str, Path | None] = {}
1798
+ for change in changes:
1799
+ path = top / change
1800
+ if not change.endswith("/"):
1801
+ reported[change] = path
1802
+ continue
1803
+ # Git names the directory, not the Markdown inside it.
1804
+ reported[change] = None
1805
+ if self.memory_root.is_relative_to(path):
1806
+ path = self.memory_root
1807
+ elif not path.is_relative_to(self.memory_root):
1808
+ continue
1809
+ for source in path.rglob("*.md"):
1810
+ reported.setdefault(source.relative_to(top).as_posix(), source)
1811
+ digest = hashlib.sha256()
1812
+
1813
+ def framed(kind: bytes, payload: bytes = b"") -> None:
1814
+ # A kind byte and a length frame each field, so no file content
1815
+ # can pass for a marker (a deleted file) or for the next path.
1816
+ digest.update(kind + len(payload).to_bytes(8, "big") + payload)
1817
+
1818
+ for relative in sorted(reported):
1819
+ source = reported[relative]
1820
+ framed(b"P", relative.encode("utf-8", "surrogateescape"))
1821
+ if source is None:
1822
+ framed(b"D")
1823
+ elif not os.path.lexists(source):
1824
+ framed(b"A")
1825
+ elif not source.is_file() or not source.resolve().is_relative_to(self.memory_root):
1826
+ framed(b"N")
1827
+ else:
1828
+ try:
1829
+ framed(b"F", source.read_bytes())
1830
+ except OSError:
1831
+ framed(b"U")
1832
+ return digest.hexdigest()[:20]
1833
+
1834
+ def _markdown_revision(self) -> str:
1835
+ """What the index reads, as a revision: used only to decide whether
1836
+ QMD must rescan.
1837
+
1838
+ It names the newest commit that changed a Markdown file, so a commit
1839
+ of other canonical files (a taste event, a YAML registry) does not
1840
+ send QMD over an unchanged corpus. When the index could read through
1841
+ a symbolic link, any commit may change what it reads, so the complete
1842
+ revision decides. Caches and receipts never key on this one.
1843
+ """
1844
+ source = self._source_revision()
1845
+ cached = self._markdown_revision_cache
1846
+ if cached is not None and cached[0] == source:
1847
+ return cached[1]
1848
+ revision = source
1849
+ if source.startswith("git:") and not self._markdown_reads_through_a_link():
1850
+ head, plus, dirty = source[len("git:"):].partition("+")
1851
+ completed = self._run_raw(
1852
+ ("git", "-C", str(self.memory_root), "rev-list", "-1", head, "--", "*.md"),
1853
+ timeout=10,
1854
+ )
1855
+ if completed.returncode == 0 and completed.stdout.strip():
1856
+ revision = f"git:{completed.stdout.strip()}{plus}{dirty}"
1857
+ self._markdown_revision_cache = (source, revision)
1858
+ return revision
1859
+
1860
+ def _markdown_reads_through_a_link(self) -> bool:
1861
+ """The repository tracks a symbolic link, or a tracked Markdown path
1862
+ lies below a directory that is one on disk; unknown counts as yes."""
1863
+ listed = self._run_raw(
1864
+ ("git", "-C", str(self.memory_root), "ls-files", "-s", "-z"), timeout=30,
1865
+ )
1866
+ if listed.returncode != 0:
1867
+ return True
1868
+ # Plain strings, not Path objects: this runs on every dirty revision.
1869
+ root = str(self.memory_root)
1870
+ checked: set[str] = set()
1871
+ for entry in listed.stdout.split("\0"):
1872
+ mode, _, name = entry.partition("\t")
1873
+ if mode.startswith("120000 "):
1874
+ return True
1875
+ if not name.endswith(".md"):
1876
+ continue
1877
+ parent = name.rpartition("/")[0]
1878
+ while parent and parent not in checked:
1879
+ if os.path.islink(os.path.join(root, parent)):
1880
+ return True
1881
+ checked.add(parent)
1882
+ parent = parent.rpartition("/")[0]
1883
+ return False
1884
+
1648
1885
  def _markdown_manifest_hash(self) -> str:
1649
1886
  digest = hashlib.sha256()
1650
1887
  for source in sorted(self.memory_root.rglob("*.md")):
@@ -1694,6 +1931,9 @@ class QmdLocalRetriever:
1694
1931
  state = json.loads(self._index_state_path().read_text(encoding="utf-8"))
1695
1932
  except (OSError, ValueError, TypeError):
1696
1933
  return False
1934
+ if not isinstance(state, dict):
1935
+ return False
1936
+ state.pop("markdown_revision", None)
1697
1937
  return state == {
1698
1938
  "adapter_version": QMD_VERSION,
1699
1939
  "collection": self.collection,
@@ -1705,6 +1945,18 @@ class QmdLocalRetriever:
1705
1945
  "source_revision": self._source_revision(),
1706
1946
  }
1707
1947
 
1948
+ def _index_markdown_current(self) -> bool:
1949
+ """The index already holds what it reads at the current revision:
1950
+ only files it never reads changed since it was built."""
1951
+ if not self._index_state_usable():
1952
+ return False
1953
+ try:
1954
+ state = json.loads(self._index_state_path().read_text(encoding="utf-8"))
1955
+ except (OSError, ValueError, TypeError):
1956
+ return False
1957
+ recorded = state.get("markdown_revision") if isinstance(state, dict) else None
1958
+ return bool(recorded) and recorded == self._markdown_revision()
1959
+
1708
1960
  def _index_state_usable(self) -> bool:
1709
1961
  """The BM25 index exists for this exact identity, revision aside.
1710
1962
 
@@ -1751,6 +2003,7 @@ class QmdLocalRetriever:
1751
2003
  "index_spec_version": INDEX_SPEC_VERSION,
1752
2004
  "memory_root": str(self.memory_root),
1753
2005
  "source_revision": self._source_revision(),
2006
+ "markdown_revision": self._markdown_revision(),
1754
2007
  }
1755
2008
  self._atomic_json(state_path, state)
1756
2009
 
@@ -1770,6 +2023,78 @@ class QmdLocalRetriever:
1770
2023
  "pending": count(r"Pending:\s*(\d+)"),
1771
2024
  }
1772
2025
 
2026
+ # -- Remembered status counts --------------------------------------------
2027
+ #
2028
+ # `qmd status` starts Node to count what the index database holds, and a
2029
+ # startup pays for it more than once (start, the embedding snapshot, the
2030
+ # launcher's follow-up status). The counts are a function of the database
2031
+ # files, so they are kept with the identity of everything they depend on
2032
+ # and reused only while that identity is unchanged and recent.
2033
+
2034
+ def _status_counts_path(self) -> Path:
2035
+ return self._runtime_root() / "status-counts.json"
2036
+
2037
+ def _status_identity(self) -> dict[str, Any]:
2038
+ """The index database and its write-ahead log, QMD's index
2039
+ configuration, this adapter's index and vector receipts, and the
2040
+ executables that would answer `qmd status`."""
2041
+
2042
+ def version(path: str | Path | None) -> list[int] | None:
2043
+ if not path:
2044
+ return None
2045
+ try:
2046
+ found = os.stat(path)
2047
+ except OSError:
2048
+ return None
2049
+ return [found.st_ino, found.st_size, found.st_mtime_ns]
2050
+
2051
+ index = self._index_path()
2052
+ command = self._command()
2053
+ executable = shutil.which(command[0], path=self.environment.get("PATH")) or command[0]
2054
+ node = self.environment.get("EGREGORE_QMD_NODE_BIN", "")
2055
+ config_dir = self.environment.get("QMD_CONFIG_DIR")
2056
+ config = Path(config_dir) / f"{self.index_name}.yml" if config_dir else None
2057
+ return {
2058
+ "schema": STATUS_COUNTS_SCHEMA,
2059
+ "qmd": [QMD_VERSION, INDEX_SPEC_VERSION, self.index_name, self.collection],
2060
+ # Every file argument too: `node /path/qmd.js` runs the script.
2061
+ "command": [*command, version(executable), [version(part) for part in command[1:]],
2062
+ node, version(node)],
2063
+ "database": version(index),
2064
+ "wal": version(f"{index}-wal"),
2065
+ "config": version(config),
2066
+ "receipts": [version(self._index_state_path()), version(self._semantic_state_path())],
2067
+ "index_path": str(index),
2068
+ }
2069
+
2070
+ def _remembered_status_counts(self) -> dict[str, int] | None:
2071
+ try:
2072
+ stored = json.loads(self._status_counts_path().read_text(encoding="utf-8"))
2073
+ if not 0 <= time.time() - float(stored["observed_at"]) <= STATUS_COUNTS_MAX_AGE_SECONDS:
2074
+ return None
2075
+ if stored["identity"] != self._status_identity():
2076
+ return None
2077
+ return {key: int(stored["counts"][key]) for key in ("documents", "vectors", "pending")}
2078
+ except (OSError, ValueError, TypeError, KeyError):
2079
+ return None
2080
+
2081
+ def _remember_status_counts(self, observed: dict[str, Any], counts: Mapping[str, int]) -> None:
2082
+ try:
2083
+ if observed != self._status_identity():
2084
+ return # the database moved while QMD counted it
2085
+ # Whole-second timestamps cannot tell two writes in one second
2086
+ # apart: wait until the newest is safely in the past.
2087
+ stamps = [observed[name][2] for name in ("database", "wal") if observed[name]]
2088
+ if stamps and all(stamp % 1_000_000_000 == 0 for stamp in stamps) \
2089
+ and time.time_ns() - max(stamps) < 2_000_000_000:
2090
+ return
2091
+ self._atomic_json(
2092
+ self._status_counts_path(),
2093
+ {"identity": observed, "counts": dict(counts), "observed_at": time.time()},
2094
+ )
2095
+ except OSError:
2096
+ pass # remembering is an optimization; health never fails on it
2097
+
1773
2098
  def _semantic_state_path(self) -> Path:
1774
2099
  return self._runtime_root() / "semantic-state.json"
1775
2100
 
@@ -1841,7 +2166,7 @@ class QmdLocalRetriever:
1841
2166
  },
1842
2167
  )
1843
2168
 
1844
- def _embedding_state(self, status: str, *, semantic_ready: bool = False) -> str:
2169
+ def _embedding_state(self, counts: Mapping[str, int], *, semantic_ready: bool = False) -> str:
1845
2170
  if self._live_lock(self._embed_lock_path()):
1846
2171
  return "running"
1847
2172
  # Model presence outranks stored-vector completeness: query-time
@@ -1851,7 +2176,6 @@ class QmdLocalRetriever:
1851
2176
  return "missing_model"
1852
2177
  if semantic_ready:
1853
2178
  return "ready"
1854
- counts = self._status_counts(status)
1855
2179
  if counts["pending"] > 0 or counts["documents"] > 0:
1856
2180
  return "pending"
1857
2181
  return "not_built"
@@ -1998,6 +2322,154 @@ class QmdLocalRetriever:
1998
2322
  return f"{operation} failed ({completed.returncode}): {detail[:400]}"
1999
2323
 
2000
2324
 
2325
+ def read_document_vectors(
2326
+ repository_root: Path,
2327
+ paths: Sequence[str],
2328
+ dimension_prefix: int = 256,
2329
+ *,
2330
+ memory_root: Path | None = None,
2331
+ environment: Mapping[str, str] | None = None,
2332
+ hashes: Mapping[str, str] | None = None,
2333
+ ) -> dict[str, Any]:
2334
+ """Read fresh, mean-pooled document embeddings without starting QMD.
2335
+
2336
+ Paths are relative to memory, optionally prefixed with ``memory/``. Each
2337
+ chunk's dimension prefix is L2-normalised, then the document mean is
2338
+ L2-normalised. Missing, stale, unsafe, or incomplete documents are absent.
2339
+ The fingerprint describes the coordinate system, not corpus membership.
2340
+ sqlite-vec's ordinary shadow tables need no extension; a read transaction
2341
+ includes WAL updates while keeping rows and vector blocks consistent.
2342
+ ``hashes`` (path → sha256 of the bytes the caller read) replaces reading
2343
+ the files, so a vector always belongs to the bytes the caller holds.
2344
+ """
2345
+ if not isinstance(dimension_prefix, int) or dimension_prefix < 1:
2346
+ raise ValueError("dimension_prefix must be a positive integer")
2347
+ adapter = QmdLocalRetriever(
2348
+ repository_root=repository_root, memory_root=memory_root, environment=environment,
2349
+ )
2350
+ result: dict[str, Any] = {"vectors": {}, "fingerprint": "", "dimension": dimension_prefix}
2351
+ index = adapter._index_path()
2352
+ if not index.is_file() or not paths:
2353
+ return result
2354
+ requested: dict[str, list[str]] = {}
2355
+ read: dict[str, str] = {}
2356
+ for original in sorted(set(paths)):
2357
+ relative = original.removeprefix("memory/")
2358
+ candidate = adapter.memory_root / relative
2359
+ if Path(relative).is_absolute() or ".." in Path(relative).parts:
2360
+ continue
2361
+ try:
2362
+ if hashes is not None:
2363
+ if original not in hashes:
2364
+ continue
2365
+ read[relative] = hashes[original]
2366
+ elif not candidate.resolve().is_relative_to(adapter.memory_root):
2367
+ continue
2368
+ else: # QMD hashes UTF-8 text, including any original line endings.
2369
+ read[relative] = hashlib.sha256(candidate.read_bytes()).hexdigest()
2370
+ requested.setdefault(relative, []).append(original)
2371
+ except OSError:
2372
+ continue
2373
+ try:
2374
+ with closing(sqlite3.connect(index.as_uri() + "?mode=ro", uri=True)) as connection:
2375
+ connection.execute("BEGIN")
2376
+ schema = connection.execute(
2377
+ "SELECT sql FROM sqlite_master WHERE name = 'vectors_vec'"
2378
+ ).fetchone()
2379
+ match = re.search(r"float\s*\[\s*(\d+)\s*\]", schema[0] if schema else "", re.I)
2380
+ if not match or dimension_prefix > int(match.group(1)):
2381
+ return result
2382
+ stored_dimensions = int(match.group(1))
2383
+ fresh: dict[str, list[str]] = {}
2384
+ for path, content_hash in connection.execute(
2385
+ "SELECT path, hash FROM documents WHERE collection = ? AND active = 1",
2386
+ (adapter.collection,),
2387
+ ):
2388
+ if path in read and read[path] == content_hash:
2389
+ fresh.setdefault(content_hash, []).extend(requested[path])
2390
+ if not fresh:
2391
+ return result
2392
+ rows = []
2393
+ ordered_hashes = sorted(fresh)
2394
+ for offset in range(0, len(ordered_hashes), 500):
2395
+ batch = ordered_hashes[offset:offset + 500]
2396
+ rows.extend(connection.execute(
2397
+ "SELECT c.hash, c.seq, c.total_chunks, c.model, c.embed_fingerprint, "
2398
+ "r.chunk_id, r.chunk_offset FROM content_vectors c "
2399
+ "JOIN vectors_vec_rowids r ON r.id = c.hash || '_' || c.seq "
2400
+ "WHERE c.hash IN (" + ",".join("?" for _ in batch) + ") "
2401
+ "ORDER BY c.hash, c.seq", batch,
2402
+ ).fetchall())
2403
+ models = {(str(row[3]), str(row[4])) for row in rows}
2404
+ result["fingerprint"] = hashlib.sha256(json.dumps(
2405
+ [INDEX_SPEC_VERSION, stored_dimensions, dimension_prefix, sorted(models)],
2406
+ separators=(",", ":"),
2407
+ ).encode()).hexdigest()[:20]
2408
+ # An index being re-embedded can contain fresh text hashes from
2409
+ # two models at once. Those vectors are not comparable, including
2410
+ # chunks of the same document. Let the caller select its complete
2411
+ # fallback representation instead of mixing coordinate systems.
2412
+ if len(models) != 1:
2413
+ return result
2414
+ blocks = {}
2415
+ for chunk_id in sorted({row[5] for row in rows}):
2416
+ block = connection.execute(
2417
+ "SELECT vectors FROM vectors_vec_vector_chunks00 WHERE rowid = ?", (chunk_id,)
2418
+ ).fetchone()
2419
+ if block:
2420
+ blocks[chunk_id] = block[0]
2421
+ pooled: dict[str, list[list[float]]] = {}
2422
+ expected: dict[str, int] = {}
2423
+ sequences: dict[str, set[int]] = {}
2424
+ invalid = set()
2425
+ decoder = struct.Struct("<" + "f" * dimension_prefix)
2426
+ for content_hash, seq, count, model, fingerprint, chunk_id, chunk_offset in rows:
2427
+ count = int(count)
2428
+ seen = sequences.setdefault(content_hash, set())
2429
+ if (not isinstance(seq, int) or count < 1 or not 0 <= seq < count
2430
+ or seq in seen or expected.get(content_hash, count) != count):
2431
+ invalid.add(content_hash)
2432
+ continue
2433
+ expected[content_hash] = count
2434
+ seen.add(seq)
2435
+ blob = blocks.get(chunk_id, b"")
2436
+ start = int(chunk_offset) * stored_dimensions * 4
2437
+ if start < 0 or start + decoder.size > len(blob):
2438
+ invalid.add(content_hash)
2439
+ continue
2440
+ vector = decoder.unpack_from(blob, start)
2441
+ norm = math.sqrt(sum(value * value for value in vector))
2442
+ if not math.isfinite(norm) or norm <= 0:
2443
+ invalid.add(content_hash)
2444
+ continue
2445
+ pooled.setdefault(content_hash, []).append([value / norm for value in vector])
2446
+ for content_hash, chunks in sorted(pooled.items()):
2447
+ if (content_hash in invalid or len(chunks) != expected[content_hash]
2448
+ or sequences[content_hash] != set(range(expected[content_hash]))):
2449
+ continue
2450
+ vector = [sum(values) for values in zip(*chunks)]
2451
+ norm = math.sqrt(sum(value * value for value in vector))
2452
+ if norm <= 0:
2453
+ continue
2454
+ vector = [value / norm for value in vector]
2455
+ for path in fresh[content_hash]:
2456
+ result["vectors"][path] = vector
2457
+ except (sqlite3.Error, OSError, ValueError, TypeError, struct.error):
2458
+ # A missing or incompatible optional index must leave callers able to
2459
+ # choose one complete fallback coordinate system for the export.
2460
+ result["vectors"] = {}
2461
+ return result
2462
+
2463
+
2464
+ def vectors_receipt(repository_root: Path, *, environment: Mapping[str, str] | None = None) -> Path:
2465
+ """Where a complete vector build leaves its receipt, found without starting anything: its
2466
+ change is the cheap sign that ``read_document_vectors`` may now read other vectors."""
2467
+ adapter = object.__new__(QmdLocalRetriever) # the path rules only; no Node check, no layout
2468
+ adapter.environment = dict(os.environ if environment is None else environment)
2469
+ adapter.memory_root = (Path(repository_root).resolve() / "memory").resolve()
2470
+ return adapter._semantic_state_path()
2471
+
2472
+
2001
2473
  __all__ = [
2002
2474
  "EMBEDDING_MODEL",
2003
2475
  "INDEX_SPEC_VERSION",
@@ -2007,4 +2479,6 @@ __all__ = [
2007
2479
  "QmdAdapterError",
2008
2480
  "QmdLocalRetriever",
2009
2481
  "QmdRuntimeMetadata",
2482
+ "read_document_vectors",
2483
+ "vectors_receipt",
2010
2484
  ]