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
@@ -7,7 +7,7 @@ implementation details. Infrastructure is composed behind ``local_runtime``.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- from .investigation_guidance import INVESTIGATION_GUIDANCE, RECALL_WORKFLOW_GUIDANCE
10
+ from .investigation_guidance import INVESTIGATION_GUIDANCE, RECALL_WORKFLOW_GUIDANCE, investigation_guidance
11
11
 
12
12
  import argparse
13
13
  import fcntl
@@ -16,12 +16,14 @@ from io import StringIO
16
16
  import json
17
17
  import os
18
18
  import re
19
+ import stat
19
20
  import subprocess
20
21
  import sys
22
+ import threading
21
23
  import time
22
24
  import unicodedata
23
25
  import uuid
24
- from contextlib import contextmanager, redirect_stdout
26
+ from contextlib import contextmanager, nullcontext, redirect_stdout
25
27
  from contextvars import ContextVar
26
28
  from dataclasses import asdict
27
29
  from datetime import UTC, datetime, timedelta
@@ -31,6 +33,7 @@ from .contracts import (
31
33
  EvidenceItem,
32
34
  OrgContext,
33
35
  Permission,
36
+ PolicyDecision,
34
37
  RetrievalMode,
35
38
  RetrievalRequest,
36
39
  TemporalScope,
@@ -40,7 +43,19 @@ from .identity import actor_lexical_formulations, organization_member_presentati
40
43
  from .investigation_contracts import InvestigationRequest, decode_request
41
44
  from . import episode_binding
42
45
  from .runtime import EgregoreRuntime, local_runtime
46
+ from .policy import is_admin
47
+ from .installation import installation_key, installation_root
48
+ from .optional_modules import optional_module
43
49
 
50
+ _record = optional_module(".record")
51
+ record_notice = _record.notice if _record is not None else lambda *args, **kwargs: None
52
+ member_cli = optional_module(".member_cli")
53
+ from .member_authority import MemberAuthorityError
54
+
55
+
56
+ # A later package adds a command as a new module with register(commands)
57
+ # and one line here; relative names resolve inside each Runtime bundle.
58
+ _COMMAND_MODULES: tuple[str, ...] = (".access_cli", ".mind_cli", ".outputs_cli")
44
59
 
45
60
  _HISTORICAL_SCOPE = re.compile(
46
61
  r"\b(history|historical|previous|older|old decision|superseded|what changed|how .* evolv(?:e|ed))\b",
@@ -98,10 +113,31 @@ _episode_session: ContextVar[str | None] = ContextVar("observe_episode_session",
98
113
  _prompt_binding: ContextVar[episode_binding.EpisodeBinding | None] = ContextVar('prompt_investigation_binding',default=None)
99
114
  _invocation_root: ContextVar[Path | None] = ContextVar('investigation_root',default=None)
100
115
 
116
+ def _runtime(root, *, push_remote=False):
117
+ member = (member_cli.current() if member_cli is not None else None)
118
+ return (member.runtime(root, push_remote=push_remote) if member
119
+ else local_runtime(root, push_remote=push_remote))
120
+
101
121
 
102
122
  @contextmanager
103
123
  def _invocation(root, reference=None):
104
124
  """A native prompt's explicit reference is stronger than shared shell state."""
125
+ member = (member_cli.current() if member_cli is not None else None)
126
+ if member:
127
+ binding = (episode_binding.resolve(root, reference) if reference
128
+ else episode_binding.resolve_native(root, harness="durable", session_id=member.session_id))
129
+ if binding is None or binding.session_id != member.session_id:
130
+ raise MemberAuthorityError()
131
+ session_token = _episode_session.set(member.session_id)
132
+ binding_token = _prompt_binding.set(binding)
133
+ root_token = _invocation_root.set(root)
134
+ try:
135
+ yield binding
136
+ finally:
137
+ _invocation_root.reset(root_token)
138
+ _prompt_binding.reset(binding_token)
139
+ _episode_session.reset(session_token)
140
+ return
105
141
  binding = _prompt_binding.get()
106
142
  if binding is None and (reference or os.environ.get('EGREGORE_EPISODE_ID')):
107
143
  binding = episode_binding.resolve(root,reference or os.environ['EGREGORE_EPISODE_ID'])
@@ -135,11 +171,11 @@ def _invocation(root, reference=None):
135
171
 
136
172
  def _execute_investigation(root, harness, request, *, runtime=None, actor=None, reference=None, request_id=None, origin='investigate'):
137
173
  with _invocation(root,reference) as binding:
138
- runtime = runtime or local_runtime(root,push_remote=False)
174
+ runtime = runtime or _runtime(root,push_remote=False)
139
175
  actor = actor or _actor(runtime,root,harness)
140
176
  episode_id = binding.episode_id if binding else _session_id(root)
141
177
  # Old receipts are read only as a migration bridge; new calls never write them.
142
- receipt = None if binding else _active_observe_receipt(root,harness)
178
+ receipt = None if binding or (member_cli.current() if member_cli is not None else None) else _active_observe_receipt(root,harness)
143
179
  contexts,initial_searches,bounds=[],0,{}
144
180
  if receipt:
145
181
  contexts=[entry.get('context',{}) for entry in _receipt_requests(receipt).values()]
@@ -150,12 +186,20 @@ def _execute_investigation(root, harness, request, *, runtime=None, actor=None,
150
186
  bounds['_not_before']=receipt['not_before']
151
187
  if receipt.get('terminal_gap'):
152
188
  bounds['_legacy_terminal_gap']=True
153
- return runtime.investigate(actor,request,episode_id=episode_id,seeds=contexts,
189
+ response = runtime.investigate(actor,request,episode_id=episode_id,seeds=contexts,
154
190
  initial_searches=initial_searches,initial_bounds=bounds,request_id=request_id,
155
191
  binding=binding,origin=origin)
192
+ _mind_call.set((response, origin, request))
193
+ return response
156
194
 
157
195
 
158
196
  def _publish_result(root, response, rendered, *, private=False, harness='shell'):
197
+ if (member_cli.current() if member_cli is not None else None):
198
+ # Member content stays on the authenticated host's private stdout.
199
+ # Local attachment, record and mind surfaces have their own operator
200
+ # lifecycle and are deliberately absent from this composition.
201
+ sys.stdout.write(rendered)
202
+ return False
159
203
  from .private_result import supports_attachment
160
204
  # An invalidated prompt cannot consume an attachment. Keep its error visible;
161
205
  # never use this fallback for successful evidence from a failed binding.
@@ -166,11 +210,13 @@ def _publish_result(root, response, rendered, *, private=False, harness='shell')
166
210
  if response['ok']:
167
211
  raise
168
212
  private = False
213
+ receipt = None
169
214
  if private:
170
215
  from .private_result import prepare
171
216
  reference=response['episode_id']
172
217
  session=episode_binding.resolve(root,reference).session_id if reference.startswith('ep_') else _session_id(root)
173
- print(prepare(root,response,rendered,session))
218
+ receipt = prepare(root,response,rendered,session)
219
+ print(receipt)
174
220
  else:
175
221
  from .private_result import record_stdout
176
222
  try:
@@ -180,12 +226,347 @@ def _publish_result(root, response, rendered, *, private=False, harness='shell')
180
226
  raise
181
227
  # A missing/corrupt receipt must not replace the operation's error.
182
228
  sys.stdout.write(rendered)
229
+ _mind_notice(root, response, rendered, harness, receipt)
183
230
  return private
184
231
 
185
232
 
233
+ # Notices for an open memory map (experiments/egregore-mind/AGENT-SPEC.md §3.6).
234
+ # Inert unless a map is open for this workspace. A notice goes to this
235
+ # checkout's door ($MIND_HOME, else experiments/egregore-mind/.mind) when its
236
+ # door.json names this checkout as the one it serves; only if that door took
237
+ # nothing, to the instance door (.egregore/runtime/mind under the installation
238
+ # this checkout belongs to) when its door.json names that installation. Either
239
+ # door needs a heartbeat under 3 s old and must be the user's alone, and no
240
+ # folder below the checkout or installation may be a link. Notices may cross
241
+ # the checkouts of one installation, never into another instance; `mind`
242
+ # requests stay per checkout (§3.3).
243
+ # Each notice is one small file of paths, counts and ids, never prompt, query or
244
+ # document text, and writing it never changes, fails or holds up the call it
245
+ # describes: every filesystem step runs on a writer thread the call waits for
246
+ # at most 0.1 s.
247
+ _mind_call: ContextVar[tuple | None] = ContextVar('mind_call', default=None)
248
+ _MIND_KINDS = ('seen-', 'asked-', 'delivered-')
249
+ _MIND_WAITING = 1000 # files a door may hold before notices are dropped (soft: writers do not lock)
250
+ _MIND_LARGEST = 65536 # bytes; a larger record is not written
251
+ _MIND_WRITERS = threading.BoundedSemaphore(4) # stuck writers a process may hold; then notices drop
252
+ _MIND_WORKSPACE = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.-]{0,63}')
253
+
254
+
255
+ def _mind_send(root, kind, harness, session, note):
256
+ if _record is None: # the Mind runtime is not installed
257
+ return
258
+ configured = os.environ.get('MIND_HOME')
259
+ native, codex = os.environ.get('EGREGORE_NATIVE_SESSION_ID'), os.environ.get('CODEX_THREAD_ID')
260
+ # Codex first: a Codex worker started from a Claude or Pi shell inherits that session's
261
+ # EGREGORE_NATIVE_* variables, and its prompt hook names it codex/<thread>, so its searches must
262
+ # too. The reverse nesting (Claude or Pi run inside a Codex shell) is split across two agents.
263
+ environment = (_mind_actor('codex', codex) if codex else
264
+ _mind_actor(os.environ.get('EGREGORE_NATIVE_HARNESS') or 'agent', native) if native else '')
265
+ if kind == 'asked' and harness == 'codex' and codex:
266
+ actor = _mind_actor('codex', codex) # one Codex session is one agent, whatever its hook calls itself
267
+ elif kind == 'delivered':
268
+ actor = _mind_actor(harness, session) if session else '' # only the hook that attached the result
269
+ else:
270
+ actor = _mind_actor(harness, session) if session else environment # the hook's own session first
271
+ if not _MIND_WRITERS.acquire(blocking=False):
272
+ return
273
+
274
+ def write():
275
+ try:
276
+ _mind_write(root, configured, kind, harness, actor, note)
277
+ except Exception:
278
+ pass # no map, a stopped map, or a door it must not use
279
+ finally:
280
+ _MIND_WRITERS.release()
281
+
282
+ try:
283
+ writer = threading.Thread(target=write, name='mind-notice', daemon=True)
284
+ writer.start()
285
+ except BaseException:
286
+ _MIND_WRITERS.release()
287
+ raise
288
+ writer.join(0.1)
289
+
290
+
291
+ def _mind_actor(harness, session):
292
+ """`<harness>/<session>` exactly as the map's bridge accepts an actor, else ''."""
293
+ harness = 'claude' if harness == 'claude-code' else str(harness or '')
294
+ if re.fullmatch(r'[a-z][a-z0-9-]{0,19}', harness) and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}', str(session)):
295
+ return f'{harness}/{session}'
296
+ return ''
297
+
298
+
299
+ def _mind_write(root, configured, kind, harness, actor, note):
300
+ """One notice: this checkout's door, else the instance door. True once one is renamed into place."""
301
+ here = os.path.realpath(root)
302
+ instance = os.path.realpath(installation_root(here))
303
+ folder = os.path.basename(here)
304
+ fields = {'actor': actor, 'harness': str(harness)[:40],
305
+ 'workspace': 'main' if here == instance else folder if _MIND_WORKSPACE.fullmatch(folder) else 'other',
306
+ **note}
307
+ checkout = [configured, 'door'] if configured else [str(root), 'experiments', 'egregore-mind', '.mind', 'door']
308
+ return (_mind_door(checkout, 'root', here, kind, fields) or # MIND_HOME never reaches the instance door
309
+ _mind_door([instance, '.egregore', 'runtime', 'mind', 'door'], 'instance', instance, kind, fields))
310
+
311
+
312
+ def _mind_door(walk, field, expected, kind, fields):
313
+ """Write one notice into the live door at ``walk`` whose door.json names ``expected`` as its ``field``.
314
+
315
+ Below the first folder none may be a link, so the door checked is the door written. True once the
316
+ notice is renamed into place; False when the door is absent, stopped, someone else's, full, names
317
+ another checkout or instance, or refuses the write.
318
+ """
319
+ held = []
320
+ try:
321
+ for part in walk:
322
+ held.append(os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NONBLOCK | (os.O_NOFOLLOW if held else 0),
323
+ dir_fd=held[-1] if held else None))
324
+ home, door = held[-2], held[-1]
325
+ beat, info = os.stat('heartbeat', dir_fd=door, follow_symlinks=False), os.fstat(door)
326
+ if (not stat.S_ISREG(beat.st_mode) or not -1 <= time.time() - beat.st_mtime <= 3
327
+ or info.st_uid != os.getuid() or info.st_mode & 0o077):
328
+ return False # a stopped map, or a door someone else could open
329
+ with os.fdopen(os.open('door.json', os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=home), 'rb') as handle:
330
+ pointer = json.loads(handle.read(4096))
331
+ if not isinstance(pointer, dict) or pointer.get(field) != expected:
332
+ return False # a map serving another checkout or instance, or one that does not say which
333
+ run = pointer.get('run')
334
+ record = json.dumps({'v': 2, 'k': kind, 't': datetime.now(UTC).isoformat(timespec='milliseconds')
335
+ .replace('+00:00', 'Z'), 'run': str(run)[:40] if run else None, **fields},
336
+ ensure_ascii=False) + '\n'
337
+ if len(record.encode('utf-8')) > _MIND_LARGEST:
338
+ return False
339
+ waiting, leftovers = 0, tuple('.' + k for k in _MIND_KINDS)
340
+ with os.scandir(door) as entries:
341
+ for entry in entries:
342
+ try:
343
+ if entry.name.startswith(leftovers) and time.time() - entry.stat(
344
+ follow_symlinks=False).st_mtime > 60:
345
+ os.unlink(entry.name, dir_fd=door) # a writer that died between create and rename
346
+ continue
347
+ except FileNotFoundError:
348
+ continue # its writer renamed it, or another writer reclaimed it, after the listing
349
+ waiting += entry.name.lstrip('.').startswith(_MIND_KINDS)
350
+ if waiting >= _MIND_WAITING:
351
+ return False
352
+ name = f'{kind}-{time.time_ns()}-{os.getpid()}-{uuid.uuid4().hex[:8]}'
353
+ descriptor = os.open('.' + name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=door)
354
+ try:
355
+ with os.fdopen(descriptor, 'w', encoding='utf-8') as handle:
356
+ handle.write(record)
357
+ os.rename('.' + name, name, src_dir_fd=door, dst_dir_fd=door)
358
+ except BaseException:
359
+ os.unlink('.' + name, dir_fd=door)
360
+ raise
361
+ return True
362
+ except (OSError, ValueError):
363
+ return False # no door here, or one it must not use: the next door may still take it
364
+ finally:
365
+ for descriptor in reversed(held):
366
+ os.close(descriptor)
367
+
368
+
369
+ def _mind_asked(root, harness, payload):
370
+ """The person asked something in this session: when and where, never what."""
371
+ try:
372
+ _mind_send(root, 'asked', harness, str(payload.get('session_id') or ''), {})
373
+ except Exception:
374
+ return
375
+
376
+
377
+ def _mind_digest(token):
378
+ """A short name two notices share for one attachment or cursor; never the token itself."""
379
+ return hashlib.sha256(str(token).encode()).hexdigest()[:16]
380
+
381
+
382
+ def _mind_delivered(root, harness, payload, receipt):
383
+ """The agent's own hook attached a prepared result: the seen notice with this packet reached the agent."""
384
+ try:
385
+ _mind_send(root, 'delivered', harness, str(payload.get('session_id') or ''), {'packet': _mind_digest(receipt)})
386
+ except Exception:
387
+ return
388
+
389
+
390
+ def _mind_notice(root, response, rendered, harness, receipt=None):
391
+ """What a find, open, query or investigate call showed the agent, or why it showed nothing.
392
+
393
+ With ``receipt`` (Claude's attachment line) the result was only prepared: it counts as seen
394
+ once a ``delivered`` notice with the same ``packet`` follows from the consuming hook.
395
+ """
396
+ if (member_cli.current() if member_cli is not None else None):
397
+ return
398
+ try:
399
+ call = _mind_call.get()
400
+ if call is None or call[0] is not response:
401
+ return
402
+ request = call[2].to_dict() if hasattr(call[2], 'to_dict') else dict(call[2])
403
+ operation = request.get('operation')
404
+ if operation not in {'discover', 'open', 'open_many'}:
405
+ return
406
+ projection = request.get('context_projection') or {}
407
+ retrieval = projection.get('retrieval') or {}
408
+ result = (response.get('result') or {}) if response.get('ok') else {}
409
+ context, details = result.get('context') or {}, result.get('coverage_details') or {}
410
+ def paths(values):
411
+ return list(dict.fromkeys(p for p in values if isinstance(p, str) and p.startswith('memory/')
412
+ and len(p) <= 300))[:100]
413
+ # A result path counts only when the published text names it, plain or as JSON either way:
414
+ # a query that missed its minimum evidence prints the gap, not the paths.
415
+ shown = [p for p in paths([row.get('path') for row in result.get('results', ())] +
416
+ [item.get('canonical_path') for item in context.get('evidence', ())])
417
+ if any(form in rendered for form in
418
+ (p, json.dumps(p)[1:-1], json.dumps(p, ensure_ascii=False)[1:-1]))]
419
+ opened = paths([s.get('path') for s in result.get('sources', [result] if 'content' in result else ())] +
420
+ (shown if retrieval.get('open_sources') else []))
421
+ kind = ({'lex': 'keyword', 'vec': 'semantic', 'lex+vec': 'hybrid'}.get(str(retrieval.get('mode')))
422
+ or result.get('operation') or request.get('kind') or 'keyword')
423
+ def day(value):
424
+ return value if isinstance(value, str) and re.fullmatch(r'\d{4}-\d{2}-\d{2}', value) else None
425
+ days = request.get('recent_days') or projection.get('recent_days')
426
+ def count(value):
427
+ return value if type(value) is int and 0 <= value <= 10**6 else None
428
+ if receipt:
429
+ from .private_result import MARKER
430
+ receipt = json.loads(receipt.removeprefix(MARKER))['id']
431
+ _mind_send(root, 'seen', harness, None, {
432
+ 'episode': str(response.get('episode_id') or '')[:80] or None,
433
+ 'invocation': str(response.get('invocation_id') or '')[:80] or None,
434
+ 'call': call[1], 'op': operation, 'delivery': 'attachment' if receipt else 'stdout',
435
+ 'packet': _mind_digest(receipt) if receipt else None,
436
+ 'kind': kind if operation == 'discover' and kind in
437
+ {'filename', 'literal', 'dates', 'keyword', 'semantic', 'hybrid'} else None,
438
+ # No query words: the kind, dates and paths say where the agent looked, never what it typed.
439
+ # A next page carries only its cursor: `continues` names the notice whose `next` handed
440
+ # that cursor out, and that notice holds the kind and dates this page continues.
441
+ 'continues': _mind_digest(request['cursor']) if request.get('cursor') else None,
442
+ 'next': _mind_digest(result['next_cursor']) if result.get('next_cursor') else None,
443
+ # This call's own date arguments. Bounds the Runtime kept from earlier calls in the
444
+ # prompt are not in its response, so these are never the whole scope searched.
445
+ 'requested': {'after': day(request.get('after')), 'before': day(request.get('before')),
446
+ 'recent_days': days if type(days) is int and 1 <= days <= 36500 else None},
447
+ 'paths': shown, 'opened': opened,
448
+ 'searches_left': count(result.get('searches_remaining')), 'reads_left': count(result.get('reads_remaining')),
449
+ 'coverage': {'degraded': bool(result.get('degraded') or context.get('degraded')),
450
+ 'absent_or_stale': count(details.get('stale_or_missing_count', 0)),
451
+ 'vectors_missing': count(details.get('vector_missing_count', 0)),
452
+ 'omitted': len(context.get('omissions') or ()),
453
+ 'more': count(result.get('remaining_in_result_set', 0)),
454
+ 'reach': str(result.get('coverage') or '')[:120]},
455
+ 'ok': bool(response.get('ok')), 'error': str(response.get('error_code') or '')[:40] or None})
456
+ except Exception:
457
+ return
458
+
459
+
460
+ # A worktree's parser and held rules can differ even when this hook's own
461
+ # source is identical. Compose and validate with that checkout's imported code,
462
+ # not just its data root. The bootstrap uses APIs present in older checkouts too.
463
+ _RESULT_CONSUMER = '''
464
+ import json, sys
465
+ from pathlib import Path
466
+ root = Path(sys.argv[1])
467
+ sys.path.insert(0, str(root))
468
+ try:
469
+ from egregore_runtime import private_result
470
+ from egregore_runtime.runtime import local_runtime
471
+ loaded = Path(private_result.__file__).resolve()
472
+ if not loaded.is_relative_to(root.resolve()) or loaded.parent != (root / 'egregore_runtime').resolve():
473
+ raise ValueError('packet Runtime could not be loaded from its checkout')
474
+ for name, module in tuple(sys.modules.items()):
475
+ if name == 'egregore_runtime' or name.startswith('egregore_runtime.'):
476
+ filename = getattr(module, '__file__', None)
477
+ if not filename or not Path(filename).resolve().is_relative_to(root.resolve()):
478
+ raise ValueError('packet Runtime could not be loaded from its checkout')
479
+ request = json.load(sys.stdin)
480
+ runtime = local_runtime(root, push_remote=False)
481
+ actor = runtime.resolve_actor(session_id=request['session_id'], harness=request['harness'])
482
+ rendered = private_result.consume(root, request['id'], runtime, actor,
483
+ session_id=request['session_id'], native_turn_id=request['native_turn_id'])
484
+ result = {'rendered': rendered}
485
+ except Exception as error:
486
+ result = {'error': str(error)}
487
+ print(json.dumps(result))
488
+ '''
489
+
490
+ # Context-projection packets recheck the same collection and index revision as
491
+ # the search. Keep those data-location overrides, not the harness environment.
492
+ _RESULT_CONSUMER_ENV = (
493
+ 'PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE',
494
+ 'EGREGORE_QMD_COLLECTION', 'EGREGORE_QMD_INDEX_PATH', 'EGREGORE_QMD_RUNTIME_DIR',
495
+ )
496
+
497
+
498
+ def _validate_packet_runtime(root: Path) -> None:
499
+ """Reject unchecked import paths anywhere in the packet's Runtime package."""
500
+ package = root / 'egregore_runtime'
501
+
502
+ def check(entry, regular_type):
503
+ # lstat rejects links, including directory links that os.walk won't follow.
504
+ if (not regular_type(entry.lstat().st_mode)
505
+ or not entry.resolve(strict=True).is_relative_to(checkout)):
506
+ raise ValueError('uncontained Runtime entry')
507
+
508
+ def walk_error(error):
509
+ raise error
510
+
511
+ try:
512
+ checkout = root.resolve(strict=True)
513
+ check(package, stat.S_ISDIR)
514
+ for directory, directories, files in os.walk(package, followlinks=False, onerror=walk_error):
515
+ for name in directories:
516
+ check(Path(directory) / name, stat.S_ISDIR)
517
+ for name in files:
518
+ check(Path(directory) / name, stat.S_ISREG)
519
+ # Preserve the required package entry points as well as tree containment.
520
+ for name in ('__init__.py', 'private_result.py'):
521
+ check(package / name, stat.S_ISREG)
522
+ except (OSError, RuntimeError, ValueError) as error:
523
+ raise EgregoreRuntimeError('packet Runtime is not contained in its checkout') from error
524
+
525
+
526
+ def _consume_private_result(root, identifier, *, session_id, harness, native_turn_id):
527
+ """Use the admitted packet checkout's Runtime without mixing module versions."""
528
+ from .private_result import consume
529
+ package = root / 'egregore_runtime'
530
+ # Data-only Runtime roots use the installed package. A checkout carrying
531
+ # its own package must validate with that package, with no fallback on error.
532
+ checkout_package = package.exists() or package.is_symlink()
533
+ if checkout_package:
534
+ _validate_packet_runtime(root)
535
+ if checkout_package and package.resolve() != Path(__file__).resolve().parent:
536
+ environment = {name: os.environ[name] for name in _RESULT_CONSUMER_ENV if name in os.environ}
537
+ environment['EGREGORE_ROOT'] = str(root)
538
+ try:
539
+ worker = subprocess.run(
540
+ [sys.executable, '-I', '-B', '-c', _RESULT_CONSUMER, str(root)],
541
+ input=json.dumps({'id': identifier, 'session_id': session_id,
542
+ 'harness': harness, 'native_turn_id': native_turn_id}),
543
+ cwd=root, env=environment,
544
+ capture_output=True, text=True, timeout=30)
545
+ except subprocess.TimeoutExpired as error:
546
+ raise EgregoreRuntimeError('packet Runtime validation timed out') from error
547
+ if worker.returncode:
548
+ raise EgregoreRuntimeError('packet Runtime validation failed')
549
+ try:
550
+ result = json.loads(worker.stdout)
551
+ if not isinstance(result, dict):
552
+ raise ValueError('expected a result object')
553
+ if 'error' in result:
554
+ raise EgregoreRuntimeError(str(result['error']))
555
+ if not isinstance(result.get('rendered'), str):
556
+ raise ValueError('expected rendered evidence')
557
+ return result['rendered']
558
+ except (ValueError, TypeError) as error:
559
+ # Never include worker stdout in a failure: it may contain evidence.
560
+ raise EgregoreRuntimeError('packet Runtime returned an invalid delivery') from error
561
+ runtime = local_runtime(root, push_remote=False)
562
+ actor = runtime.resolve_actor(session_id=session_id, harness=harness)
563
+ return consume(root, identifier, runtime, actor, session_id=session_id, native_turn_id=native_turn_id)
564
+
565
+
186
566
  def _result_hook(args):
187
567
  """Native PostToolUse adapter; stdin and stdout use the host hook contract."""
188
- from .private_result import allowed_root, consume, receipts_from_tool
568
+ from .private_result import allowed_root, receipts_from_tool
569
+ delivered=[]
189
570
  try:
190
571
  payload=json.load(sys.stdin)
191
572
  if payload.get('tool_name')!='Bash':
@@ -197,20 +578,25 @@ def _result_hook(args):
197
578
  for receipt in receipts_from_tool(payload):
198
579
  root=allowed_root(_root(),receipt['root'])
199
580
  session=str(payload.get('session_id') or _session_id(root))
200
- runtime=local_runtime(root,push_remote=False)
201
- actor=runtime.resolve_actor(session_id=session,harness=args.harness)
202
- results.append(consume(root,receipt['id'],runtime,actor,session_id=session,
203
- native_turn_id=payload.get('turn_id') or payload.get('prompt_id')))
581
+ results.append(_consume_private_result(root,receipt['id'],session_id=session,harness=args.harness,
582
+ native_turn_id=payload.get('turn_id') or payload.get('prompt_id')))
583
+ delivered.append((root,receipt['id']))
204
584
  if not results:
205
585
  return 0
206
586
  context='EGREGORE_RETRIEVAL_CONTEXT_V1\nAuthorized Runtime evidence for this invocation. Do not reproduce ranking traces.\n'+'\n'.join(results)+'\nEND_EGREGORE_RETRIEVAL_CONTEXT_V1'
207
587
  except (OSError,ValueError,TypeError,KeyError,EgregoreRuntimeError) as error:
208
588
  context=f'Runtime private result could not be attached: {error}. Do not treat it as received evidence.'
589
+ delivered=[] # nothing in this tool call reached the agent
209
590
  print(json.dumps({'hookSpecificOutput':{'hookEventName':'PostToolUse','additionalContext':context}}))
591
+ for root,receipt in delivered:
592
+ record_notice(root,'delivered',args.harness,payload.get('session_id'))
593
+ _mind_delivered(root,args.harness,payload,receipt)
210
594
  return 0
211
595
 
212
596
 
213
597
  def _session_id(root: Path) -> str:
598
+ if member := (member_cli.current() if member_cli is not None else None):
599
+ return member.session_id
214
600
  if pinned := _episode_session.get():
215
601
  return pinned
216
602
  configured = os.environ.get("EGREGORE_SESSION_ID", "").strip()
@@ -293,11 +679,83 @@ def _actor_contract_marker_path(
293
679
  session there and rewritten by each new one, so keying on it alone made
294
680
  parallel sessions re-attach the full contract on every prompt."""
295
681
 
682
+ return _session_state_path(root, "actor-contract", harness, session, ".sent")
683
+
684
+
685
+ def _session_state_path(root: Path, kind: str, harness: str, session: str | None, suffix: str) -> Path:
296
686
  session = re.sub(r"[^A-Za-z0-9_.-]", "-", session or _session_id(root))[:120]
297
- return (
298
- root / ".egregore" / "runtime" / "observe"
299
- / f"actor-contract-{harness}-{session}.sent"
687
+ return root / ".egregore" / "runtime" / "observe" / f"{kind}-{harness}-{session}{suffix}"
688
+
689
+
690
+ def _situated_hints_enabled(root: Path) -> bool:
691
+ """Off switch: ``egregore.json`` → ``knowledge.situated_hints: false``."""
692
+ try:
693
+ config = json.loads((root / "egregore.json").read_text(encoding="utf-8"))
694
+ except (OSError, ValueError):
695
+ return True
696
+ knowledge = config.get("knowledge") if isinstance(config, dict) else None
697
+ return not (isinstance(knowledge, dict) and knowledge.get("situated_hints") is False)
698
+
699
+
700
+ def _working_paths(root: Path) -> list[str]:
701
+ """Paths changed in this workspace: tracked edits plus untracked files."""
702
+ paths: list[str] = []
703
+ # NUL-delimited so Git does not quote non-ASCII names ("caf\303\251.md").
704
+ for args in (("diff", "--name-only", "-z", "HEAD"), ("ls-files", "-z", "--others", "--exclude-standard")):
705
+ try:
706
+ result = subprocess.run(["git", "-C", str(root), *args], capture_output=True, text=True, timeout=2)
707
+ except (OSError, subprocess.TimeoutExpired):
708
+ continue
709
+ if result.returncode == 0:
710
+ paths.extend(name for name in result.stdout.split("\0") if name)
711
+ return list(dict.fromkeys(paths))[:500]
712
+
713
+
714
+ def _sweep_situated_state(directory: Path, max_age_days: int = 7) -> None:
715
+ """Drop per-session offer records older than a week; a new session starts one."""
716
+ cutoff = time.time() - max_age_days * 86400
717
+ try:
718
+ for stale in directory.glob("situated-*.json"):
719
+ try:
720
+ if stale.stat().st_mtime < cutoff:
721
+ stale.unlink()
722
+ except OSError:
723
+ continue
724
+ except OSError:
725
+ return
726
+
727
+
728
+ def _situated_hint(runtime, root: Path, harness: str, session: str | None, prompt: str) -> str:
729
+ """At most one offer line per prompt, each pattern at most once per session.
730
+
731
+ An offer, never a gate: the working changes and the prompt are matched
732
+ against pattern situations; nothing is retrieved for the model.
733
+ """
734
+ memory = root / "memory"
735
+ if not (memory / "knowledge" / "patterns").is_dir() or not _situated_hints_enabled(root):
736
+ return ""
737
+ from .knowledge import CanonicalKnowledgeService, situated_hint_line
738
+ state = _session_state_path(root, "situated", harness, session, ".json")
739
+ try:
740
+ offered = set(json.loads(state.read_text(encoding="utf-8")))
741
+ except (OSError, ValueError, TypeError):
742
+ offered = set()
743
+ _sweep_situated_state(state.parent)
744
+ found = CanonicalKnowledgeService(runtime, memory).situated(
745
+ _actor(runtime, root, harness), paths=_working_paths(root), text=prompt,
746
+ limit=len(offered) + 3, count_instances=False,
300
747
  )
748
+ for hit in found["patterns"]:
749
+ line = situated_hint_line(hit["title"], hit["canonical_path"])
750
+ if hit["id"] in offered or not line:
751
+ continue
752
+ try:
753
+ state.parent.mkdir(parents=True, exist_ok=True)
754
+ state.write_text(json.dumps(sorted(offered | {hit["id"]})), encoding="utf-8")
755
+ except OSError:
756
+ return "" # without the record the same offer would repeat every prompt
757
+ return line
758
+ return ""
301
759
 
302
760
 
303
761
  def _receipt_requests(receipt) -> dict:
@@ -511,11 +969,12 @@ def _query(args: argparse.Namespace) -> int:
511
969
 
512
970
 
513
971
  def _query_in_episode(args: argparse.Namespace, root: Path) -> int:
514
- runtime = local_runtime(root, push_remote=False)
972
+ runtime = _runtime(root, push_remote=False)
515
973
  actor = _actor(runtime, root, args.harness)
516
974
  request = _request(
517
975
  task=args.task, org_id=actor.profile.org_id, actor_id=actor.actor.actor_id,
518
- lex=identity_aware_lexical_formulations(root, actor, args.task, tuple(args.lex)),
976
+ lex=identity_aware_lexical_formulations(root, actor, args.task, tuple(args.lex),
977
+ decision=runtime.authorize(actor, Permission.READ)),
519
978
  vec=tuple(args.vec), mode=args.mode, limit=args.limit,
520
979
  temporal_scope=getattr(args, 'temporal_scope', None),
521
980
  recent_days=getattr(args, 'recent_days', None),
@@ -535,6 +994,7 @@ def _query_in_episode(args: argparse.Namespace, root: Path) -> int:
535
994
  private=True,harness=args.harness)
536
995
  return 0 if attached else 2 # Claude's attachment carries its operation error.
537
996
  print(response['error'], file=sys.stderr)
997
+ _mind_notice(root, response, '', args.harness)
538
998
  return 2
539
999
  context = _restore_context(response['result']['context'])
540
1000
  entry = _observe_entry(context)
@@ -585,35 +1045,41 @@ def _actor_context_block(
585
1045
  if value and value.strip() and value.strip().casefold() != actor.display_name.casefold()
586
1046
  )
587
1047
  ) or "none"
1048
+ # Prompt context is a read surface: only profiles this actor may read name anyone.
1049
+ readable = runtime.authorize(actor_context, Permission.READ)
588
1050
  member_names = "; ".join(
589
1051
  f"{', '.join(member.aliases)} → {member.display_name}"
590
1052
  if member.aliases
591
1053
  else member.display_name
592
- for member in organization_member_presentations(root, actor_context)
1054
+ for member in organization_member_presentations(root, actor_context, decision=readable)
593
1055
  ) or "unavailable"
594
1056
  search_command = 'bash bin/search.sh find "<query>" --kind <kind>'
595
1057
  if harness == "claude":
596
1058
  search_command += " --context-packet"
597
1059
  if not full:
1060
+ # Rides on every later prompt, so every line here costs once per turn.
598
1061
  return "\n".join(
599
1062
  (
600
1063
  "EGREGORE_ACTOR_CONTEXT_V1",
601
- "Reminder — the full contract was attached earlier in this session.",
602
- RECALL_WORKFLOW_GUIDANCE,
1064
+ "Reminder: the full contract was attached earlier in this session; if it is no longer "
1065
+ "in your context, read `.claude/context/retrieval-investigation.md` once.",
603
1066
  f"Actor: {actor.display_name} · Actor ID: {actor.actor_id} · "
604
1067
  f"Organization ID: {actor_context.profile.org_id} · Runtime root: {root}",
1068
+ "`me` and `myself` mean this actor; never infer identity from OS, Git, or GitHub names.",
605
1069
  _RECALL_ONLY_SCOPE,
1070
+ RECALL_WORKFLOW_GUIDANCE,
606
1071
  "When a prompt needs that recall and no EGREGORE_ORG_CONTEXT_V1 block is "
607
1072
  f"attached, choose the operation kind and use `{search_command}`; exact latest/oldest/open "
608
1073
  "handoff questions use `bash bin/search.sh handoffs …` instead.",
609
- 'Reuse evidence; batch source reads with search.sh open path-a path-b. '
610
- 'Runtime retains evidence and enforces the prompt budget and date boundary.',
611
- "`me` and `myself` mean this Actor ID and display name; never infer identity "
612
- "from the OS user, Git, or GitHub names.",
613
1074
  "END_EGREGORE_ACTOR_CONTEXT_V1",
614
1075
  )
615
1076
  )
616
- return "\n".join(
1077
+ packet_note = (
1078
+ "On Claude, `--context-packet` keeps the internal evidence trace out of the user "
1079
+ "transcript and attaches it through PostToolUse. Keep that flag on model-led "
1080
+ "searches; never repeat the attached ranking trace in the answer."
1081
+ ) if harness == "claude" else None
1082
+ return "\n".join(line for line in
617
1083
  (
618
1084
  "EGREGORE_ACTOR_CONTEXT_V1",
619
1085
  "Resolved automatically by EgregoreRuntime before the ritual.",
@@ -638,25 +1104,18 @@ def _actor_context_block(
638
1104
  "work, a prior handoff, or continuation of earlier work and no "
639
1105
  "EGREGORE_ORG_CONTEXT_V1 block is attached, choose the operation kind and use "
640
1106
  f"`{search_command}`. ",
641
- INVESTIGATION_GUIDANCE,
642
- "On Claude, `--context-packet` keeps the internal evidence trace out of the user "
643
- "transcript and attaches it through PostToolUse. Keep that flag on model-led "
644
- "searches; never repeat the attached ranking trace in the answer.",
645
- "Exact lifecycle questions route differently: recognize them from meaning, not "
646
- "fixed phrases. When the user asks for the latest/newest/oldest handoff addressed "
647
- "to or sent by a specific person (including `me`), or open/unresolved handoffs, "
648
- "run the typed deterministic lookup `bash bin/search.sh handoffs "
649
- "[--mine|--sent|--addressed-to X|--sent-by X] [--status open] "
650
- "[--order newest|oldest] [--limit N] [--open]` instead of a semantic query — one "
651
- "memory-form retrieval beat, then answer from its result. Topical or synthesis "
652
- "questions about handoffs (`the handoff about privacy`) remain semantic "
653
- "`search.sh find` recall.",
1107
+ investigation_guidance(harness),
1108
+ packet_note,
1109
+ "Recognize exact lifecycle questions from meaning: the latest/newest/oldest handoff "
1110
+ "addressed to or sent by someone (including `me`), or open/unresolved handoffs, use "
1111
+ "`bash bin/search.sh handoffs` with [--mine|--sent|--addressed-to X|--sent-by X] "
1112
+ "[--status open] [--order newest|oldest] [--limit N] [--open], never a semantic "
1113
+ "query. Topical handoff questions (`the handoff about privacy`) stay `search.sh find` recall.",
654
1114
  "Do not substitute Activity, Dashboard, Project, Git inspection, optional hosted indexes, or raw "
655
1115
  "memory scans for that recall. A named request to display a status card is an "
656
1116
  "explicit status workflow; a question asking for an explained synthesis of "
657
1117
  "organizational work is Runtime/QMD recall. Do not query optional hosted indexes unless the user "
658
- "explicitly requests relationship "
659
- "or optional hosted index status.",
1118
+ "explicitly requests relationship or optional hosted index status.",
660
1119
  "For every identity-bearing workflow, `me`, `myself`, `from me`, "
661
1120
  "and `to myself` mean this Actor ID and display name. Use the "
662
1121
  "display name for presentation and the Actor ID for ownership, "
@@ -664,7 +1123,7 @@ def _actor_context_block(
664
1123
  "respondent fields. Never infer identity from the OS user, Git, "
665
1124
  "GitHub, a branch name, or prose aliases.",
666
1125
  "END_EGREGORE_ACTOR_CONTEXT_V1",
667
- )
1126
+ ) if line is not None
668
1127
  )
669
1128
 
670
1129
 
@@ -691,13 +1150,16 @@ def identity_aware_lexical_formulations(
691
1150
  actor: object,
692
1151
  task: str,
693
1152
  formulations: tuple[str, ...],
1153
+ *,
1154
+ decision: PolicyDecision,
694
1155
  ) -> tuple[str, ...]:
695
1156
  """Bind the active actor and expand explicitly named organization members.
696
1157
 
697
1158
  This keeps cold BM25 retrieval equivalent to stable identity resolution:
698
1159
  a chosen display name, provider handle, and legacy local alias all refer to
699
1160
  one member. First-person references use the active actor; the directory is
700
- never added wholesale. Semantic identity binding happens at request construction.
1161
+ never added wholesale, and only profiles ``decision`` lets the reader see
1162
+ add terms. Semantic identity binding happens at request construction.
701
1163
  """
702
1164
 
703
1165
  def normalized(value: str) -> str:
@@ -710,7 +1172,7 @@ def identity_aware_lexical_formulations(
710
1172
  task_tokens = f" {normalized(task)} "
711
1173
  expanded = list(actor_lexical_formulations(actor, task, formulations))
712
1174
  seen = {value.strip().casefold() for value in expanded if value.strip()}
713
- for member in organization_member_presentations(root, actor):
1175
+ for member in organization_member_presentations(root, actor, decision=decision):
714
1176
  references = (member.display_name, *member.aliases)
715
1177
  if not any(
716
1178
  reference_normalized
@@ -727,7 +1189,95 @@ def identity_aware_lexical_formulations(
727
1189
  return tuple(expanded)
728
1190
 
729
1191
 
1192
+ def _member_prompt_hook(args):
1193
+ """Trusted Durable hook: member identity only, no operator startup surfaces."""
1194
+ member = (member_cli.current() if member_cli is not None else None)
1195
+ raw = sys.stdin.read(262145)
1196
+ if len(raw) > 262144:
1197
+ raise MemberAuthorityError()
1198
+ try:
1199
+ payload = json.loads(raw)
1200
+ except (ValueError, TypeError):
1201
+ raise MemberAuthorityError() from None
1202
+ if (not isinstance(payload, dict) or payload.get("session_id") != member.native_session
1203
+ or not isinstance(payload.get("prompt", payload.get("user_prompt", "")), str)):
1204
+ raise MemberAuthorityError()
1205
+ prompt = payload.get("prompt", payload.get("user_prompt", "")).strip()
1206
+ runtime = member.runtime(member.root)
1207
+ actor = runtime.resolve_actor(session_id=member.session_id, harness="durable")
1208
+ if not runtime.authorize(actor, Permission.READ).allowed:
1209
+ raise MemberAuthorityError()
1210
+ binding = episode_binding.begin(
1211
+ member.root, actor, harness="durable", session_id=member.session_id,
1212
+ native_turn_id=payload.get("turn_id") or payload.get("prompt_id"), question=prompt,
1213
+ )
1214
+ context = "\n".join((
1215
+ episode_binding.context_line(binding),
1216
+ "EGREGORE_ACTOR_CONTEXT_V1",
1217
+ f"Actor ID: {actor.actor.actor_id}",
1218
+ f"Actor display name: {actor.actor.display_name}",
1219
+ f"Organization ID: {actor.profile.org_id}",
1220
+ "`me` and `myself` mean this verified member, never the host operator.",
1221
+ "Organizational retrieval status: NOT_PERFORMED.",
1222
+ _RECALL_ONLY_SCOPE,
1223
+ 'Use the supplied memory tools: `bash bin/search.sh find "<query>" --kind <kind>`, '
1224
+ '`bash bin/search.sh open memory/path.md`, or `bash bin/search.sh handoffs --mine`.',
1225
+ "The host supplies member authority to those tools. Status and identity context do not grant access.",
1226
+ INVESTIGATION_GUIDANCE,
1227
+ "END_EGREGORE_ACTOR_CONTEXT_V1",
1228
+ ))
1229
+ # Verification is current at delivery, not only before writing the binding.
1230
+ if not runtime.authorize(actor, Permission.READ).allowed:
1231
+ raise MemberAuthorityError()
1232
+ print(json.dumps({"hookSpecificOutput": {
1233
+ "hookEventName": "UserPromptSubmit", "additionalContext": context,
1234
+ }}, separators=(",", ":")))
1235
+ return 0
1236
+
1237
+
1238
+ def _member_ready(args):
1239
+ member = (member_cli.current() if member_cli is not None else None)
1240
+ if member is None:
1241
+ raise MemberAuthorityError()
1242
+ runtime = member.runtime(member.root)
1243
+ actor = runtime.resolve_actor(session_id=member.session_id, harness="durable")
1244
+ if not runtime.retrieval_health().available:
1245
+ raise MemberAuthorityError()
1246
+ if not all(runtime.authorize(actor, operation).allowed
1247
+ for operation in (Permission.DISCOVER, Permission.READ)):
1248
+ raise MemberAuthorityError()
1249
+ print(json.dumps({
1250
+ "schema": "egregore.member-ready/v1",
1251
+ "accountId": actor.account.account_id,
1252
+ "orgId": actor.profile.org_id,
1253
+ "actorId": actor.actor.actor_id,
1254
+ "membershipId": actor.membership.membership_id,
1255
+ "verifiedAt": int(time.time() * 1000),
1256
+ }, separators=(",", ":")))
1257
+ return 0
1258
+
1259
+
1260
+ def _bind_native_session(root: Path, harness: str, session) -> None:
1261
+ """Codex starts before its thread exists, so its first prompt maps it.
1262
+
1263
+ bin/session-id.sh binds the thread to the Egregore session id it resolves
1264
+ now and keeps an existing map, so commits made in this thread keep their
1265
+ own Egregore-Session trailer after a later session start elsewhere.
1266
+ """
1267
+ script = Path(root) / 'bin' / 'session-id.sh'
1268
+ if not isinstance(session, str) or not session or not script.is_file():
1269
+ return
1270
+ try:
1271
+ subprocess.run(['bash', str(script), 'bind', harness, session, '--root', str(root)],
1272
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
1273
+ stderr=subprocess.DEVNULL, timeout=2, check=False)
1274
+ except (OSError, subprocess.SubprocessError):
1275
+ pass # a missing map falls back to the shared file; never cost the prompt
1276
+
1277
+
730
1278
  def _prompt_hook(args: argparse.Namespace) -> int:
1279
+ if (member_cli.current() if member_cli is not None else None):
1280
+ return _member_prompt_hook(args)
731
1281
  try:
732
1282
  payload = json.load(sys.stdin)
733
1283
  except (json.JSONDecodeError, OSError):
@@ -735,7 +1285,12 @@ def _prompt_hook(args: argparse.Namespace) -> int:
735
1285
  if not isinstance(payload, dict):
736
1286
  return 0
737
1287
  prompt = str(payload.get("prompt") or payload.get("user_prompt") or "").strip()
738
- root = _prompt_root(payload, _root())
1288
+ launch = _root()
1289
+ root = _prompt_root(payload, launch)
1290
+ if args.harness == 'codex':
1291
+ _bind_native_session(launch, 'codex', payload.get('session_id'))
1292
+ record_notice(launch, 'asked', args.harness, payload.get('session_id'))
1293
+ _mind_asked(launch, args.harness, payload) # the root the hook runs in (§3.3), never one the prompt names
739
1294
  try:
740
1295
  native_session = str(payload.get('session_id') or '').strip()
741
1296
  if native_session:
@@ -783,11 +1338,17 @@ def _prompt_hook_in_episode(args, payload, prompt, root):
783
1338
  marker.write_text(datetime.now(UTC).isoformat(), encoding="utf-8")
784
1339
  except OSError:
785
1340
  pass
1341
+ try:
1342
+ hint = _situated_hint(runtime, root, args.harness, hook_session, prompt)
1343
+ except Exception: # an offer only; a failure never costs the prompt its context
1344
+ hint = ""
1345
+ if hint:
1346
+ additional_context += "\n" + hint
786
1347
  except Exception as exc: # Hooks are advisory and must never block a prompt.
787
1348
  additional_context = (
788
1349
  f"Actor context is unavailable ({type(exc).__name__}). "
789
1350
  "Do not guess actor identity. Organizational retrieval has not run.\n"
790
- + INVESTIGATION_GUIDANCE
1351
+ + investigation_guidance(args.harness)
791
1352
  )
792
1353
  if _prompt_binding.get():
793
1354
  additional_context = episode_binding.context_line(_prompt_binding.get()) + '\n' + additional_context
@@ -812,61 +1373,13 @@ def _admin_status(args: argparse.Namespace) -> int:
812
1373
  actor = _actor(runtime, root, args.harness)
813
1374
  result["display_name"] = actor.actor.display_name
814
1375
  result["actor_id"] = actor.actor.actor_id
815
- gate = runtime.admin_gate
816
- result["admin"] = bool(gate is not None and gate.actor_is_admin(actor))
1376
+ result["admin"] = is_admin(runtime.authorize(actor, Permission.READ))
817
1377
  except Exception:
818
1378
  result = {"admin": False, "display_name": None, "actor_id": None}
819
1379
  print(json.dumps(result, sort_keys=True))
820
1380
  return 0
821
1381
 
822
1382
 
823
- def _access_status(args: argparse.Namespace) -> int:
824
- """One authorized snapshot of identity, membership, and capabilities.
825
-
826
- The Runtime authorization backend is the source of truth: every value is
827
- a real authorize() decision, never derived independently by a UI. Any
828
- failure reports status "attention" and nothing else — no partial claims.
829
- """
830
-
831
- root = _root()
832
- try:
833
- runtime = local_runtime(root, push_remote=False)
834
- actor = _actor(runtime, root, args.harness)
835
- gate = runtime.admin_gate
836
-
837
- def allowed(*permissions: Permission) -> bool:
838
- return all(
839
- runtime.authorize(actor, permission).allowed
840
- for permission in permissions
841
- )
842
-
843
- read_decision = runtime.authorize(actor, Permission.READ)
844
- membership = actor.membership
845
- out = {
846
- "status": "ok",
847
- "display_name": actor.actor.display_name,
848
- "actor_id": actor.actor.actor_id,
849
- "org_name": actor.profile.name,
850
- "org_id": actor.profile.org_id,
851
- "membership_status": membership.status if membership else "unknown",
852
- "roles": sorted(membership.roles) if membership else [],
853
- "admin": bool(gate is not None and gate.actor_is_admin(actor)),
854
- "capabilities": {
855
- "read": allowed(Permission.READ),
856
- "create": allowed(Permission.WRITE),
857
- "update": allowed(Permission.WRITE),
858
- "publish": allowed(Permission.SHARE),
859
- "notify": allowed(Permission.SHARE, Permission.EXECUTE),
860
- "org_settings": allowed(Permission.ADMINISTER),
861
- },
862
- "shared_memory_scope": sorted(read_decision.scopes),
863
- }
864
- except Exception:
865
- out = {"status": "attention"}
866
- print(json.dumps(out, sort_keys=True))
867
- return 0
868
-
869
-
870
1383
  def _handoffs(args: argparse.Namespace) -> int:
871
1384
  """Typed lifecycle lookup — exact handoff questions, zero semantic search.
872
1385
 
@@ -878,7 +1391,7 @@ def _handoffs(args: argparse.Namespace) -> int:
878
1391
  from .handoff_lookup import HandoffLookupService
879
1392
 
880
1393
  root = _root()
881
- runtime = local_runtime(root, push_remote=False)
1394
+ runtime = _runtime(root, push_remote=False)
882
1395
  actor = _actor(runtime, root, args.harness)
883
1396
  service = HandoffLookupService(runtime, root)
884
1397
  started = time.monotonic()
@@ -893,57 +1406,62 @@ def _handoffs(args: argparse.Namespace) -> int:
893
1406
  lookup_ms = round((time.monotonic() - started) * 1000)
894
1407
  if args.timing:
895
1408
  print(f"lookup: {lookup_ms}ms", file=sys.stderr)
1409
+ from . import held
1410
+ from .policy import allows
1411
+ decision, memory = runtime.authorize(actor, Permission.READ), service.memory_root
1412
+ records = [record for record in records # the lookup's rows, decided again where they are printed
1413
+ if allows(decision, "memory/" + record.canonical_path.removeprefix("memory/"), root=memory)]
896
1414
  if args.json:
897
- print(
898
- json.dumps(
899
- {
900
- "records": [asdict(record) for record in records],
901
- "lookup_ms": lookup_ms,
902
- },
903
- sort_keys=True,
904
- )
905
- )
906
- return 0
907
- if not records:
908
- print("no matching handoff exists for that filter")
909
- return 0
1415
+ return held.emit(decision, {"records": [asdict(record) for record in records], "lookup_ms": lookup_ms},
1416
+ root=memory, render=lambda shown: json.dumps(shown, sort_keys=True))
1417
+ lines = []
910
1418
  for index, record in enumerate(records, start=1):
911
1419
  day = record.occurred_at.split("T")[0]
912
- print(f"{index}. {day} · from {record.sender} · to {record.recipient} · {record.status} · {record.topic}")
913
- if record.claim:
914
- print(f" claim: {record.claim}")
915
- if record.ask:
916
- print(f" ask: {record.ask}")
917
- print(f" source: {record.canonical_path}")
1420
+ lines.append(f"{index}. {day} · from {record.sender} · to {record.recipient} · {record.status} · {record.topic}")
1421
+ lines += [f" claim: {record.claim}"] * bool(record.claim) + [f" ask: {record.ask}"] * bool(record.ask)
1422
+ lines.append(f" source: {record.canonical_path}")
918
1423
  if args.open and records:
919
- content = runtime.open_source(actor, records[0].canonical_path)
920
- print()
921
- print(content)
922
- return 0
1424
+ lines += ["", runtime.open_source(actor, records[0].canonical_path)]
1425
+ return held.emit(decision, "\n".join(lines) or "no matching handoff exists for that filter", root=memory, render=str)
923
1426
 
924
1427
 
925
1428
  def _status(args: argparse.Namespace) -> int:
926
- health = local_runtime(_root(), push_remote=False).retrieval_health()
927
- if args.json:
928
- print(json.dumps(health.to_dict(), indent=2))
929
- else:
930
- print(f"adapter: {health.adapter} {health.adapter_version}")
931
- print(f"index spec: {health.index_spec_version}")
932
- print(f"index revision: {health.index_revision}")
933
- print(f"source revision: {health.source_revision}")
934
- print(f"embedding: {health.embedding_state}")
935
- print(f"readiness: {health.runtime_state}")
936
- print(f"bm25 ready: {'yes' if health.bm25_ready else 'no'}")
937
- print(f"semantic ready: {'yes' if health.semantic_ready else 'no'}")
938
- print(f"semantic build: {health.semantic_source_revision or 'none'}")
939
- print(f"worker pid: {health.runtime_pid or 'none'}")
940
- print(f"endpoint: {health.runtime_endpoint or 'none'}")
941
- print(f"index: {health.index_path or 'unknown'}")
942
- print(f"collection: {health.collection or 'unknown'}")
943
- print(f"available: {'yes' if health.available else 'no'}")
944
- for warning in health.warnings:
945
- print(f"warning: {warning}")
946
- return 0 if health.available else 1
1429
+ from dataclasses import replace
1430
+ from . import held
1431
+ root = _root()
1432
+ runtime = local_runtime(root, push_remote=False)
1433
+ health = runtime.retrieval_health()
1434
+ try:
1435
+ actor = _actor(runtime, root, "shell")
1436
+ except Exception: # noqa: BLE001 - no identity: health still prints, its warnings (which can name memory) counted
1437
+ actor = None
1438
+ health = replace(health, warnings=(f"{len(health.warnings)} warnings withheld: identity is unavailable",)
1439
+ if health.warnings else ())
1440
+ value = health.to_dict() if args.json else "\n".join((
1441
+ f"adapter: {health.adapter} {health.adapter_version}",
1442
+ f"index spec: {health.index_spec_version}",
1443
+ f"index revision: {health.index_revision}",
1444
+ f"source revision: {health.source_revision}",
1445
+ f"embedding: {health.embedding_state}",
1446
+ f"readiness: {health.runtime_state}",
1447
+ f"bm25 ready: {'yes' if health.bm25_ready else 'no'}",
1448
+ f"semantic ready: {'yes' if health.semantic_ready else 'no'}",
1449
+ f"semantic build: {health.semantic_source_revision or 'none'}",
1450
+ f"worker pid: {health.runtime_pid or 'none'}",
1451
+ f"endpoint: {health.runtime_endpoint or 'none'}",
1452
+ f"index: {health.index_path or 'unknown'}",
1453
+ f"collection: {health.collection or 'unknown'}",
1454
+ f"available: {'yes' if health.available else 'no'}",
1455
+ *(f"warning: {warning}" for warning in health.warnings)))
1456
+ render = (lambda shown: json.dumps(shown, indent=2)) if args.json else str # JSON packaged field by field
1457
+ shown = held.emit_for(runtime, actor, value, render=render) if actor else _content_free(render(value))
1458
+ return shown or (0 if health.available else 1)
1459
+
1460
+
1461
+ def _content_free(text: str) -> int:
1462
+ """Health with every warning replaced by a count: no memory text to package."""
1463
+ print(text)
1464
+ return 0
947
1465
 
948
1466
 
949
1467
  _RUNTIME_CANDIDATE_VERSION = re.compile(
@@ -1052,7 +1570,7 @@ def _upgrade(args: argparse.Namespace) -> int:
1052
1570
  if action == "status":
1053
1571
  state = upgrade_module.describe(store.load())
1054
1572
  state["active_version"] = _active_runtime_version(root, store)
1055
- state["instance_hash"] = upgrade_module.instance_key(root)
1573
+ state["instance_hash"] = installation_key(root)
1056
1574
  # A live apply journal means a previous candidate application died
1057
1575
  # mid-transaction; the next activate recovers deterministically.
1058
1576
  state["interrupted_apply"] = store.load_apply_journal() is not None
@@ -1101,7 +1619,7 @@ def _upgrade(args: argparse.Namespace) -> int:
1101
1619
  state = upgrade_module.new_state(
1102
1620
  instance_path=root,
1103
1621
  org_id=org_id,
1104
- instance_hash=upgrade_module.instance_key(root),
1622
+ instance_hash=installation_key(root),
1105
1623
  candidate_version=version,
1106
1624
  active_version=_active_runtime_version(root, store),
1107
1625
  candidate_source=str(tarball),
@@ -1279,6 +1797,7 @@ def _open(args: argparse.Namespace) -> int:
1279
1797
  private=True,harness=args.harness)
1280
1798
  return 0 if attached else 2 # Claude's attachment carries its operation error.
1281
1799
  print(response['error'],file=sys.stderr)
1800
+ _mind_notice(root, response, '', args.harness)
1282
1801
  return 2
1283
1802
  sources = response['result'].get('sources', [response['result']])
1284
1803
  parts = []
@@ -1384,7 +1903,12 @@ def _session_env(args):
1384
1903
 
1385
1904
  def _parser() -> argparse.ArgumentParser:
1386
1905
  parser = argparse.ArgumentParser(prog="egregore-runtime")
1906
+ if member_cli is not None:
1907
+ member_cli.add_arguments(parser)
1387
1908
  commands = parser.add_subparsers(dest="command", required=True)
1909
+ if member_cli is not None:
1910
+ member_ready = commands.add_parser("member-ready", help=argparse.SUPPRESS)
1911
+ member_ready.set_defaults(handler=_member_ready)
1388
1912
 
1389
1913
  session_env = commands.add_parser('session-env', help=argparse.SUPPRESS)
1390
1914
  session_env.add_argument('--harness', choices=['claude'], required=True)
@@ -1469,12 +1993,6 @@ def _parser() -> argparse.ArgumentParser:
1469
1993
  )
1470
1994
  handoffs.set_defaults(handler=_handoffs)
1471
1995
 
1472
- access_status = commands.add_parser("access-status")
1473
- access_status.add_argument(
1474
- "--harness", default=os.environ.get("EGREGORE_RUNTIME", "shell")
1475
- )
1476
- access_status.set_defaults(handler=_access_status)
1477
-
1478
1996
  readiness = commands.add_parser("readiness")
1479
1997
  readiness.add_argument("--json", action="store_true")
1480
1998
  readiness.add_argument("--plain", action="store_true")
@@ -1518,14 +2036,21 @@ def _parser() -> argparse.ArgumentParser:
1518
2036
  source.add_argument('--request-id',help='stable id for retrying this exact invocation')
1519
2037
  source.add_argument('--private-result',action='store_true',help=argparse.SUPPRESS)
1520
2038
  source.set_defaults(handler=_open)
2039
+ for name in _COMMAND_MODULES:
2040
+ module = optional_module(name)
2041
+ if module is not None:
2042
+ module.register(commands)
1521
2043
  return parser
1522
2044
 
1523
2045
 
1524
- def main() -> int:
1525
- args = _parser().parse_args()
2046
+ def main(argv=None) -> int:
2047
+ args = _parser().parse_args(argv)
1526
2048
  if getattr(args, "limit", None) is not None and args.limit < 1:
1527
2049
  raise ValueError("retrieval limit must be positive")
1528
- return args.handler(args)
2050
+ with (member_cli.invocation(args, _root(), allowed_commands={
2051
+ "prompt-hook", "find", "open", "handoffs", "member-ready",
2052
+ }) if member_cli is not None else nullcontext()):
2053
+ return args.handler(args)
1529
2054
 
1530
2055
 
1531
2056
  if __name__ == "__main__":