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
@@ -0,0 +1,608 @@
1
+ from dataclasses import dataclass, replace
2
+ import json
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+ import re
7
+ import selectors
8
+ import signal
9
+ import stat
10
+ import subprocess
11
+
12
+
13
+ PRINCIPAL_KINDS = ("person", "agent", "egregore")
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class Principal:
18
+ handle: str
19
+ kind: str = ""
20
+ identity: str = ""
21
+ read: tuple[str, ...] = ()
22
+ append: tuple[str, ...] = ()
23
+ file_kb: int = 64
24
+ per_day: int = 50
25
+ problem: str | None = None
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class AuthorizationInputs:
30
+ policy: str
31
+ admins: frozenset[str]
32
+ removed: frozenset[str]
33
+ memory_root: Path
34
+ source: str = "working-tree"
35
+ problem: str | None = None
36
+ org_id: str = ""
37
+ principals: tuple[Principal, ...] = ()
38
+ principals_problem: str | None = None
39
+ released: tuple[str, ...] = ()
40
+ memory_revision: str = ""
41
+ base: str = ""
42
+ settings: str = "" # the egregore.json text these inputs were read from
43
+
44
+
45
+ SHARED_SETTINGS_UNREADABLE = "the shared settings could not be read; nothing was handed over"
46
+ ROOT_MARKER = ".egregore-read-only" # names a projection root (``access materialize``)
47
+ PROJECTION_ROOT = "projection-root"
48
+ _BRANCH = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]{0,199}")
49
+
50
+
51
+ def _identity(kind: str, identity: str) -> str:
52
+ identity = identity.strip().casefold()
53
+ return identity.removeprefix("@") if kind in {"person", "agent"} else identity
54
+
55
+
56
+ def _principal_row(key: str, value: object, org_id: str) -> Principal:
57
+ # policy imports access; defer this import to avoid a module initialization cycle.
58
+ from .policy import _canonical_parts
59
+
60
+ handle, problems, fields = key.strip().casefold(), [], {}
61
+ if not re.fullmatch(r"[a-z0-9][a-z0-9_.-]{0,63}", handle):
62
+ problems.append("handle is invalid")
63
+ if not isinstance(value, dict):
64
+ return Principal(handle, problem="; ".join([*problems, "row must be an object"]))
65
+ if "write" in value:
66
+ problems.append("outside principals cannot hold write; use append")
67
+ for name in sorted(value.keys() - {"kind", "identity", "read", "append", "limits", "write"}):
68
+ problems.append(f"unknown field {name}")
69
+ kind = value.get("kind", "")
70
+ fields["kind"] = kind if isinstance(kind, str) else ""
71
+ if kind not in PRINCIPAL_KINDS:
72
+ problems.append("kind must be person, agent or egregore")
73
+ identity = value.get("identity")
74
+ fields["identity"] = _identity(fields["kind"], identity) if isinstance(identity, str) else ""
75
+ if not re.fullmatch(r"[a-z0-9][a-z0-9_.:-]{0,127}", fields["identity"]):
76
+ problems.append("identity must be a nonempty valid identity")
77
+ elif kind == "egregore" and (fields["identity"].startswith("legacy-org:") or fields["identity"] == org_id.casefold()):
78
+ problems.append("identity must be another organization's stable org id")
79
+ for name in ("read", "append"):
80
+ prefixes = value.get(name, [])
81
+ canonical = []
82
+ if not isinstance(prefixes, list):
83
+ problems.append(f"{name} must be a list of memory prefixes")
84
+ else:
85
+ for prefix in prefixes:
86
+ parts = _canonical_parts(prefix) if isinstance(prefix, str) else None
87
+ if not parts or parts[0] != "memory" or _canonical_parts("/".join(parts)) != parts:
88
+ problems.append(f"{name} contains an invalid memory prefix")
89
+ elif (normalized := "/".join(parts)) not in canonical:
90
+ canonical.append(normalized)
91
+ fields[name] = tuple(canonical)
92
+ limits = value.get("limits", {})
93
+ if not isinstance(limits, dict):
94
+ problems.append("limits must be an object")
95
+ else:
96
+ for name, limit in limits.items():
97
+ if name not in {"file_kb", "per_day"}:
98
+ problems.append(f"unknown limits field {name}")
99
+ elif type(limit) is not int or limit <= 0:
100
+ problems.append(f"limits.{name} must be a positive integer")
101
+ else:
102
+ fields[name] = limit
103
+ return Principal(handle, **fields, problem="; ".join(dict.fromkeys(problems)) or None)
104
+
105
+
106
+ def _principals(config: dict, org_id: str, admins: frozenset[str]) -> tuple[tuple[Principal, ...], str | None]:
107
+ access = config.get("access", {})
108
+ rows = access.get("principals", {}) if isinstance(access, dict) else None
109
+ if not isinstance(rows, dict):
110
+ return (), "the access settings could not be read"
111
+ parsed = tuple(_principal_row(key, value, org_id) for key, value in sorted(rows.items()))
112
+ result = []
113
+ for row in parsed:
114
+ problems = [row.problem] if row.problem else []
115
+ if sum(other.handle == row.handle for other in parsed) > 1:
116
+ problems.append("handle is defined more than once")
117
+ if row.identity and sum((other.kind, other.identity) == (row.kind, row.identity) for other in parsed) > 1:
118
+ problems.append("identity is claimed by more than one principal")
119
+ if row.handle in admins:
120
+ problems.append("handle is also an admin")
121
+ result.append(replace(row, problem="; ".join(problems) or None))
122
+ return tuple(result), None
123
+
124
+
125
+ def principal(inputs: AuthorizationInputs, handle: str) -> Principal | None:
126
+ if inputs.principals_problem:
127
+ return None
128
+ return next((row for row in inputs.principals if row.handle == handle.strip().casefold()), None)
129
+
130
+
131
+ def principal_by_identity(inputs: AuthorizationInputs, kind: str, identity: str) -> Principal | None:
132
+ if inputs.principals_problem:
133
+ return None
134
+ normalized = _identity(kind, identity)
135
+ return next((row for row in inputs.principals if row.kind == kind and row.identity == normalized), None)
136
+
137
+
138
+ def _released(config: dict) -> tuple[str, ...]:
139
+ from .policy import _canonical_parts
140
+
141
+ access = config.get("access", {})
142
+ rows = access.get("release", []) if isinstance(access, dict) else []
143
+ parts = [_canonical_parts(row) for row in (rows if isinstance(rows, list) else []) if isinstance(row, str)]
144
+ # An invalid entry releases nothing: releases only lift holds, so dropping one fails closed.
145
+ return tuple(dict.fromkeys("/".join(part) for part in parts if part and part[0] == "memory"))
146
+
147
+
148
+ def _parse(text: str, memory_root: Path, problem: str, **fields) -> AuthorizationInputs:
149
+ policy, admins, removed, released = "all-access-v1", frozenset(), frozenset(), ()
150
+ org_id, principals, principals_problem = "", (), None
151
+ try:
152
+ config = json.loads(text)
153
+ profile = config.get("profile", {})
154
+ policy = profile.get("default_policy", "all-access-v1")
155
+ if not isinstance(policy, str) or not policy.strip():
156
+ raise ValueError("policy must be a nonempty string")
157
+ removed_rows = config.get("people_removed", [])
158
+ if not isinstance(removed_rows, list) or any(not isinstance(row, str) for row in removed_rows):
159
+ raise ValueError("removed people must be a string list")
160
+ removed = frozenset(row.strip().casefold() for row in removed_rows if row.strip())
161
+ admin_rows = config.get("admins", [])
162
+ if isinstance(admin_rows, list):
163
+ admins = frozenset(row.strip().casefold() for row in admin_rows if isinstance(row, str) and row.strip())
164
+ # Match profile.load_profile's org-id resolution without its unrelated validation.
165
+ org_id = str(config.get("org_id") or "").strip() or f"legacy-org:{str(config.get('slug') or '').strip()}"
166
+ principals, principals_problem = _principals(config, org_id, admins)
167
+ released, problem = _released(config), None
168
+ except (OSError, ValueError, AttributeError):
169
+ policy, admins, released = "all-access-v1", frozenset(), ()
170
+ return AuthorizationInputs(policy, admins, removed, memory_root, problem=problem, org_id=org_id, principals=principals,
171
+ principals_problem=principals_problem, released=released, settings=text, **fields)
172
+
173
+
174
+ def _git(cwd: Path, *args: str, stdin: bytes | None = None) -> bytes:
175
+ environment = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
176
+ return subprocess.run(["git", "-C", str(cwd), *args], input=stdin, capture_output=True, check=True, timeout=60,
177
+ env={**environment, "GIT_OPTIONAL_LOCKS": "0", "LC_ALL": "C"}).stdout
178
+
179
+
180
+ _ABSENT = object()
181
+
182
+
183
+ def _repository(url: str) -> str:
184
+ """A clone URL as compared for identity.
185
+
186
+ On github.com the scheme, the SSH form, case and a trailing .git name one
187
+ repository, so they are ignored. Elsewhere only an HTTP user or token is
188
+ dropped and the scheme and host are case-folded: the path keeps its case and
189
+ suffix, and scp-style and ssh:// spellings stay apart, so two spellings that
190
+ may name different repositories never match. A different spelling of the
191
+ same repository refuses the hand-over, which fails closed.
192
+ """
193
+ url = url.strip().rstrip("/")
194
+ if match := re.match(r"^([A-Za-z][A-Za-z0-9+.-]*://)([^@/]*@)?([^/]*)(.*)$", url):
195
+ scheme, user, host, path = match[1].casefold(), match[2] or "", match[3].casefold(), match[4]
196
+ if host == "github.com":
197
+ return "github.com/" + path.lstrip("/").removesuffix(".git").casefold()
198
+ return f"{scheme}{'' if scheme in ('http://', 'https://') else user}{host}{path}"
199
+ if (match := re.match(r"^[^@/:]+@([^:/]+):(.*)$", url)) and match[1].casefold() == "github.com":
200
+ return "github.com/" + match[2].lstrip("/").removesuffix(".git").casefold()
201
+ return url
202
+
203
+
204
+ def committed_revision(memory_root: Path, memory_repo: object = _ABSENT, *, read_budget=None) -> str:
205
+ """The memory repository's origin/main commit.
206
+
207
+ Memory must be its own repository and, when the committed settings name
208
+ ``memory_repo`` (any value, null included), a clone of that repository: a
209
+ memory link to another organization's clone hands nothing over.
210
+ """
211
+ run = (lambda *args: _bounded_git(memory_root, args, read_budget, limit=8192)) if read_budget is not None else (lambda *args: _git(memory_root, *args))
212
+ top = run("rev-parse", "--show-toplevel").decode().strip()
213
+ if Path(top).resolve() != memory_root.resolve():
214
+ raise ValueError("memory is not its own repository")
215
+ if memory_repo is not _ABSENT:
216
+ origin = (run("config", "--local", "--get", "remote.origin.url") if read_budget is not None
217
+ else run("config", "--get", "remote.origin.url")).decode().strip()
218
+ if not isinstance(memory_repo, str) or not memory_repo.strip() or _repository(origin) != _repository(memory_repo):
219
+ raise ValueError("memory is not the organization's memory repository")
220
+ return run("rev-parse", "--verify", "--end-of-options", "refs/remotes/origin/main^{commit}").decode().strip()
221
+
222
+
223
+ def _declared_base(text: str) -> str:
224
+ base = json.loads(text).get("base_branch") or "develop"
225
+ if not isinstance(base, str) or not _BRANCH.fullmatch(base) or ".." in base:
226
+ raise ValueError("base_branch is invalid")
227
+ return base
228
+
229
+
230
+ def projection_root(root: Path) -> bool:
231
+ """Whether ``root`` is a read-only copy: its marker is this user's private file, never one Git checked out."""
232
+ try:
233
+ info = os.lstat(Path(root) / ROOT_MARKER)
234
+ except OSError:
235
+ return False
236
+ return stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and not info.st_mode & 0o077
237
+
238
+
239
+ def authorization_inputs(root: Path, principal: str | None = None, *, read_budget=None) -> AuthorizationInputs:
240
+ """Read every authorization input from one source.
241
+
242
+ The operator (``principal`` None) reads the working tree. Anyone else reads
243
+ the committed base: ``origin/{base}:egregore.json`` and memory at the memory
244
+ repository's ``origin/main``, so an uncommitted grant, policy switch,
245
+ release, roster or hold-marker edit widens no one else. ``{base}`` comes from
246
+ the working tree but must equal the base the committed file itself declares.
247
+ Either source unreadable refuses every hand-over.
248
+
249
+ A projection root decides from nothing it holds: its settings and identity
250
+ are its reader's to edit, so it reads only the generation its slot admits
251
+ (``policy.policy_for``) and acts for no one else.
252
+ """
253
+ memory_root = (root / "memory").resolve()
254
+ if read_budget is not None:
255
+ from .held import _read_budget
256
+ _read_budget(read_budget)
257
+ if projection_root(root):
258
+ from .projection import REBUILDING, serves
259
+ own = memory_root.parent.parent.parent == Path(root).resolve() # DIR/memory names DIR's generation, no other
260
+ problem = "a read-only copy acts for no one else" if principal is not None else None if own and serves(
261
+ memory_root) else REBUILDING
262
+ return AuthorizationInputs("all-access-v1", frozenset(), frozenset(), memory_root, source=PROJECTION_ROOT,
263
+ problem=problem)
264
+ if principal is None:
265
+ try:
266
+ text = (_bounded_settings(root / "egregore.json", read_budget).decode("utf-8") if read_budget is not None
267
+ else (root / "egregore.json").read_text(encoding="utf-8"))
268
+ except (OSError, ValueError):
269
+ text = ""
270
+ if read_budget is not None:
271
+ read_budget.bind_authority((str(root.resolve()), "operator"), hashlib.sha256(text.encode()).hexdigest())
272
+ return _parse(text, memory_root, "the organization settings could not be read")
273
+ try:
274
+ try:
275
+ local = (_bounded_settings(root / "egregore.json", read_budget).decode("utf-8") if read_budget is not None
276
+ else (root / "egregore.json").read_text(encoding="utf-8"))
277
+ base = _declared_base(local)
278
+ except (OSError, json.JSONDecodeError, AttributeError):
279
+ base = "develop"
280
+ if read_budget is None:
281
+ text = _git(root, "cat-file", "blob", f"refs/remotes/origin/{base}:egregore.json").decode("utf-8")
282
+ else:
283
+ base_oid = _bounded_git(root, ("rev-parse", "--verify", f"refs/remotes/origin/{base}^{{commit}}"), read_budget, limit=128).decode().strip()
284
+ text = _bounded_git(root, ("cat-file", "blob", f"{base_oid}:egregore.json"), read_budget,
285
+ limit=read_budget.limits.settings_bytes).decode("utf-8")
286
+ read_budget.charge("total_settings_bytes", len(text.encode()))
287
+ if _declared_base(text) != base:
288
+ raise ValueError("the committed settings declare another base")
289
+ revision = committed_revision(memory_root, json.loads(text).get("memory_repo", _ABSENT), read_budget=read_budget)
290
+ if read_budget is not None:
291
+ read_budget.bind_authority((str(root.resolve()), "principal", principal),
292
+ (base, base_oid, hashlib.sha256(text.encode()).hexdigest(), str(memory_root), revision))
293
+ inputs = _parse(text, memory_root, SHARED_SETTINGS_UNREADABLE, source="committed", base=base,
294
+ memory_revision=revision)
295
+ if not inputs.problem:
296
+ return inputs
297
+ except (OSError, ValueError, AttributeError, subprocess.SubprocessError):
298
+ pass
299
+ return AuthorizationInputs("all-access-v1", frozenset(), frozenset(), memory_root, source="committed",
300
+ problem=SHARED_SETTINGS_UNREADABLE, principals_problem=SHARED_SETTINGS_UNREADABLE)
301
+
302
+
303
+ def _bounded_settings(path, budget):
304
+ from .held import _read_budget
305
+ _read_budget(budget)
306
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
307
+ with os.fdopen(descriptor, "rb", buffering=0) as stream:
308
+ before = os.fstat(stream.fileno())
309
+ if not stat.S_ISREG(before.st_mode) or not before.st_mode & 0o444 or before.st_size > budget.limits.settings_bytes:
310
+ budget.fail()
311
+ budget.charge("total_settings_bytes", before.st_size)
312
+ data = stream.read(before.st_size + 1)
313
+ after = os.fstat(stream.fileno())
314
+ if len(data) != before.st_size or (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (
315
+ after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns):
316
+ budget.fail()
317
+ budget.remaining()
318
+ return data
319
+
320
+
321
+ def _start_bounded_git(root, args, budget, *, stdin=False):
322
+ from .held import _read_budget
323
+ _read_budget(budget)
324
+ environment = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")}
325
+ environment.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=os.devnull, GIT_TERMINAL_PROMPT="0",
326
+ GIT_OPTIONAL_LOCKS="0", GIT_NO_LAZY_FETCH="1", LC_ALL="C")
327
+ return subprocess.Popen(["git", "--no-replace-objects", "-c", "protocol.allow=never", "-c", "core.fsmonitor=false",
328
+ "-c", "core.hooksPath=" + os.devnull, "-C", str(root), *args],
329
+ stdin=subprocess.PIPE if stdin else subprocess.DEVNULL, stdout=subprocess.PIPE,
330
+ stderr=subprocess.DEVNULL, env=environment, start_new_session=True, bufsize=0)
331
+
332
+
333
+ def _close_bounded_git(process):
334
+ # Fixed local plumbing does not launch filters/hooks/transport helpers. Kill
335
+ # its owned group even when the leader has already exited, then reap it.
336
+ try:
337
+ os.killpg(process.pid, signal.SIGKILL)
338
+ except ProcessLookupError:
339
+ pass
340
+ finally:
341
+ process.wait()
342
+ if process.stdin:
343
+ process.stdin.close()
344
+ process.stdout.close()
345
+
346
+
347
+ def _bounded_git_read(process, count, budget):
348
+ with selectors.DefaultSelector() as selected:
349
+ selected.register(process.stdout, selectors.EVENT_READ)
350
+ if not selected.select(budget.remaining()):
351
+ budget.fail()
352
+ return os.read(process.stdout.fileno(), min(count, 65536))
353
+
354
+
355
+ def _bounded_git(root, args, budget, *, limit):
356
+ process = _start_bounded_git(root, args, budget)
357
+ output = bytearray()
358
+ try:
359
+ while True:
360
+ chunk = _bounded_git_read(process, limit + 1 - len(output), budget)
361
+ if not chunk:
362
+ break
363
+ output.extend(chunk)
364
+ if len(output) > limit:
365
+ budget.fail()
366
+ if process.wait(timeout=budget.remaining()) != 0:
367
+ budget.fail()
368
+ budget.remaining()
369
+ return bytes(output)
370
+ except (OSError, ValueError, subprocess.SubprocessError):
371
+ budget.fail()
372
+ finally:
373
+ _close_bounded_git(process)
374
+
375
+
376
+ class _BoundedCommittedMemory:
377
+ """Read-only fixed source owner; complete inventory and size-first blobs."""
378
+ def __init__(self, root, revision, budget):
379
+ from .held import _file_identity, _read_budget
380
+ self.root, self.revision, self.budget = Path(root), revision, _read_budget(budget)
381
+ self.entries, self.folders, self._blobs, self._process = {}, set(), {}, None
382
+ if (not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", revision) or not self.root.is_absolute()
383
+ or self.root != self.root.resolve(strict=True)):
384
+ budget.fail()
385
+ self.identity = _file_identity(self.root.stat())
386
+ git_dir = self.root / ".git"
387
+ if git_dir.is_symlink() or not git_dir.is_dir():
388
+ budget.fail() # Canonical memory owns its repository, not another worktree's object store.
389
+ self._git_identity = _file_identity(git_dir.stat())[:4]
390
+ config = _bounded_settings(git_dir / "config", budget)
391
+ if re.search(rb"(?im)^\s*\[\s*include(?:if)?\b|\b(?:partialclone|promisor)\s*=", config):
392
+ budget.fail()
393
+ for name in ("commondir", "shallow", "info/grafts", "objects/info/alternates", "objects/info/http-alternates"):
394
+ selected = git_dir / name
395
+ for parent in selected.parents:
396
+ if parent == git_dir:
397
+ break
398
+ if parent.is_symlink():
399
+ budget.fail()
400
+ if selected.exists() or selected.is_symlink():
401
+ budget.fail()
402
+ if (git_dir / "objects").is_symlink():
403
+ budget.fail()
404
+ # Local Git may consult packs, loose objects and refs without exposing
405
+ # their filesystem paths. Refuse indirect stores before any Git read.
406
+ def metadata(directory, depth=0):
407
+ budget.remaining()
408
+ with os.scandir(directory) as found:
409
+ for entry in found:
410
+ budget.charge("entries")
411
+ budget.charge("inventory_bytes", len(os.fsencode(entry.name)) + 1)
412
+ info = entry.stat(follow_symlinks=False)
413
+ if stat.S_ISLNK(info.st_mode):
414
+ budget.fail()
415
+ if stat.S_ISDIR(info.st_mode):
416
+ if depth >= 8:
417
+ budget.fail()
418
+ child = os.open(entry.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory)
419
+ try:
420
+ if _file_identity(os.fstat(child)) != _file_identity(info):
421
+ budget.fail()
422
+ metadata(child, depth + 1)
423
+ finally:
424
+ os.close(child)
425
+ elif not stat.S_ISREG(info.st_mode):
426
+ budget.fail()
427
+ for name in ("objects", "refs"):
428
+ if (git_dir / name).exists():
429
+ descriptor = os.open(git_dir / name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
430
+ try:
431
+ metadata(descriptor)
432
+ finally:
433
+ os.close(descriptor)
434
+ for name in ("HEAD", "packed-refs"):
435
+ path = git_dir / name
436
+ if path.is_symlink() or path.exists() and not stat.S_ISREG(path.lstat().st_mode):
437
+ budget.fail()
438
+ output = _bounded_git(root, ("ls-tree", "-r", "-t", "-z", "--full-tree", "--end-of-options", revision), budget,
439
+ limit=budget.limits.inventory_bytes - budget.counts.get("inventory_bytes", 0))
440
+ budget.charge("inventory_bytes", len(output))
441
+ for row in output.split(b"\0"):
442
+ if not row:
443
+ continue
444
+ budget.charge("entries")
445
+ meta, sep, path = row.partition(b"\t")
446
+ if not sep or len(path) > budget.limits.path_bytes:
447
+ budget.fail()
448
+ mode, kind, oid = meta.decode("ascii").split(" ")
449
+ name = path.decode("utf-8", "surrogateescape")
450
+ if (not name or any(part in {"", ".", ".."} for part in name.split("/"))
451
+ or not re.fullmatch(r"[a-f0-9]{40}|[a-f0-9]{64}", oid) or len(oid) != len(revision)
452
+ or name in self.entries or name in self.folders):
453
+ budget.fail()
454
+ if kind == "tree" and mode == "040000":
455
+ self.folders.add(name)
456
+ elif kind in {"blob", "commit"}:
457
+ self.entries[name] = (mode, oid)
458
+ else:
459
+ budget.fail()
460
+ self.fingerprint = hashlib.sha256(output).hexdigest()
461
+
462
+ def _exact(self, count):
463
+ value = bytearray()
464
+ while len(value) < count:
465
+ chunk = _bounded_git_read(self._process, count - len(value), self.budget)
466
+ if not chunk:
467
+ self.budget.fail()
468
+ value.extend(chunk)
469
+ return bytes(value)
470
+
471
+ def read(self, relative):
472
+ budget = self.budget
473
+ budget.remaining()
474
+ mode, oid = self.entries[relative]
475
+ if mode not in {"100644", "100755", "120000"}:
476
+ budget.fail()
477
+ if oid in self._blobs:
478
+ return self._blobs[oid]
479
+ if self._process is None:
480
+ self._process = _start_bounded_git(self.root, ("cat-file", "--batch"), budget, stdin=True)
481
+ self._process.stdin.write(oid.encode() + b"\n")
482
+ header = bytearray()
483
+ while not header.endswith(b"\n"):
484
+ if len(header) >= 128:
485
+ budget.fail()
486
+ header.extend(self._exact(1))
487
+ values = header.split()
488
+ if len(values) != 3 or values[0] != oid.encode() or values[1] != b"blob" or not values[2].isdigit():
489
+ budget.fail()
490
+ size = int(values[2])
491
+ limit = min(budget.limits.dependency_bytes, budget.limits.path_bytes) if mode == "120000" else budget.limits.dependency_bytes
492
+ if size > limit:
493
+ budget.fail()
494
+ budget.charge("total_bytes", size)
495
+ data = self._exact(size)
496
+ if self._exact(1) != b"\n":
497
+ budget.fail()
498
+ algorithm = hashlib.sha1 if len(oid) == 40 else hashlib.sha256
499
+ if algorithm(b"blob " + str(size).encode() + b"\0" + data).hexdigest() != oid:
500
+ budget.fail()
501
+ self._blobs[oid] = data
502
+ return data
503
+
504
+ def hold_documents(self):
505
+ from .held import committed_target
506
+ documents, identities = {}, {}
507
+ try:
508
+ for name, (mode, _oid) in sorted(self.entries.items()):
509
+ self.budget.remaining()
510
+ if mode not in {"100644", "100755", "120000"} or not name.lower().endswith(".md") or any(p.startswith(".") for p in name.split("/")):
511
+ continue
512
+ target = name if mode != "120000" else committed_target(self, name)
513
+ if target is None:
514
+ continue
515
+ if len(documents) >= self.budget.limits.documents:
516
+ self.budget.fail()
517
+ documents[name] = (target, self.read(target))
518
+ identities[name] = None
519
+ self.budget.remaining()
520
+ return self.identity, self.fingerprint, documents, identities
521
+ finally:
522
+ if self._process is not None:
523
+ _close_bounded_git(self._process)
524
+ self._process = None
525
+
526
+ def recheck(self):
527
+ from .held import _file_identity
528
+ self.budget.remaining()
529
+ if self.root != self.root.resolve(strict=True) or _file_identity(self.root.stat()) != self.identity:
530
+ self.budget.fail()
531
+ directory = self.root / ".git"
532
+ if directory.is_symlink() or _file_identity(directory.stat())[:4] != self._git_identity:
533
+ self.budget.fail()
534
+ if _bounded_git(self.root, ("rev-parse", "--verify", "--end-of-options", self.revision + "^{commit}"), self.budget, limit=128).decode().strip() != self.revision:
535
+ self.budget.fail()
536
+ inventory = _bounded_git(self.root, ("ls-tree", "-r", "-t", "-z", "--full-tree", "--end-of-options", self.revision),
537
+ self.budget, limit=self.budget.limits.inventory_bytes - self.budget.counts.get("inventory_bytes", 0))
538
+ self.budget.charge("inventory_bytes", len(inventory))
539
+ if hashlib.sha256(inventory).hexdigest() != self.fingerprint:
540
+ self.budget.fail()
541
+
542
+
543
+ def bounded_committed_memory(memory_root, revision, *, budget):
544
+ from .held import _read_budget
545
+ _read_budget(budget)
546
+ try:
547
+ return _BoundedCommittedMemory(memory_root, revision, budget)
548
+ except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError):
549
+ budget.fail()
550
+
551
+
552
+ class CommittedMemory:
553
+ """Memory exactly as committed at one revision (``git ls-tree`` and ``git cat-file --batch``)."""
554
+
555
+ def __init__(self, root: Path, revision: str):
556
+ self.root, self.revision, self.entries, self.folders, self._blobs = root, revision, {}, set(), {}
557
+ for row in _git(root, "ls-tree", "-r", "-t", "-z", "--full-tree", "--end-of-options", revision).split(b"\0"):
558
+ if row:
559
+ meta, _, path = row.partition(b"\t")
560
+ mode, kind, oid = meta.decode().split(" ")
561
+ name = path.decode("utf-8", "surrogateescape")
562
+ if kind == "tree":
563
+ self.folders.add(name)
564
+ else:
565
+ self.entries[name] = (mode, oid)
566
+
567
+ def kind(self, relative: str) -> str | None:
568
+ """file, folder, link or other for the path, or for the first committed non-folder above it; None if absent."""
569
+ parts = relative.split("/")
570
+ for depth in range(1, len(parts) + 1):
571
+ if entry := self.entries.get("/".join(parts[:depth])):
572
+ if entry[0] == "120000":
573
+ return "link"
574
+ return "file" if entry[0] in {"100644", "100755"} and depth == len(parts) else "other"
575
+ return "folder" if relative in self.folders else None
576
+
577
+ def prefetch(self, relatives) -> None:
578
+ """Read many blobs through one ``git cat-file --batch``."""
579
+ oids = [oid for oid in dict.fromkeys(self.entries[r][1] for r in relatives if r in self.entries) if oid not in self._blobs]
580
+ if not oids:
581
+ return
582
+ output, offset = _git(self.root, "cat-file", "--batch", stdin=("\n".join(oids) + "\n").encode()), 0
583
+ for oid in oids:
584
+ end = output.index(b"\n", offset)
585
+ header = output[offset:end].split(b" ")
586
+ if len(header) != 3 or header[0].decode() != oid or header[1] != b"blob":
587
+ raise ValueError("committed memory could not be read")
588
+ size = int(header[2])
589
+ self._blobs[oid], offset = output[end + 1:end + 1 + size], end + 2 + size
590
+
591
+ def read(self, relative: str) -> bytes:
592
+ self.prefetch([relative])
593
+ return self._blobs[self.entries[relative][1]]
594
+
595
+
596
+ _TREES: dict[tuple[str, str], CommittedMemory] = {}
597
+
598
+
599
+ def committed_memory(memory_root: Path, revision: str) -> CommittedMemory:
600
+ """One cached reader per memory repository and revision (the last four)."""
601
+ if not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", revision):
602
+ raise ValueError("a committed revision is required")
603
+ key = (str(memory_root.resolve()), revision)
604
+ if key not in _TREES:
605
+ while len(_TREES) >= 4:
606
+ _TREES.pop(next(iter(_TREES)))
607
+ _TREES[key] = CommittedMemory(memory_root, revision)
608
+ return _TREES[key]