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
@@ -2,9 +2,20 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from dataclasses import dataclass, field
5
+ import contextlib
6
+ from dataclasses import dataclass, field, replace
7
+ import hashlib
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import re
12
+ import stat
13
+ import subprocess
14
+ import time
6
15
  from typing import Mapping, Sequence
7
16
 
17
+ from .access import PROJECTION_ROOT, AuthorizationInputs, authorization_inputs, bounded_committed_memory, committed_memory, principal
18
+ from . import held, shadow
8
19
  from .contracts import (
9
20
  ActionAuthorizer,
10
21
  ActorContext,
@@ -14,25 +25,517 @@ from .contracts import (
14
25
  )
15
26
 
16
27
 
28
+ ACTIONS = ("read", "write", "append")
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class Verdict:
33
+ allowed: bool
34
+ reason: str
35
+ canonical: str | None = None
36
+ resolved: Path | None = None
37
+ note: str = ""
38
+
39
+
17
40
  def _canonical_parts(path: str) -> tuple[str, ...] | None:
18
- normalized = path.replace("\\", "/").strip().strip("/")
41
+ normalized = path.strip()
42
+ if normalized.startswith("/") or re.match(r"^[A-Za-z]:", normalized) or "\x00" in normalized or "\\" in normalized:
43
+ return None
44
+ normalized = normalized.strip("/")
19
45
  parts = tuple(normalized.split("/")) if normalized else ()
20
46
  if not parts or any(part in {"", ".", ".."} for part in parts):
21
47
  return None
22
48
  return parts
23
49
 
24
50
 
25
- def scope_allows_path(path: str, scopes: Sequence[str]) -> bool:
26
- """Return whether a normalized canonical path is inside an ACL scope."""
51
+ def _resolve(path: Path) -> Path:
52
+ try:
53
+ return path.resolve(strict=True)
54
+ except FileNotFoundError:
55
+ return path.resolve(strict=False)
56
+
27
57
 
58
+ def within(path: str, prefixes: Sequence[str], *, root: Path | None = None) -> bool:
59
+ """Containment only: both requested and resolved spellings must fit."""
28
60
  canonical = _canonical_parts(path)
29
61
  if canonical is None:
30
62
  return False
31
- for raw_scope in scopes:
32
- scope = _canonical_parts(raw_scope)
33
- if scope is not None and canonical[: len(scope)] == scope:
34
- return True
35
- return False
63
+ if not any(canonical[:len(prefix)] == prefix for raw in prefixes if (prefix := _canonical_parts(raw))):
64
+ return False
65
+ if root is None:
66
+ return True
67
+ try:
68
+ if canonical[0] != "memory":
69
+ return False
70
+ resolved = _resolve(root / Path(*canonical[1:]))
71
+ relative = resolved.relative_to(_resolve(root))
72
+ physical = ("memory", *relative.parts)
73
+ return not any("\\" in part or "\x00" in part for part in physical) and any(
74
+ physical[:len(prefix)] == prefix for raw in prefixes if (prefix := _canonical_parts(raw)))
75
+ except (OSError, RuntimeError, ValueError):
76
+ return False
77
+
78
+
79
+ def is_admin(decision: PolicyDecision) -> bool:
80
+ return decision.allowed and decision.audience == "admin"
81
+
82
+
83
+ _POLICIES = frozenset({"all-access-v1", "roster-v1-shadow", "roster-v1"})
84
+
85
+
86
+ def _trusted(decision: PolicyDecision) -> bool:
87
+ return decision.audience != "outside" and decision.policy_epoch in _POLICIES
88
+
89
+
90
+ class _ReadProjection:
91
+ __slots__ = ("decision", "root", "paths", "budget", "held", "rechecked", "binding")
92
+
93
+ def __init__(self, decision, root, paths, budget):
94
+ self.decision, self.root, self.paths, self.budget = decision, root, paths, budget
95
+ source = bounded_committed_memory(root, decision.memory_revision, budget=budget) if decision.memory_revision else None
96
+ self.held = held.bounded_tree(root, decision.released, budget=budget, committed=source)
97
+ self.rechecked, self.binding = False, None
98
+
99
+ def validate(self, decision, root):
100
+ self.budget.remaining()
101
+ if (decision != self.decision or Path(root) != self.root or self.root.resolve(strict=True) != self.root
102
+ or held._file_identity(self.root.stat()) != self.held.identity or self.held.rules != held.HELD_RULES_VERSION):
103
+ self.budget.fail()
104
+
105
+
106
+ def prepare_read_projection(decision, *, root, paths, budget):
107
+ """Prepare complete bounded hold inputs; this object is not a reusable grant."""
108
+ held._read_budget(budget)
109
+ try:
110
+ root = Path(root)
111
+ if (type(decision) is not PolicyDecision or not decision.allowed or decision.permission != Permission.READ
112
+ or decision.scope not in {"read", "hand-over"} or decision.policy_epoch not in _POLICIES
113
+ or decision.audience not in {"member", "admin", "outside"}
114
+ or decision.audience == "outside" and (decision.scope != "hand-over" or not decision.memory_revision)
115
+ or not root.is_absolute() or root != root.resolve(strict=True)
116
+ or not isinstance(paths, (tuple, list)) or len(paths) > 64):
117
+ budget.fail()
118
+ selected = set()
119
+ for path in paths:
120
+ parts = _canonical_parts(path) if isinstance(path, str) else None
121
+ if (not parts or parts[0] != "memory" or path != "/".join(parts)
122
+ or len(os.fsencode(path)) > budget.limits.path_bytes):
123
+ budget.fail()
124
+ selected.add(path)
125
+ return _ReadProjection(decision, root, tuple(sorted(selected)), budget)
126
+ except (OSError, ValueError, RuntimeError):
127
+ budget.fail()
128
+
129
+
130
+ def _projection(projection, decision, root):
131
+ if type(projection) is not _ReadProjection:
132
+ raise held.BoundedReadUnavailable("bounded read unavailable")
133
+ projection.validate(decision, root)
134
+ return projection
135
+
136
+
137
+ def _projection_path(projection, decision, root, path):
138
+ projection = _projection(projection, decision, root)
139
+ if path not in projection.paths or not within(path, decision.read_prefixes):
140
+ return None
141
+ relative = path.removeprefix("memory/")
142
+ entry = projection.held.documents.get(relative)
143
+ if entry is None:
144
+ return None
145
+ target, raw = entry
146
+ if not within("memory/" + target, decision.read_prefixes):
147
+ return None
148
+ if projection.held.source is not None:
149
+ if projection.held.source.entries.get(relative, (None,))[0] not in {"100644", "100755"}:
150
+ return None
151
+ else:
152
+ original, physical = projection.held.identities[relative]
153
+ if not stat.S_ISREG(original[2]) or original[3] != 1 or physical[3] != 1:
154
+ return None
155
+ return relative, target, raw
156
+
157
+
158
+ def read_projection_header(decision, path, *, root, projection, max_bytes=16384):
159
+ try:
160
+ projection = _projection(projection, decision, root)
161
+ if type(max_bytes) is not int or not 1 <= max_bytes <= 16384:
162
+ projection.budget.fail()
163
+ entry = _projection_path(projection, decision, root, path)
164
+ if entry is None:
165
+ return None
166
+ raw = entry[2]
167
+ if not raw.startswith(b"---\n"):
168
+ return None
169
+ end = raw.find(b"\n---\n", 3, max_bytes)
170
+ if end < 0:
171
+ end = raw.find(b"\n---\r\n", 3, max_bytes)
172
+ size = end + 6
173
+ else:
174
+ size = end + 5
175
+ if end < 0 or size > max_bytes:
176
+ return None
177
+ projection.budget.charge("selected_bytes", size)
178
+ return raw[:size]
179
+ except (OSError, ValueError, RuntimeError):
180
+ return None
181
+
182
+
183
+ def read_projection_bytes(decision, path, *, root, projection):
184
+ try:
185
+ projection = _projection(projection, decision, root)
186
+ entry = _projection_path(projection, decision, root, path)
187
+ if entry is None:
188
+ return None
189
+ raw = entry[2]
190
+ if len(raw) > 256 * 1024:
191
+ return None
192
+ verdict = check(decision, path, root=root, observe_shadow=False, max_source_bytes=256 * 1024,
193
+ read_projection=projection)
194
+ if not verdict.allowed or bytes_held(decision, path, raw, root=root, read_projection=projection):
195
+ return None
196
+ projection.budget.charge("selected_bytes", len(raw))
197
+ return raw
198
+ except (OSError, ValueError, RuntimeError):
199
+ return None
200
+
201
+
202
+ def recheck_read_projection(decision, *, root, projection):
203
+ try:
204
+ projection = _projection(projection, decision, root)
205
+ projection.budget.recheck()
206
+ projection.held.recheck()
207
+ projection.validate(decision, root)
208
+ preimage = {"schema": "egregore.policy-read-projection/v1", "decision": decision.to_dict(),
209
+ "root": str(projection.root), "root_identity": projection.held.identity,
210
+ "source_revision": decision.memory_revision, "inventory": projection.held.fingerprint,
211
+ "rules": projection.held.rules, "releases": list(decision.released),
212
+ "selection": [{"path": path, "target": entry[0], "digest": hashlib.sha256(entry[1]).hexdigest()}
213
+ if (entry := projection.held.documents.get(path.removeprefix("memory/"))) is not None
214
+ else {"path": path, "target": None, "digest": None} for path in projection.paths],
215
+ "authority": sorted((list(key), value) for key, value in projection.budget.authority.items())}
216
+ projection.binding = hashlib.sha256(json.dumps(preimage, sort_keys=True, ensure_ascii=True, separators=(",", ":")).encode()).hexdigest()
217
+ projection.rechecked = True
218
+ projection.budget.remaining()
219
+ return True
220
+ except (OSError, ValueError, RuntimeError):
221
+ if type(projection) is _ReadProjection:
222
+ projection.budget.invalid = True
223
+ return False
224
+
225
+
226
+ def read_projection_binding(decision, *, root, projection):
227
+ try:
228
+ projection = _projection(projection, decision, root)
229
+ return projection.binding if projection.rechecked else None
230
+ except (OSError, ValueError, RuntimeError):
231
+ return None
232
+
233
+
234
+ def tree_for(decision: PolicyDecision, root: Path) -> held.Tree:
235
+ """The memory a decision judges every class against: as committed at its revision, or as walked,
236
+ with the hint only for the operator's own configured decision."""
237
+ if decision.memory_revision:
238
+ return held.committed_tree(committed_memory(root, decision.memory_revision), decision.released)
239
+ return held.tree(root, decision.released, trust_cache=_trusted(decision))
240
+
241
+
242
+ def _committed(decision: PolicyDecision, relative: str, action: str, root: Path, every: bool) -> tuple[int, ...] | str:
243
+ """Held classes of the committed file, or the reason committed memory refuses it."""
244
+ try:
245
+ tree = committed_memory(root, decision.memory_revision)
246
+ kind = tree.kind(relative)
247
+ if action == "append":
248
+ return "exists" if kind else ()
249
+ if kind != "file":
250
+ return "link" if kind == "link" else "not-committed"
251
+ data = tree.read(relative)
252
+ return tree_for(decision, root).classes(relative, data) if every else held.blob_classes(data)
253
+ except (OSError, ValueError, subprocess.SubprocessError):
254
+ return "uncommitted-source"
255
+
256
+
257
+ def _counted_later(decision: PolicyDecision) -> bool:
258
+ """A roster-v1-shadow read: only class 1 holds it, so the class roster-v1 would hold is counted, not awaited."""
259
+ return decision.scope == "read" and decision.policy_epoch == "roster-v1-shadow"
260
+
261
+
262
+ def check(decision: PolicyDecision, path: str, action: str = "read", *, root: Path, explain: bool = False,
263
+ observe_shadow: bool = True, max_source_bytes: int | None = None, read_projection=None) -> Verdict:
264
+ """The authority predicate; callers open or write only the returned path.
265
+
266
+ A decision bound to a ``memory_revision`` is also decided on memory as
267
+ committed there; an outside decision without one is refused, so nothing
268
+ handed to another principal reads working-tree bytes or hold markers.
269
+
270
+ Under roster-v1-shadow a read holds class 1 alone, read from the bytes;
271
+ the class roster-v1 would hold is counted by ``shadow.defer``, which
272
+ never builds a tree on the read path. ``explain`` judges it now, for a
273
+ verdict's ``note``: ``access check`` names it. A bounded read-only caller may
274
+ set ``observe_shadow=False`` to suppress this optional shadow accounting,
275
+ including its deferred tree walk and writes, without changing the verdict.
276
+ This does not make roster-v1 or an explained read a bounded tree-free read.
277
+ ``max_source_bytes`` optionally bounds each working-file authority read
278
+ (plus one byte to detect overflow). It does not bound a rule tree, and
279
+ committed-memory reads refuse this option until their owner supports it.
280
+ """
281
+ projected = None
282
+ if read_projection is not None:
283
+ try:
284
+ _projection(read_projection, decision, root)
285
+ if action != "read":
286
+ return Verdict(False, "invalid-action")
287
+ projected = _projection_path(read_projection, decision, root, path)
288
+ if projected is None:
289
+ return Verdict(False, "bounded-read-unavailable")
290
+ except (OSError, ValueError, RuntimeError):
291
+ return Verdict(False, "bounded-read-unavailable")
292
+ if action not in ACTIONS:
293
+ return Verdict(False, "invalid-action")
294
+ if decision.scope not in {"read", "hand-over"}:
295
+ return Verdict(False, "invalid-scope")
296
+ if not decision.allowed:
297
+ return Verdict(False, "denied")
298
+ if max_source_bytes is not None:
299
+ if type(max_source_bytes) is not int or not 1 <= max_source_bytes <= 8 * 1024 * 1024:
300
+ return Verdict(False, "invalid-source-limit")
301
+ if decision.memory_revision and read_projection is None:
302
+ return Verdict(False, "bounded-committed-read-unavailable")
303
+ if decision.audience == "root": # a read-only copy: only the generation its slot admits now, never a stale one
304
+ from .projection import serves
305
+ if not serves(root):
306
+ return Verdict(False, "being-rebuilt")
307
+ prefixes = getattr(decision, action + "_prefixes") + (decision.write_prefixes if action == "append" else ())
308
+ if not prefixes:
309
+ return Verdict(False, "outside-scope")
310
+ canonical = _canonical_parts(path)
311
+ if canonical is None:
312
+ return Verdict(False, "invalid-path")
313
+ parts, canonical = canonical, "/".join(canonical)
314
+ if parts[0] != "memory":
315
+ return Verdict(False, "outside-scope", canonical)
316
+ if decision.audience == "outside" and not decision.memory_revision:
317
+ return Verdict(False, "uncommitted-source", canonical)
318
+ try:
319
+ requested = root / Path(*parts[1:])
320
+ resolved, base = ((root / projected[1], root) if projected is not None else (_resolve(requested), _resolve(root)))
321
+ if not resolved.is_relative_to(base):
322
+ return Verdict(False, "outside-root", canonical)
323
+ if not within(canonical, prefixes, root=None if projected is not None else root):
324
+ return Verdict(False, "outside-scope", canonical)
325
+ if action == "append" and (os.path.lexists(resolved) or os.path.lexists(requested)):
326
+ return Verdict(False, "exists", canonical)
327
+ every = not (decision.scope == "read" and decision.policy_epoch == "all-access-v1") # else class 1 alone matters
328
+ relative = "/".join(parts[1:])
329
+ found = (read_projection.held.classes(relative, projected[2], projected[1]) if projected is not None
330
+ else _committed(decision, relative, action, root, every) if decision.memory_revision else None)
331
+ if projected is not None and max_source_bytes is not None and len(projected[2]) > max_source_bytes:
332
+ return Verdict(False, "source-limit", canonical)
333
+ if isinstance(found, str):
334
+ return Verdict(False, found, canonical)
335
+ if is_admin(decision):
336
+ return Verdict(True, "allowed", canonical, resolved)
337
+ released = (within(canonical, decision.released) and within("memory/" + projected[1], decision.released)
338
+ if projected is not None else within(canonical, decision.released, root=None if decision.memory_revision else root))
339
+ later = None
340
+ if found is None and not every and max_source_bytes is None:
341
+ found = held.classes(resolved)
342
+ elif found is None:
343
+ if not resolved.is_file():
344
+ data = None
345
+ elif max_source_bytes is None:
346
+ data = resolved.read_bytes()
347
+ else:
348
+ with resolved.open("rb") as source:
349
+ data = source.read(max_source_bytes + 1)
350
+ if len(data) > max_source_bytes:
351
+ return Verdict(False, "source-limit", canonical)
352
+ physical = resolved.relative_to(base).as_posix()
353
+ if data is None:
354
+ found = ()
355
+ elif not every:
356
+ found = held.blob_classes(data)
357
+ elif _counted_later(decision) and not explain:
358
+ found = held.admin_class(data)
359
+ later = None if found or released else (relative, physical, data)
360
+ else:
361
+ found = tree_for(decision, root).classes(relative, data, physical)
362
+ hold, would = held.judge(decision.scope, decision.policy_epoch, found, released)
363
+ if hold:
364
+ return Verdict(False, f"held:{hold}", canonical)
365
+ if observe_shadow and would and read_projection is None:
366
+ shadow.note(root, would)
367
+ if observe_shadow and later and read_projection is None:
368
+ shadow.defer(root, decision.released, *later)
369
+ except (OSError, RuntimeError, ValueError):
370
+ return Verdict(False, "invalid-path", canonical)
371
+ note = f"shadow:{would}" if would else "released" if released and any(number != 1 for number in found) else ""
372
+ return Verdict(True, "allowed", canonical, resolved, note)
373
+
374
+
375
+ _REFUSALS = {
376
+ "outside-root": "it resolves outside memory", "invalid-path": "not a memory path",
377
+ "invalid-action": "not an action", "invalid-scope": "not a decision scope",
378
+ "exists": "it already exists; append adds new files only", "not-committed": "it is not in committed memory",
379
+ "link": "it is a link in committed memory", "uncommitted-source": "decided without the committed settings",
380
+ "being-rebuilt": "this read-only copy is being rebuilt",
381
+ }
382
+
383
+
384
+ def rule(decision: PolicyDecision, verdict: Verdict, action: str) -> str:
385
+ """Name the rule that decided a verdict, in plain words."""
386
+ if verdict.allowed:
387
+ granted = getattr(decision, action + "_prefixes") + (decision.write_prefixes if action == "append" else ())
388
+ prefix = next((p for p in granted if within(verdict.canonical or "", (p,))), "")
389
+ named = f"{action} grant {prefix}" + (" · admin" if is_admin(decision) else "")
390
+ if verdict.note.startswith("shadow:"):
391
+ number = int(verdict.note.removeprefix("shadow:"))
392
+ return named + f" · roster-v1 would hold it ({held.CLASS_NAMES.get(number, f'class {number}')})"
393
+ return named + (" · released by access.release" if verdict.note == "released" else "")
394
+ if verdict.reason.startswith("held:"):
395
+ number = int(verdict.reason.removeprefix("held:"))
396
+ return f"held: {held.CLASS_NAMES.get(number, f'class {number}')}"
397
+ if verdict.reason == "denied":
398
+ return "no grant: " + "; ".join(decision.reasons)
399
+ if verdict.reason == "outside-scope":
400
+ return f"outside the {action} grant"
401
+ return _REFUSALS.get(verdict.reason, verdict.reason)
402
+
403
+
404
+ def allows(decision: PolicyDecision, path: str, action: str = "read", *, root: Path) -> bool:
405
+ return check(decision, path, action, root=root).allowed
406
+
407
+
408
+ def bytes_held(decision: PolicyDecision, path: str, data: bytes, *, root: Path, read_projection=None) -> int | None:
409
+ """The class that holds ``data``, bytes a caller read at memory ``path``, as ``check`` would hold
410
+ the file if it read them: every class the decision applies, less the rule classes ``access.release``
411
+ lifts. None for an admin. ``check`` decides the path and this the bytes read from it, so a file
412
+ rewritten between the read and the check never carries bytes that were held. Fails closed."""
413
+ projected = None
414
+ if read_projection is not None:
415
+ try:
416
+ projected = _projection_path(read_projection, decision, root, path)
417
+ if projected is None or type(data) is not bytes or data != projected[2]:
418
+ read_projection.budget.fail()
419
+ except (OSError, ValueError, RuntimeError, AttributeError):
420
+ return 1
421
+ if is_admin(decision):
422
+ return None
423
+ parts = _canonical_parts(path)
424
+ try:
425
+ if parts is None or parts[0] != "memory":
426
+ raise ValueError("not a memory path")
427
+ relative = "/".join(parts[1:])
428
+ every = not (decision.scope == "read" and decision.policy_epoch == "all-access-v1")
429
+ if projected is not None:
430
+ found = read_projection.held.classes(relative, data, projected[1])
431
+ elif not every:
432
+ found = held.blob_classes(data)
433
+ elif _counted_later(decision): # only class 1 holds it; check counts what roster-v1 would hold
434
+ found = held.admin_class(data)
435
+ elif decision.memory_revision:
436
+ found = tree_for(decision, root).classes(relative, data)
437
+ else:
438
+ physical = _resolve(root / relative).relative_to(_resolve(root)).as_posix()
439
+ found = tree_for(decision, root).classes(relative, data, physical)
440
+ released = (within("/".join(parts), decision.released) and within("memory/" + projected[1], decision.released)
441
+ if projected is not None else within("/".join(parts), decision.released, root=None if decision.memory_revision else root))
442
+ return held.judge(decision.scope, decision.policy_epoch, found, released)[0]
443
+ except (OSError, RuntimeError, ValueError, subprocess.SubprocessError):
444
+ return 1
445
+
446
+
447
+ def strips(decision: PolicyDecision) -> bool:
448
+ """Whether a decision strips held names: a non-admin's, in a scope that applies every class (``held.judge``)."""
449
+ return not is_admin(decision) and not (decision.scope == "read" and decision.policy_epoch in held.LENIENT)
450
+
451
+
452
+ _REDACTORS: dict[tuple, held.Redactor] = {}
453
+ _DOTTED: dict[str, tuple[float, tuple]] = {}
454
+
455
+
456
+ def _dotted(root: Path) -> tuple[tuple, tuple]:
457
+ """What the held walk skips and ``check`` still reads, as listed within ``held.FRESH_SECONDS``: Markdown
458
+ documents under a dot name (``.git`` aside) with the stat of what they read as, and folder links inside
459
+ memory as (link, target)."""
460
+ now, seen = time.monotonic(), _DOTTED.get(str(root))
461
+ if seen is None or now - seen[0] >= held.FRESH_SECONDS:
462
+ found, links, base = [], [], _resolve(root)
463
+ for folder, folders, files in os.walk(root):
464
+ parts = Path(folder).relative_to(root).parts
465
+ for name in [name for name in folders if os.path.islink(os.path.join(folder, name))]:
466
+ with contextlib.suppress(OSError, RuntimeError, ValueError):
467
+ target = _resolve(Path(folder, name))
468
+ links += [("/".join((*parts, name)), target.relative_to(base).as_posix())] if target.is_dir() else []
469
+ folders[:] = [name for name in folders if name != ".git"]
470
+ for name in files:
471
+ if name.lower().endswith(".md") and any(part.startswith(".") for part in (*parts, name)):
472
+ with contextlib.suppress(OSError):
473
+ info = os.stat(os.path.join(folder, name))
474
+ found.append(("/".join((*parts, name)), info.st_mtime_ns, info.st_size, info.st_ino))
475
+ seen = _DOTTED[str(root)] = (now, (tuple(sorted(found)), tuple(sorted(links))))
476
+ return seen[1]
477
+
478
+
479
+ def redactor(decision: PolicyDecision, root: Path) -> held.Redactor | None:
480
+ """The held names a decision strips, from the memory its holds are judged on (``tree_for``); None when it
481
+ strips none. Each held document is read for its title and ids; the shown ones only for their titles,
482
+ once a held title turns up in a text. Shown means readable by this decision: a committed link, or a
483
+ link out of the read grant, shields no name."""
484
+ if not strips(decision):
485
+ return None
486
+ root = Path(root)
487
+ tree = tree_for(decision, root)
488
+ memory = committed_memory(root, decision.memory_revision) if decision.memory_revision else None
489
+ dotted, links = ((tuple((name,) for name in memory.entries if name.lower().endswith(".md")
490
+ and any(part.startswith(".") for part in name.split("/"))), ()) if memory else _dotted(root))
491
+ stamp = tree.walk or decision.memory_revision
492
+ key = (str(root), stamp, dotted, links, decision.released, decision.read_prefixes, _trusted(decision))
493
+ if stamp and key in _REDACTORS:
494
+ return _REDACTORS[key]
495
+ if memory is not None:
496
+ def read(rel: str) -> bytes:
497
+ target = held.committed_target(memory, rel)
498
+ return memory.read(target) if target else b""
499
+ else:
500
+ base = _resolve(root)
501
+
502
+ def read(rel: str) -> bytes:
503
+ path = _resolve(base / rel)
504
+ return path.read_bytes() if path.is_relative_to(base) and path.is_file() else b""
505
+ holds = set(tree.held)
506
+ shown = [rel for rel, target in tree.listed if rel not in holds and (target == rel if memory else
507
+ within("memory/" + target, decision.read_prefixes)) and within("memory/" + rel, decision.read_prefixes)]
508
+ whole = replace(decision, read_prefixes=("memory",)) # held whatever the grant, as the walked tree is
509
+ for rel, *_ in dotted: # judged one by one, as a read of each would be
510
+ holds.update((rel,) if check(whole, "memory/" + rel, "read", root=root).reason.startswith("held:") else ())
511
+ shown += [rel] if check(decision, "memory/" + rel, "read", root=root).allowed else []
512
+ named = {rel: {rel} for rel in holds} # and every spelling through a folder link, a chain of them too
513
+ for _ in range(held.LINK_HOPS if links else 0):
514
+ grown = {rel: names | {link + name[len(target):] for name in names for link, target in links
515
+ if name.startswith(target + "/")} for rel, names in named.items()}
516
+ named, done = grown, grown == named
517
+ if done:
518
+ break
519
+ found = held.Redactor([("memory/" + name, *held.describe("memory/" + rel, read(rel)))
520
+ for rel in sorted(holds) for name in sorted(named[rel])],
521
+ ["memory/" + rel for rel in shown],
522
+ lambda: [held.describe("memory/" + rel, read(rel))[0] for rel in shown])
523
+ if stamp:
524
+ while len(_REDACTORS) >= 4:
525
+ _REDACTORS.pop(next(iter(_REDACTORS)))
526
+ _REDACTORS[key] = found
527
+ return found
528
+
529
+
530
+ def narrow(decision: PolicyDecision, action: str, prefixes: Sequence[str]) -> PolicyDecision:
531
+ if action not in ACTIONS:
532
+ raise ValueError("invalid action")
533
+ selected = tuple(prefix for prefix in prefixes if within(prefix, getattr(decision, action + "_prefixes")))
534
+ return replace(decision, **{action + "_prefixes": selected}, **({"scopes": selected} if action_for(decision.permission) == action else {}))
535
+
536
+
537
+ def action_for(permission: Permission) -> str:
538
+ return "read" if permission in {Permission.READ, Permission.DISCOVER} else "write"
36
539
 
37
540
 
38
541
  @dataclass(frozen=True, slots=True)
@@ -40,6 +543,18 @@ class ActorGrant:
40
543
  permissions: frozenset[Permission]
41
544
  scopes: tuple[str, ...] = ()
42
545
  resource_ids: tuple[str, ...] = ()
546
+ audience: str = "member"
547
+ read: tuple[str, ...] | None = None
548
+ write: tuple[str, ...] | None = None
549
+ append: tuple[str, ...] | None = None
550
+
551
+ def decision_fields(self, allowed: bool) -> dict:
552
+ return dict(
553
+ audience=self.audience if allowed else "member", scope="read", holds_epoch="",
554
+ read_prefixes=self.read if self.read is not None else (self.scopes if any(action_for(p) == "read" for p in self.permissions) else ()),
555
+ write_prefixes=self.write if self.write is not None else (self.scopes if any(action_for(p) == "write" for p in self.permissions) else ()),
556
+ append_prefixes=self.append if self.append is not None else (),
557
+ )
43
558
 
44
559
 
45
560
  @dataclass(slots=True)
@@ -96,6 +611,7 @@ class StaticPolicy(ActionAuthorizer):
96
611
  resource_ids=grant.resource_ids if grant else (),
97
612
  scopes=grant.scopes if grant else (),
98
613
  reasons=tuple(reasons) if reasons else ("explicit actor grant",),
614
+ **(grant or ActorGrant(frozenset())).decision_fields(not reasons),
99
615
  )
100
616
 
101
617
  def authorize_action(
@@ -124,21 +640,132 @@ class StaticPolicy(ActionAuthorizer):
124
640
  resource_ids=tuple(resource_ids),
125
641
  scopes=grant.scopes if grant else (),
126
642
  reasons=tuple(reasons) if reasons else ("explicit actor grant",),
643
+ **(grant or ActorGrant(frozenset())).decision_fields(not reasons),
644
+ )
645
+
646
+
647
+ @dataclass(slots=True)
648
+ class _ConfiguredPolicy(ActionAuthorizer):
649
+ inputs: AuthorizationInputs
650
+
651
+ def _policy(self, actor: ActorContext) -> StaticPolicy:
652
+ return StaticPolicy(actor.profile.org_id, self.inputs.policy, {
653
+ actor.actor.actor_id: ActorGrant(frozenset(Permission), scopes=("memory",)),
654
+ })
655
+
656
+ def _finish(self, actor: ActorContext, decision: PolicyDecision) -> PolicyDecision:
657
+ alias = actor.actor.aliases.get("github.username")
658
+ handle = alias.strip().casefold() if isinstance(alias, str) else ""
659
+ reasons = list(decision.reasons) if not decision.allowed else []
660
+ if self.inputs.problem:
661
+ reasons.append(self.inputs.problem)
662
+ if handle and handle in self.inputs.removed:
663
+ reasons.append("this person was removed from the organization")
664
+ if self.inputs.policy not in {"all-access-v1", "roster-v1-shadow", "roster-v1"}:
665
+ reasons.append(f"unknown access policy {self.inputs.policy}")
666
+ return replace(decision, allowed=not reasons, reasons=tuple(reasons) or decision.reasons,
667
+ audience="admin" if not reasons and handle and handle in self.inputs.admins else "member",
668
+ holds_epoch=held.holds_epoch(self.inputs.admins, self.inputs.released),
669
+ released=self.inputs.released)
670
+
671
+ def authorize_observe(self, actor: ActorContext, request: RetrievalRequest) -> PolicyDecision:
672
+ return self._finish(actor, self._policy(actor).authorize_observe(actor, request))
673
+
674
+ def authorize_action(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
675
+ return self._finish(actor, self._policy(actor).authorize_action(actor, permission, resource_ids))
676
+
677
+ def authorize_principal(self, handle: str, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
678
+ handle = handle.strip().casefold()
679
+ row = principal(self.inputs, handle)
680
+ reasons = [reason for reason in (self.inputs.problem, self.inputs.principals_problem) if reason]
681
+ if row is None:
682
+ reasons.append(f"no outside principal {handle}")
683
+ elif row.problem:
684
+ reasons.append(f"access.principals.{handle} is invalid: {row.problem}")
685
+ if row and (row.handle in self.inputs.removed or row.identity in self.inputs.removed):
686
+ reasons.append("this person was removed from the organization")
687
+ action = action_for(permission)
688
+ if permission not in {Permission.DISCOVER, Permission.READ, Permission.WRITE}:
689
+ reasons.append(f"outside principals cannot {permission.value}")
690
+ elif row and not (row.read if action == "read" else row.append):
691
+ reasons.append(f"{handle} holds no {'read' if action == 'read' else 'append'} grant")
692
+ if self.inputs.policy not in {"all-access-v1", "roster-v1-shadow", "roster-v1"}:
693
+ reasons.append(f"unknown access policy {self.inputs.policy}")
694
+ # A decision carries only the action it decided, so nothing it holds goes unchecked by as_principal.
695
+ read = row.read if not reasons and row and action == "read" else ()
696
+ append = row.append if not reasons and row and action != "read" else ()
697
+ return PolicyDecision(
698
+ allowed=not reasons, actor_id=f"principal:{handle}", org_id=self.inputs.org_id,
699
+ permission=permission, policy_epoch=self.inputs.policy, resource_ids=tuple(resource_ids),
700
+ reasons=tuple(reasons) or ("outside principal grant",), audience="outside", scope="hand-over",
701
+ read_prefixes=read, append_prefixes=append, write_prefixes=(), scopes=read if action == "read" else (),
702
+ holds_epoch=held.holds_epoch(self.inputs.admins, self.inputs.released),
703
+ released=self.inputs.released, memory_revision=self.inputs.memory_revision,
127
704
  )
128
705
 
129
706
 
130
- def all_access_dogfood_policy(
131
- *, org_id: str, actor_id: str, policy_epoch: str = "all-access-v1"
132
- ) -> StaticPolicy:
133
- """Return the explicit dogfood policy; never bypass the policy interface."""
134
-
135
- return StaticPolicy(
136
- org_id=org_id,
137
- policy_epoch=policy_epoch,
138
- grants={
139
- actor_id: ActorGrant(
140
- permissions=frozenset(Permission),
141
- scopes=("memory",),
142
- )
143
- },
144
- )
707
+ @dataclass(slots=True)
708
+ class _RootPolicy(_ConfiguredPolicy):
709
+ """A Runtime over a projection root (``access materialize``) reads the generation the root admits and writes
710
+ nothing. Whoever its copied settings or identity name, the decision is the same: audience ``root``, read
711
+ ``memory`` (all of it is the grant, held material and held names already gone), and ``check`` refuses it while
712
+ the root is dark."""
713
+
714
+ def _decide(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
715
+ from .projection import READ_ONLY
716
+ reasons = [self.inputs.problem] if self.inputs.problem else []
717
+ if permission not in {Permission.READ, Permission.DISCOVER}:
718
+ reasons = [READ_ONLY]
719
+ granted = () if reasons else ("memory",)
720
+ return PolicyDecision(allowed=not reasons, actor_id=actor.actor.actor_id, org_id=actor.profile.org_id,
721
+ permission=permission, policy_epoch="all-access-v1", resource_ids=tuple(resource_ids),
722
+ scopes=granted, reasons=tuple(reasons) or ("read-only copy",), audience="root",
723
+ read_prefixes=granted)
724
+
725
+ def authorize_observe(self, actor: ActorContext, request: RetrievalRequest) -> PolicyDecision:
726
+ return self._decide(actor, Permission.READ)
727
+
728
+ def authorize_action(self, actor: ActorContext, permission: Permission, resource_ids: Sequence[str] = ()) -> PolicyDecision:
729
+ return self._decide(actor, permission, resource_ids)
730
+
731
+
732
+ def policy_for(inputs: AuthorizationInputs) -> ActionAuthorizer:
733
+ """Build operator policy and authorize_principal decisions from authorization inputs."""
734
+ return _RootPolicy(inputs) if inputs.source == PROJECTION_ROOT else _ConfiguredPolicy(inputs)
735
+
736
+
737
+ def committed_settings(root: Path, decision: PolicyDecision, *, read_budget=None) -> AuthorizationInputs | None:
738
+ """The committed inputs an outside principal's decision (``as_principal``) was made from, read again for what a
739
+ hand-over copies besides memory; None once they make another decision (the base moved in between)."""
740
+ handle = decision.actor_id.removeprefix("principal:")
741
+ inputs = authorization_inputs(root, handle, read_budget=read_budget)
742
+ return inputs if policy_for(inputs).authorize_principal(handle, decision.permission) == decision else None
743
+
744
+
745
+ def as_principal(root: Path, actor: ActorContext, operator: PolicyDecision, handle: str,
746
+ permission: Permission, *, read_budget=None) -> PolicyDecision:
747
+ """Act for an outside principal: its committed grant, refused unless the actor holds all of it.
748
+
749
+ The one way a Runtime surface decides another principal. The principal's
750
+ grant, policy, releases and memory come only from the committed base; the
751
+ acting operator stays an operator: ``operator`` is their own working-tree
752
+ decision, and they must also be eligible on the committed base (a removal
753
+ undone only in the working tree acts for no one). Every prefix the result
754
+ holds must sit inside the operator's for that action.
755
+ """
756
+ handle = handle.strip().casefold()
757
+ policy = policy_for(authorization_inputs(root, handle, read_budget=read_budget))
758
+ decision = policy.authorize_principal(handle, permission)
759
+ if not decision.allowed:
760
+ return decision
761
+ reasons = [] if operator.allowed and policy.authorize_action(actor, permission).allowed else [
762
+ "whoever acts for them holds no grant"]
763
+ if operator.org_id != decision.org_id:
764
+ reasons.append("the shared settings name another organization")
765
+ for action, holding in (("read", operator.read_prefixes), ("append", operator.append_prefixes + operator.write_prefixes)):
766
+ reasons += [f"acting for {handle} would widen {action} to {prefix}"
767
+ for prefix in getattr(decision, action + "_prefixes") if not within(prefix, holding)]
768
+ if not reasons:
769
+ return decision
770
+ return replace(decision, allowed=False, reasons=tuple(reasons),
771
+ read_prefixes=(), append_prefixes=(), write_prefixes=(), scopes=())