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,1848 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // boundary-guard.cjs — the one implementation of Egregore's environment
5
+ // isolation policy. Every runtime calls it before each action:
6
+ // Claude Code .claude/hooks/boundary-check.sh → bin/boundary-hook.sh
7
+ // Codex .codex/hooks.json PreToolUse → bin/boundary-hook.sh
8
+ // Pi / Prime tool_call handler → toolCallBlock() ({ block, reason })
9
+ // bin/boundary-hook.sh runs this file with the PreToolUse exit contract and
10
+ // turns any failure to start into a denial. Session starts write the boundary
11
+ // cache through writeCache() (CLI: `write-cache <project_dir>`) and Claude's
12
+ // native deny rules through nativeDenials() (CLI: `native-denials`);
13
+ // bin/boundary.sh delegates its `check` and `compute-denied` modes here.
14
+ // Adapters stay thin; no rule lives anywhere else.
15
+ //
16
+ // Two-tier model (decided 2026-07-08, harvest-2026-07-08-boundary-hook-refinement):
17
+ // HARD tier — other instances, their Claude transcripts, and shared
18
+ // per-user state except documented agent-facing families:
19
+ // denied for every tool, always. No consent path exists.
20
+ // SOFT tier — everything else outside the boundary: consent-gated. Grants
21
+ // live in .egregore-boundary-consent (session-scoped, cleared by
22
+ // session start) or in config read roots.
23
+ // Posture (strict|standard|open), lock, and read roots are merged from
24
+ // egregore.json .boundary + .egregore-boundary.local.json into the cached
25
+ // boundary JSON. permission_mode=bypassPermissions skips the soft tier (never
26
+ // the hard tier) unless the org boundary is locked.
27
+ //
28
+ // Failure policy: the hard tier fails closed. Denied instances are the
29
+ // registry's entries plus the session cache's, never fewer: a registry that
30
+ // has gone missing while the cache still lists denials keeps those denials.
31
+ // Known denials are enforced before any other rule. An unreadable or empty
32
+ // registry then denies every path outside the core boundary; an unexpected
33
+ // error denies paths outside the core boundary and allows paths inside it.
34
+ // The registry and the boundary cache can be read, never changed, through a
35
+ // tool.
36
+ //
37
+ // What this check guarantees, and what it does not. Structured tools (read,
38
+ // write, edit, search, patch, unknown tools with path fields) are checked on
39
+ // their resolved paths. Shell text is read literally, in linear time: words
40
+ // after quote removal, ~ / $HOME / ${HOME} / $PWD, names relative to the
41
+ // working directory as literal cd / pushd steps change it, brace lists of up
42
+ // to 64 items, wildcards (judged by their literal directory prefix), and
43
+ // recursive walks (judged by their root). The shell check never guesses
44
+ // which directory a name might belong to. It cannot see a path assembled at
45
+ // run time (a variable root, command output, interpreter path arithmetic,
46
+ // encoded text), a command split across separate write_stdin calls, or a
47
+ // directory changed in an earlier call. Confining those is the job of an
48
+ // OS-level sandbox, the planned follow-up layer.
49
+
50
+ const crypto = require("node:crypto");
51
+ const fs = require("node:fs");
52
+ const os = require("node:os");
53
+ const path = require("node:path");
54
+
55
+ const POSTURES = new Set(["strict", "standard", "open"]);
56
+ // Agent-facing operations name only these shared per-user families. The drift
57
+ // test imports this single source of truth; a trailing * means a name prefix.
58
+ const EGREGORE_ALLOWED_FAMILIES = Object.freeze([
59
+ "loom*", "telemetry*", "platform", "exports", "emissary-config.json", "context", "trails",
60
+ ]);
61
+ const SHARED_NATIVE_DENIALS = [
62
+ "mind", "runtime", "attendant", "graph-cache", "notifications", "notices-*",
63
+ "session-*", "greeting-card-*", "graph-wal*", "autosave.log",
64
+ "gh-identity-mismatch-*", "dashboard-id-*",
65
+ ];
66
+ const READ_TOOLS = new Set(["Read", "read", "ls", "view_image", "NotebookRead"]);
67
+ // Structured searches walk a directory tree from their root and honour ignore
68
+ // files (Claude's Grep and Glob, Pi's grep and find).
69
+ const SEARCH_TOOLS = new Set(["Glob", "Grep", "grep", "find"]);
70
+ const WRITE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "write", "edit"]);
71
+ // write_stdin continues a Codex interactive session; each chunk is checked as
72
+ // shell text on its own.
73
+ const SHELL_TOOLS = new Set(["Bash", "exec_command", "shell_command", "shell", "bash", "ipython", "write_stdin"]);
74
+ const PATCH_TOOLS = new Set(["apply_patch"]);
75
+ // Egregore's own tools that open a file named in their input.
76
+ const TOOL_PATHS = { egregore_publish_scroll: { kind: "read", fields: ["source"] } };
77
+ // Any other tool: path-named fields (snake_case after splitting camelCase), at
78
+ // any depth, get the hard-tier check.
79
+ const PATH_KEY = /(?:^|_)(?:path|paths|file|files|filename|filenames|source|sources|src|dir|dirs|directory|directories|cwd|workdir|target|targets|dest|destination|root|roots)$/;
80
+
81
+ // Soft tier, best effort: only user-home-area literals. System paths are core
82
+ // and relative paths stay inside the project; a miss degrades to "no prompt",
83
+ // never to a hard-tier breach.
84
+ const SOFT_TOKEN = /(?:~|\/Users\/[A-Za-z0-9._-]+|\/home\/[A-Za-z0-9._-]+)\/[A-Za-z0-9._/-]+/g;
85
+
86
+ // Markers inside a lexed shell word. Input control characters are replaced
87
+ // before lexing, so a command cannot forge them.
88
+ const UNKNOWN = "\u0000"; // a value known only at run time ($VAR, $(…), `…`)
89
+ const PWD_MARK = "\u0001"; // $PWD: the working directory where the word runs
90
+ const LITERAL = "\u0002"; // the next character was quoted or escaped
91
+ const WILD = "*?[]{},";
92
+ // Inside a word, the characters that end a path written in code or options.
93
+ const FRAGMENT = /[\s;&|()<>=:"'`]+/;
94
+ const BRACE_LIMIT = 64;
95
+ const DIR_SCAN_LIMIT = 10000;
96
+
97
+ // Command classes, by program basename.
98
+ const WRAPPERS = new Set(["sudo", "doas", "command", "builtin", "env", "nohup", "nice", "time", "exec", "xargs",
99
+ "timeout", "stdbuf", "caffeinate", "then", "do", "else", "elif", "if", "while", "until", "!", "{", "}", "fi", "done"]);
100
+ const SHELLS = new Set(["bash", "sh", "zsh", "dash", "ksh"]);
101
+ const INTERPRETERS = new Set(["python", "python3", "node", "ruby", "perl", "php", "osascript", "deno", "bun", "eval",
102
+ "awk", "gawk", ...SHELLS]);
103
+ const DELETERS = new Set(["rm", "rmdir", "unlink", "shred", "srm", "trash"]);
104
+ const COPIERS = new Set(["cp", "install", "ln", "rsync", "ditto", "scp", "gcp"]);
105
+ const OVERWRITERS = new Set(["truncate", "tee", "dd"]);
106
+
107
+ function home() {
108
+ return process.env.HOME || os.homedir();
109
+ }
110
+
111
+ function md5(value) {
112
+ return crypto.createHash("md5").update(String(value)).digest("hex");
113
+ }
114
+
115
+ function cachePath(projectDir) {
116
+ return `/tmp/egregore-boundary-${md5(projectDir)}.json`;
117
+ }
118
+
119
+ function registryPath() {
120
+ return path.join(home(), ".egregore", "instances.json");
121
+ }
122
+
123
+ function within(candidate, root) {
124
+ return Boolean(root) && (candidate === root || candidate.startsWith(`${root}/`));
125
+ }
126
+
127
+ function strings(value) {
128
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item) : [];
129
+ }
130
+
131
+ function unique(values) {
132
+ return [...new Set(values)];
133
+ }
134
+
135
+ function readJson(file) {
136
+ try {
137
+ return JSON.parse(fs.readFileSync(file, "utf8"));
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ function isDirectory(candidate) {
144
+ try {
145
+ return fs.statSync(candidate).isDirectory();
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ function exists(candidate) {
152
+ try {
153
+ fs.lstatSync(candidate);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ // ── Path resolution ──────────────────────────────────────────────────────
161
+
162
+ // Physical resolution, like the kernel's: symlinks are followed and ".." is
163
+ // applied to the resolved directory (realpath.native, not node's lexical
164
+ // pre-pass). A target that does not exist yet resolves its nearest existing
165
+ // ancestor, so a write through a symlinked directory with several missing
166
+ // levels below it still lands where the kernel would put it.
167
+ function physicalPath(candidate, depth = 0) {
168
+ try {
169
+ return fs.realpathSync.native(candidate);
170
+ } catch {}
171
+ const parts = candidate.split("/");
172
+ for (let i = parts.length - 1; i > 0; i--) {
173
+ let base;
174
+ try {
175
+ base = fs.realpathSync.native(parts.slice(0, i).join("/") || "/");
176
+ } catch {
177
+ continue;
178
+ }
179
+ const rest = parts.slice(i);
180
+ const joined = path.posix.normalize(`${base}/${rest.join("/")}`);
181
+ // ".." past a missing level can climb back into existing, possibly
182
+ // linked, directories: resolve the collapsed path again.
183
+ if (rest.includes("..") && depth < 8) return physicalPath(joined, depth + 1);
184
+ return joined.length > 1 ? joined.replace(/\/$/, "") : joined;
185
+ }
186
+ return path.posix.normalize(candidate);
187
+ }
188
+
189
+ function expandHome(target, h) {
190
+ return String(target).replace(/^~(?=\/|$)/, h).replace(/^\$\{?HOME\}?(?=\/|$)/, h);
191
+ }
192
+
193
+ function absolute(target, baseDir, h) {
194
+ const expanded = expandHome(target, h);
195
+ return expanded.startsWith("/") ? expanded : `${baseDir}/${expanded}`;
196
+ }
197
+
198
+ function resolvePath(target, baseDir) {
199
+ return physicalPath(absolute(target, baseDir, home()));
200
+ }
201
+
202
+ // ── Boundary computation (session start, worktree creation, every check) ──
203
+
204
+ // Registered instances not proven to share this project's identity. An entry
205
+ // is excluded only when it IS the project, is the caller-proven same instance
206
+ // (allowPath: the main checkout a linked worktree shares its git dir with), or
207
+ // both org_ids are present and equal. No path relationship proves identity.
208
+ // A registry that exists but cannot be read, is empty, or does not parse is
209
+ // reported, never treated as empty: the caller fails closed on it. Session
210
+ // start writes the registry through a temporary file and a rename, so an
211
+ // empty registry is damage, not a write in progress.
212
+ function computeDenied(registry, projectDir, orgId = "", allowPath = "") {
213
+ let raw;
214
+ try {
215
+ raw = fs.readFileSync(registry, "utf8");
216
+ } catch (error) {
217
+ if (error && error.code === "ENOENT") return { paths: [], unreadable: false };
218
+ return { paths: [], unreadable: true };
219
+ }
220
+ let entries;
221
+ try {
222
+ entries = JSON.parse(raw);
223
+ } catch {
224
+ return { paths: [], unreadable: true };
225
+ }
226
+ if (!Array.isArray(entries)) return { paths: [], unreadable: true };
227
+ const paths = entries
228
+ .filter((entry) => entry && typeof entry === "object" && typeof entry.path === "string" && entry.path)
229
+ .filter((entry) => entry.path !== projectDir)
230
+ .filter((entry) => !allowPath || entry.path !== allowPath)
231
+ .filter((entry) => !orgId || (typeof entry.org_id === "string" ? entry.org_id : "") !== orgId)
232
+ .map((entry) => entry.path);
233
+ return { paths, unreadable: false };
234
+ }
235
+
236
+ // A linked worktree's .git is a file naming its git dir inside the main
237
+ // checkout (<main>/.git/worktrees/<name>); the main checkout is that dir's
238
+ // great-grandparent. Same derivation as worktree-create.sh. The .git file
239
+ // sits in the project and anyone working there can rewrite it, so the claim
240
+ // counts only when the git dir links back to this project, as git's own
241
+ // <gitdir>/gitdir file does for a real linked worktree.
242
+ function mainCheckout(projectDir) {
243
+ try {
244
+ const gitFile = path.join(projectDir, ".git");
245
+ if (!fs.statSync(gitFile).isFile()) return "";
246
+ const match = fs.readFileSync(gitFile, "utf8").match(/^gitdir:\s*(.+)$/m);
247
+ if (!match) return "";
248
+ const gitDir = path.resolve(projectDir, match[1].trim());
249
+ const back = fs.readFileSync(path.join(gitDir, "gitdir"), "utf8").trim();
250
+ if (fs.realpathSync(path.resolve(gitDir, back)) !== fs.realpathSync(gitFile)) return "";
251
+ const main = path.resolve(gitDir, "..", "..", "..");
252
+ return isDirectory(main) ? main : "";
253
+ } catch {
254
+ return "";
255
+ }
256
+ }
257
+
258
+ function managedRepos(projectDir, main) {
259
+ const parent = path.dirname(main || projectDir);
260
+ const config = readJson(path.join(projectDir, "egregore.json")) || {};
261
+ const repos = Array.isArray(config.repos) ? config.repos : [];
262
+ const resolved = main ? [main] : [];
263
+ for (const entry of repos) {
264
+ const name = typeof entry === "string" ? entry : entry && typeof entry.name === "string" ? entry.name : "";
265
+ if (!name || name.includes("..") || name.startsWith("/")) continue;
266
+ let real;
267
+ try {
268
+ real = fs.realpathSync(path.join(parent, name));
269
+ } catch {
270
+ continue;
271
+ }
272
+ if (!real.startsWith(`${parent}/`)) continue;
273
+ if (!resolved.includes(real)) resolved.push(real);
274
+ }
275
+ return resolved;
276
+ }
277
+
278
+ // Posture, lock, and read roots: the org layer (egregore.json .boundary,
279
+ // committed) merged with the personal layer (.egregore-boundary.local.json,
280
+ // gitignored), which is ignored entirely when the org layer is locked.
281
+ function boundaryPolicy(configDir) {
282
+ const org = (readJson(path.join(configDir, "egregore.json")) || {}).boundary || {};
283
+ let posture = POSTURES.has(org.posture) ? org.posture : "standard";
284
+ const locked = org.locked === true || org.locked === "true";
285
+ const personal = locked ? null : readJson(path.join(configDir, ".egregore-boundary.local.json"));
286
+ if (personal && POSTURES.has(personal.posture)) posture = personal.posture;
287
+ const h = home();
288
+ const roots = posture === "strict" ? [] : [`${h}/Downloads`, `${h}/Desktop`];
289
+ roots.push(...strings(org.read));
290
+ if (personal) roots.push(...strings(personal.read));
291
+ const readRoots = [...new Set(roots.map((root) => root.replace(/^~/, h)))].sort();
292
+ return { posture, locked, read_roots: readRoots };
293
+ }
294
+
295
+ function computeBoundary(projectDir) {
296
+ const main = mainCheckout(projectDir);
297
+ let memoryDir = "";
298
+ try {
299
+ const memory = path.join(projectDir, "memory");
300
+ if (fs.lstatSync(memory).isSymbolicLink()) memoryDir = fs.realpathSync(memory);
301
+ } catch {}
302
+ const config = readJson(path.join(projectDir, "egregore.json")) ||
303
+ (main ? readJson(path.join(main, "egregore.json")) : null) || {};
304
+ const orgId = typeof config.org_id === "string" ? config.org_id : "";
305
+ const denied = computeDenied(registryPath(), projectDir, orgId, main);
306
+ const boundary = {
307
+ project_dir: projectDir,
308
+ main_checkout: main,
309
+ memory_dir: memoryDir,
310
+ ...boundaryPolicy(main || projectDir),
311
+ managed_repos: managedRepos(projectDir, main),
312
+ denied_paths: denied.paths,
313
+ };
314
+ if (denied.unreadable) boundary.registry_unreadable = true;
315
+ return boundary;
316
+ }
317
+
318
+ function writeCache(projectDir) {
319
+ const boundary = computeBoundary(projectDir);
320
+ const target = cachePath(projectDir);
321
+ const temporary = `${target}.${process.pid}.tmp`;
322
+ fs.writeFileSync(temporary, `${JSON.stringify(boundary, null, 2)}\n`);
323
+ fs.renameSync(temporary, target);
324
+ return target;
325
+ }
326
+
327
+ // Claude Code's native permission rules, the second line of defence there.
328
+ // "//" marks an absolute path in Claude's rule syntax. Native rules cannot
329
+ // express deny-by-default with exceptions, so enumerate known and present
330
+ // state; the hook handles unknown families created after generation.
331
+ function nativeDenials(projectDir) {
332
+ const ctx = context(projectDir, {});
333
+ const rules = [];
334
+ const own = ownRoots(ctx);
335
+ const ownTranscripts = transcriptRoots(ctx).flatMap((root) => ownEncodings(ctx).map((name) => `${root}/${name}`));
336
+ const add = (target, file = false) => {
337
+ // Native rules cannot carve a project out of an enclosing denied tree.
338
+ // The hook still enforces the remainder of that tree on every call.
339
+ const covers = new RegExp(`^${escapeRegex(target).replace(/\\\*/g, "[^/]*")}(?:/|$)`);
340
+ if ([...own, ...ownTranscripts].some((root) => covers.test(root))) return;
341
+ rules.push(`Read(/${target}/**)`, `Edit(/${target}/**)`);
342
+ if (file) rules.push(`Read(/${target})`, `Edit(/${target})`);
343
+ };
344
+ for (const denied of ctx.boundary.denied) add(denied);
345
+ for (const root of sharedRoots(ctx)) {
346
+ const entries = directoryEntries(root);
347
+ const names = unique([...SHARED_NATIVE_DENIALS, ...entries.map((entry) => entry.name)]);
348
+ for (const name of names) {
349
+ if (name === "instances.json" || allowedFamily(name)) continue;
350
+ const entry = entries.find((entry) => entry.name === name);
351
+ // The fixed wildcard families include files (notices, lifecycle, WALs,
352
+ // identity markers); cover their bare names even before one exists.
353
+ add(`${root}/${name}`, name === "autosave.log" || name.endsWith("*") || Boolean(entry && !entry.isDirectory()));
354
+ }
355
+ }
356
+ // Enumerate existing folders, not worktree globs: an own encoding may
357
+ // collide with another instance's worktree prefix. New families/folders
358
+ // created after generation are always covered by the hook.
359
+ for (const root of transcriptRoots(ctx)) {
360
+ for (const entry of directoryEntries(root)) {
361
+ const target = `${root}/${entry.name}`;
362
+ if (isDirectory(target) && transcriptDenial(target, ctx)) add(target);
363
+ }
364
+ }
365
+ // Edit() covers Write/NotebookEdit too; Write() rules are never matched.
366
+ rules.push("Edit(~/.egregore/instances.json)");
367
+ return unique(rules);
368
+ }
369
+
370
+ // ── Boundary loading ─────────────────────────────────────────────────────
371
+
372
+ function normalizeBoundary(raw) {
373
+ const b = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
374
+ const posture = POSTURES.has(b.posture) ? b.posture : "standard";
375
+ const h = home();
376
+ // Legacy caches (no read_roots key) get the inbox defaults unless strict.
377
+ const readRoots = Object.prototype.hasOwnProperty.call(b, "read_roots")
378
+ ? strings(b.read_roots)
379
+ : posture === "strict" ? [] : [`${h}/Downloads`, `${h}/Desktop`];
380
+ return {
381
+ projectDir: typeof b.project_dir === "string" ? b.project_dir : "",
382
+ mainCheckout: typeof b.main_checkout === "string" ? b.main_checkout : "",
383
+ memoryDir: typeof b.memory_dir === "string" ? b.memory_dir : "",
384
+ posture,
385
+ locked: b.locked === true || b.locked === "true",
386
+ readRoots,
387
+ managedRepos: strings(b.managed_repos),
388
+ denied: strings(b.denied_paths),
389
+ registryUnreadable: b.registry_unreadable === true,
390
+ };
391
+ }
392
+
393
+ // A cache is used only when its shape is intact; anything else is ignored.
394
+ function usableCache(value) {
395
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
396
+ const list = (key) => !(key in value) || (Array.isArray(value[key]) && value[key].every((item) => typeof item === "string"));
397
+ return Array.isArray(value.denied_paths) && list("denied_paths") && list("managed_repos") && list("read_roots");
398
+ }
399
+
400
+ // Each denied entry in its registered spelling and its on-disk form, so a link
401
+ // or a different letter case in either spelling still matches a physically
402
+ // resolved target.
403
+ function deniedForms(entries) {
404
+ return unique(entries.flatMap((denied) => [denied, physicalPath(denied)]));
405
+ }
406
+
407
+ // The registry is read on every check (one small file). The session cache
408
+ // supplies the soft-tier policy and core roots it was written with, and its
409
+ // denials are kept: denied = registry ∪ cache, never fewer. A registry that
410
+ // disappeared after session start therefore keeps the cached denials, and a
411
+ // stale, emptied or planted cache cannot open the wall.
412
+ function loadBoundary(projectDir) {
413
+ const fresh = normalizeBoundary(computeBoundary(projectDir));
414
+ const cached = readJson(cachePath(projectDir));
415
+ const boundary = usableCache(cached) ? normalizeBoundary(cached) : { ...fresh };
416
+ boundary.denied = deniedForms([...fresh.denied, ...boundary.denied]);
417
+ boundary.registryUnreadable = fresh.registryUnreadable || boundary.registryUnreadable;
418
+ // Roots that relax a hard-tier rule come from disk, never from the cache.
419
+ boundary.verified = { memoryDir: fresh.memoryDir, managedRepos: fresh.managedRepos, mainCheckout: fresh.mainCheckout };
420
+ return boundary;
421
+ }
422
+
423
+ // ── Policy ───────────────────────────────────────────────────────────────
424
+
425
+ function context(projectDir, input) {
426
+ const boundary = loadBoundary(projectDir);
427
+ const permissionMode = String(input.permission_mode || input.permissionMode || "");
428
+ let realProject = projectDir;
429
+ try { realProject = fs.realpathSync.native(projectDir); } catch {}
430
+ return {
431
+ projectDir,
432
+ // The project as it sits on disk: the hard tier's self-scope carve-out
433
+ // keys on the project the hook runs for, never on a cached value.
434
+ realProject,
435
+ boundary,
436
+ home: home(),
437
+ // A session the user runs in bypassPermissions has already declared
438
+ // maximal trust — Egregore doesn't re-ask (soft tier off). The hard tier
439
+ // stays on. An org lock disables this relaxation.
440
+ relaxed: !boundary.locked && permissionMode === "bypassPermissions",
441
+ // A deleted project (removed worktree) keeps the hard tier only.
442
+ softOff: !isDirectory(projectDir),
443
+ memo: new Map(),
444
+ };
445
+ }
446
+
447
+ // physicalPath, remembered for one decision: a long command names the same
448
+ // path many times.
449
+ function resolved(ctx, candidate) {
450
+ if (!ctx.memo) return physicalPath(candidate);
451
+ let value = ctx.memo.get(candidate);
452
+ if (value === undefined) {
453
+ value = physicalPath(candidate);
454
+ ctx.memo.set(candidate, value);
455
+ }
456
+ return value;
457
+ }
458
+
459
+ function lexicalPath(candidate) {
460
+ return path.posix.normalize(candidate).replace(/\/$/, "") || "/";
461
+ }
462
+
463
+ function pathSpellings(target, ctx) {
464
+ return unique([lexicalPath(target), resolved(ctx, target)]);
465
+ }
466
+
467
+ function sharedRoots(ctx) {
468
+ return pathSpellings(`${ctx.home}/.egregore`, ctx);
469
+ }
470
+
471
+ function transcriptRoots(ctx) {
472
+ return pathSpellings(`${ctx.home}/.claude/projects`, ctx);
473
+ }
474
+
475
+ function allowedFamily(name) {
476
+ return EGREGORE_ALLOWED_FAMILIES.some((family) => family.endsWith("*")
477
+ ? name.startsWith(family.slice(0, -1)) : name === family);
478
+ }
479
+
480
+ function ownRoots(ctx) {
481
+ const main = ctx.boundary.verified.mainCheckout;
482
+ return unique([ctx.projectDir, ctx.realProject, ...(main ? pathSpellings(main, ctx) : [])]);
483
+ }
484
+
485
+ function encodeProject(project) {
486
+ return project.replace(/[^A-Za-z0-9]/g, "-");
487
+ }
488
+
489
+ function ownEncodings(ctx) {
490
+ return ownRoots(ctx).map(encodeProject);
491
+ }
492
+
493
+ function encodingMatches(name, encoding) {
494
+ return name === encoding || name.startsWith(`${encoding}--claude-worktrees-`);
495
+ }
496
+
497
+ // Both normalized lexical and physical spellings must pass. In particular,
498
+ // an unknown name linked into reports is still unknown; reports linked into
499
+ // runtime is still runtime. Only HOME's state tree is reserved, never a
500
+ // project's own .egregore directory. Apply self-scope per spelling so a link
501
+ // inside the project does not grant access to state outside the project.
502
+ function sharedDenial(target, ctx) {
503
+ for (const candidate of pathSpellings(target, ctx)) {
504
+ if (inOwnProject(candidate, ctx)) continue;
505
+ for (const root of sharedRoots(ctx)) {
506
+ if (candidate === root || !within(candidate, root)) continue;
507
+ const rel = candidate.slice(root.length + 1);
508
+ if (rel === "instances.json" || allowedFamily(rel.split("/")[0])) continue;
509
+ return "shared-state";
510
+ }
511
+ }
512
+ return null;
513
+ }
514
+
515
+ function transcriptDenial(target, ctx) {
516
+ const own = ownEncodings(ctx);
517
+ for (const candidate of pathSpellings(target, ctx)) {
518
+ for (const root of transcriptRoots(ctx)) {
519
+ if (candidate === root || !within(candidate, root)) continue;
520
+ const name = candidate.slice(root.length + 1).split("/")[0];
521
+ if (own.some((encoding) => encodingMatches(name, encoding))) continue;
522
+ if (ctx.boundary.registryUnreadable) return "registry";
523
+ if (ctx.boundary.denied.some((denied) => encodingMatches(name, encodeProject(denied)))) return "transcripts";
524
+ }
525
+ }
526
+ return null;
527
+ }
528
+
529
+ function directoryEntries(root) {
530
+ try { return fs.readdirSync(root, { withFileTypes: true }); } catch { return []; }
531
+ }
532
+
533
+ // Files count too: a recursive grep and a wildcard can read a direct state
534
+ // file just as they can reach a directory. Ignore missing future entries.
535
+ function containedState(root, ctx) {
536
+ for (const [roots, denial] of [[sharedRoots(ctx), sharedDenial], [transcriptRoots(ctx), transcriptDenial]]) {
537
+ for (const parent of roots) {
538
+ for (const entry of directoryEntries(parent)) {
539
+ const target = `${parent}/${entry.name}`;
540
+ const kind = denial(target, ctx);
541
+ if (!kind) continue;
542
+ if (pathSpellings(target, ctx).some((candidate) => candidate !== root && within(candidate, root))) {
543
+ return { path: target, kind };
544
+ }
545
+ }
546
+ }
547
+ }
548
+ return null;
549
+ }
550
+
551
+ function inSystem(target, h) {
552
+ if (target === "/tmp" || target.startsWith("/tmp/") || target === "/private" || target.startsWith("/private/")) return true;
553
+ if (within(target, `${h}/.claude`)) return true;
554
+ if (/^\/(usr|etc|var|bin|sbin|opt|proc|sys|System|Applications|Library)\//.test(target)) return true;
555
+ return target === "/dev" || target.startsWith("/dev/");
556
+ }
557
+
558
+ // Core boundary: no consent needed, every tier. `strict` drops the parent
559
+ // directory allowance and uses only roots verified on disk, for when the
560
+ // registry cannot be read: the siblings of a checkout are exactly where
561
+ // other instances live.
562
+ function inCore(target, ctx, strict = false) {
563
+ const h = ctx.home;
564
+ if (inSystem(target, h)) return true;
565
+ // Names-only listing, the read-only registry, and documented families.
566
+ // All hard-tier checks run first, including for symlinked spellings.
567
+ if (sharedRoots(ctx).some((root) => target === root || target === `${root}/instances.json`
568
+ || (within(target, root) && allowedFamily(target.slice(root.length + 1).split("/")[0])))) return true;
569
+ const b = ctx.boundary;
570
+ if (within(target, ctx.realProject) || within(target, ctx.projectDir)) return true;
571
+ const roots = strict ? b.verified : b;
572
+ if (within(target, roots.memoryDir)) return true;
573
+ if (roots.managedRepos.some((repo) => within(target, repo))) return true;
574
+ if (strict) return false;
575
+ if (within(target, b.projectDir)) return true;
576
+ // Parent directory (sibling repo operations). The hard tier is always
577
+ // checked before this helper, so a denied sibling never reaches it.
578
+ return within(target, path.dirname(b.projectDir || ctx.projectDir));
579
+ }
580
+
581
+ function enclosesProject(denied, ctx) {
582
+ return within(ctx.realProject, denied) || within(ctx.projectDir, denied);
583
+ }
584
+
585
+ function inOwnProject(target, ctx) {
586
+ return within(target, ctx.realProject) || within(target, ctx.projectDir);
587
+ }
588
+
589
+ // The project's own tree: the project, and the main checkout a linked
590
+ // worktree belongs to. An instance registered inside it is nested.
591
+ function inOwnTree(target, ctx) {
592
+ return inOwnProject(target, ctx) || within(target, ctx.boundary.verified.mainCheckout);
593
+ }
594
+
595
+ // HARD tier. Known denials come first, on every path, so neither the core
596
+ // boundary nor a damaged registry can hide them. Self-scope carve-out, per
597
+ // entry: an ENCLOSING denied checkout (different or unproven identity) never
598
+ // covers the project's own files. A denied entry nested INSIDE the project
599
+ // is still enforced: residence under the project proves nothing about
600
+ // identity. With the registry unreadable, every path outside the verified
601
+ // core boundary is then denied.
602
+ function deniedEntry(target, ctx) {
603
+ const state = sharedDenial(target, ctx) || transcriptDenial(target, ctx);
604
+ if (state) return state;
605
+ target = resolved(ctx, target);
606
+ const b = ctx.boundary;
607
+ const inProject = inOwnProject(target, ctx);
608
+ for (const denied of b.denied) {
609
+ if (!within(target, denied)) continue;
610
+ if (inProject && enclosesProject(denied, ctx)) continue;
611
+ return denied;
612
+ }
613
+ if (b.registryUnreadable && !inCore(target, ctx, true)) return "registry";
614
+ return null;
615
+ }
616
+
617
+ // Denied state or an instance strictly inside `root` that exists on disk:
618
+ // a walk or wildcard from root can reach it. A deleted entry cannot.
619
+ function containedDenied(root, ctx) {
620
+ return containedState(root, ctx)
621
+ || ctx.boundary.denied.find((denied) => denied !== root && within(denied, root) && exists(denied)) || null;
622
+ }
623
+
624
+ // The registry and this project's boundary cache: readable, never changed
625
+ // through a tool. Any boundary cache in the temporary directory counts.
626
+ function protectedFiles(ctx) {
627
+ if (!ctx.protectedFiles) {
628
+ const files = [path.join(ctx.home, ".egregore", "instances.json"), cachePath(ctx.projectDir)];
629
+ const main = ctx.boundary && ctx.boundary.verified && ctx.boundary.verified.mainCheckout;
630
+ if (main) files.push(cachePath(main));
631
+ ctx.protectedFiles = unique(files.flatMap((file) => [file, physicalPath(file)]));
632
+ }
633
+ return ctx.protectedFiles;
634
+ }
635
+
636
+ function isProtected(target, ctx) {
637
+ if (protectedFiles(ctx).includes(target)) return true;
638
+ const dir = path.dirname(target);
639
+ return /^egregore-boundary-[0-9a-f]{32}\.json$/.test(path.basename(target))
640
+ && (dir === "/tmp" || dir === physicalPath("/tmp"));
641
+ }
642
+
643
+ // A folder holding protected state: deleting or moving it removes the state.
644
+ function holdsProtected(target, ctx) {
645
+ return protectedFiles(ctx).some((file) => file !== target && within(file, target));
646
+ }
647
+
648
+ function inReadRoots(target, ctx) {
649
+ return ctx.boundary.readRoots.some((root) => within(target, root));
650
+ }
651
+
652
+ function isConsented(target, ctx) {
653
+ // A locked org boundary has no consent path — stale grant files are void.
654
+ if (ctx.boundary.locked) return false;
655
+ let lines;
656
+ try {
657
+ lines = fs.readFileSync(path.join(ctx.projectDir, ".egregore-boundary-consent"), "utf8").split("\n");
658
+ } catch {
659
+ return false;
660
+ }
661
+ return lines.some((line) => line && within(target, line.replace(/^~/, ctx.home)));
662
+ }
663
+
664
+ function allow() {
665
+ return { outcome: "allow", reason: "inside", message: "" };
666
+ }
667
+
668
+ function hardBlock(target, entry) {
669
+ if (entry === "shared-state" || entry === "transcripts") {
670
+ const area = entry === "shared-state" ? "protected shared per-user state" : "another instance's Claude transcripts";
671
+ return {
672
+ outcome: "deny", reason: entry,
673
+ message: `Environment isolation: ${target} names ${area}. Access is denied for every tool and cannot be consented — do not retry and do not attempt via other tools.`,
674
+ };
675
+ }
676
+ if (entry === "registry") {
677
+ return {
678
+ outcome: "deny",
679
+ reason: "registry-unreadable",
680
+ message: `Environment isolation: ${target} cannot be verified because the instance registry (~/.egregore/instances.json) cannot be read, so Egregore cannot tell which folders belong to other instances. Access to this path is denied until the registry is readable again — do not retry and do not attempt via other tools. Ask the user to repair or remove the registry file, then restart the session.`,
681
+ };
682
+ }
683
+ return {
684
+ outcome: "deny",
685
+ reason: "instance",
686
+ message: `Environment isolation: ${target} belongs to another Egregore instance. Access is denied for every tool and cannot be consented — do not retry and do not attempt via other tools. If the user needs something from it, they must fetch it themselves in that instance.`,
687
+ };
688
+ }
689
+
690
+ function walkBlock(root, entry, nested) {
691
+ if (typeof entry === "object") return hardBlock(`${root} (contains ${entry.path})`, entry.kind);
692
+ const where = nested
693
+ ? `${root} holds another Egregore instance registered inside this project (${entry}), and this command walks it without ignore rules`
694
+ : `${root} contains another Egregore instance (${entry}), and a recursive search, copy or listing from there would read it`;
695
+ return {
696
+ outcome: "deny",
697
+ reason: "instance",
698
+ message: `Environment isolation: ${where}. Access is denied for every tool and cannot be consented — do not retry and do not attempt via other tools. Narrow the search to a folder that does not contain ${entry}${nested ? ", or use a search that respects ignore rules (rg without -u / --no-ignore)" : ""}.`,
699
+ };
700
+ }
701
+
702
+ function wildcardBlock(pattern, prefix, entry) {
703
+ if (typeof entry === "object") return hardBlock(`${pattern} (contains ${entry.path})`, entry.kind);
704
+ return {
705
+ outcome: "deny",
706
+ reason: "instance",
707
+ message: `Environment isolation: the wildcard ${pattern} starts at ${prefix}, which contains another Egregore instance (${entry}), so it could expand into that instance. Access is denied for every tool and cannot be consented — do not retry and do not attempt via other tools. Name the files explicitly or narrow the wildcard to a folder that does not contain ${entry}.`,
708
+ };
709
+ }
710
+
711
+ function stateBlock(target) {
712
+ return {
713
+ outcome: "deny",
714
+ reason: "state-write",
715
+ message: `Environment isolation: ${target} is Egregore's isolation state (the instance registry ~/.egregore/instances.json or a boundary cache), which only session start maintains. Changing, moving or deleting it is denied for every tool and cannot be consented — do not retry and do not attempt via other tools.`,
716
+ };
717
+ }
718
+
719
+ function softBlock(target, action, ctx) {
720
+ const dir = path.dirname(target);
721
+ if (ctx.boundary.locked) {
722
+ return {
723
+ outcome: "deny",
724
+ reason: "locked",
725
+ message: `Environment isolation: ${target} is outside this instance's boundary and the org boundary is locked (egregore.json boundary.locked) — no consent path exists. Do not retry. Use AskUserQuestion with options: 'Paste contents inline' / 'Move the file into the repo and point me at the new path' / 'Cancel'.`,
726
+ };
727
+ }
728
+ const session = `'Allow ${dir} for this session' (on approval: append ${dir} as one line to ${ctx.projectDir}/.egregore-boundary-consent, then retry)`;
729
+ // read[] grants reads only, so a blocked write never offers it: every
730
+ // option must grant what it names.
731
+ const always = action === "write"
732
+ ? ""
733
+ : ` / 'Always allow on this instance' (on approval: add "${dir}" to the read[] array in ${ctx.projectDir}/.egregore-boundary.local.json, then retry)`;
734
+ return {
735
+ outcome: "consent",
736
+ reason: "consent",
737
+ message: `Boundary consent needed: ${target} is outside this instance's ${action} surface (posture: ${ctx.boundary.posture}). Do not retry yet and do not route around via other tools. Ask via AskUserQuestion with exactly these options: ${session}${always} / 'Paste contents inline' / 'Cancel'. Never write a consent grant without the user's explicit approval in this exchange.`,
738
+ };
739
+ }
740
+
741
+ function hardCheck(target, ctx) {
742
+ const entry = deniedEntry(target, ctx);
743
+ return entry ? hardBlock(target, entry) : null;
744
+ }
745
+
746
+ // A walk from root: stopped by an instance below a root outside the
747
+ // project's own tree, and by an instance nested inside the project only when
748
+ // the walk ignores ignore files (nested instances live in gitignored runtime
749
+ // state).
750
+ function walkCheck(root, ctx, bypassesIgnore) {
751
+ const nested = inOwnTree(root, ctx);
752
+ if (nested && !bypassesIgnore) return null;
753
+ const inside = containedDenied(root, ctx);
754
+ return inside ? walkBlock(root, inside, nested) : null;
755
+ }
756
+
757
+ function checkPath(target, action, ctx, baseDir) {
758
+ const named = absolute(target, baseDir, ctx.home);
759
+ const place = resolved(ctx, named);
760
+ const hard = hardCheck(named, ctx);
761
+ if (hard) return hard;
762
+ if (action === "write" && isProtected(place, ctx)) return stateBlock(place);
763
+ if (inCore(place, ctx) || ctx.softOff) return null;
764
+ if (action === "read" && ctx.boundary.posture === "open") return null;
765
+ if (ctx.relaxed) return null;
766
+ if (action === "read" && inReadRoots(place, ctx)) return null;
767
+ if (isConsented(place, ctx)) return null;
768
+ return softBlock(place, action, ctx);
769
+ }
770
+
771
+ // ── Wildcards ────────────────────────────────────────────────────────────
772
+
773
+ function plainText(text) {
774
+ return text.split(LITERAL).join("");
775
+ }
776
+
777
+ function escapeLiteral(text) {
778
+ return String(text).replace(/[*?[\]{},]/g, (char) => LITERAL + char);
779
+ }
780
+
781
+ // Index of the first character that makes a word a pattern: an unquoted
782
+ // wildcard or brace, or a run-time value.
783
+ function firstSpecial(text) {
784
+ for (let i = 0; i < text.length; i++) {
785
+ const c = text[i];
786
+ if (c === LITERAL) { i++; continue; }
787
+ if (c === "*" || c === "?" || c === "[" || c === "{" || c === UNKNOWN) return i;
788
+ }
789
+ return -1;
790
+ }
791
+
792
+ function escapeRegex(text) {
793
+ return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
794
+ }
795
+
796
+ // A pattern component as a regular expression, or null when it could match
797
+ // any name (a brace list too large to expand). A run-time value matches any
798
+ // run of characters.
799
+ function componentRegex(component) {
800
+ let out = "";
801
+ for (let i = 0; i < component.length; i++) {
802
+ const c = component[i];
803
+ if (c === LITERAL) {
804
+ i++;
805
+ if (i < component.length) out += escapeRegex(component[i]);
806
+ } else if (c === "{") {
807
+ return null;
808
+ } else if (c === UNKNOWN) {
809
+ out += ".*";
810
+ } else if (c === "*") {
811
+ out += "[^/]*";
812
+ } else if (c === "?") {
813
+ out += "[^/]";
814
+ } else if (c === "[") {
815
+ const close = component.indexOf("]", i + 2);
816
+ if (close < 0) { out += "\\["; continue; }
817
+ const body = plainText(component.slice(i + 1, close)).replace(/^!/, "^").replace(/\\/g, "\\\\");
818
+ out += `[${body}]`;
819
+ i = close;
820
+ } else {
821
+ out += escapeRegex(c);
822
+ }
823
+ }
824
+ return out;
825
+ }
826
+
827
+ function componentMatches(component, name) {
828
+ if (firstSpecial(component) < 0) return plainText(component) === name;
829
+ const source = componentRegex(component);
830
+ if (source === null) return true;
831
+ try {
832
+ return new RegExp(`^${source}$`).test(name);
833
+ } catch {
834
+ return true; // an unparseable pattern could match anything
835
+ }
836
+ }
837
+
838
+ // Does a pattern (components after its literal prefix) name `rel` (the
839
+ // components of a path below that prefix)? `exact` asks for the path itself;
840
+ // otherwise naming a folder above it counts too. Inside the project a `**`
841
+ // walk does not reach a nested instance (shells skip dot-folders, where
842
+ // nested runtime state lives); elsewhere it reaches everything.
843
+ function patternNames(rest, rel, { exact = false, globstar = true } = {}) {
844
+ if (exact && rest.length !== rel.length && !rest.some((part) => part.includes("**"))) return false;
845
+ for (let i = 0; i < rest.length && i < rel.length; i++) {
846
+ if (rest[i].includes("**")) return globstar;
847
+ if (!componentMatches(rest[i], rel[i])) return false;
848
+ // A run-time value can carry further components of its own.
849
+ if (rest[i].includes(UNKNOWN)) return true;
850
+ }
851
+ return exact ? rest.length >= rel.length : true;
852
+ }
853
+
854
+ function relativeParts(prefix, target) {
855
+ return target.slice(prefix === "/" ? 1 : prefix.length + 1).split("/");
856
+ }
857
+
858
+ // The literal directory before a pattern's first wildcard, resolved
859
+ // physically. Each ".." after the wildcard can climb one level above it.
860
+ function wildcardRoot(abs, ctx) {
861
+ const parts = abs.split("/");
862
+ const first = parts.findIndex((part) => firstSpecial(part) >= 0);
863
+ const rest = parts.slice(first);
864
+ let lexicalPrefix = plainText(parts.slice(0, first).join("/")) || "/";
865
+ let prefix = resolved(ctx, lexicalPrefix);
866
+ const climbs = rest.filter((part) => plainText(part) === "..").length;
867
+ for (let k = 0; k < climbs && prefix !== "/"; k++) {
868
+ prefix = path.dirname(prefix);
869
+ lexicalPrefix = path.dirname(lexicalPrefix);
870
+ }
871
+ return { prefix, lexicalPrefix, rest, climbs: climbs > 0 };
872
+ }
873
+
874
+ // The first wildcard component, matched against the prefix's entries: a
875
+ // symlink it matches is followed, so it must not lead into an instance.
876
+ function linkedEntry(dir, component, ctx) {
877
+ if (!component || firstSpecial(component) < 0) return null;
878
+ let entries;
879
+ try {
880
+ entries = fs.readdirSync(dir, { withFileTypes: true });
881
+ } catch {
882
+ return null;
883
+ }
884
+ if (entries.length > DIR_SCAN_LIMIT) return null;
885
+ for (const entry of entries) {
886
+ if (!entry.isSymbolicLink() || !componentMatches(component, entry.name)) continue;
887
+ const verdict = hardCheck(path.join(dir, entry.name), ctx);
888
+ if (verdict) return verdict;
889
+ }
890
+ return null;
891
+ }
892
+
893
+ // A wildcard is judged by its literal prefix. Outside the project's own tree
894
+ // it is denied when that prefix is, or contains, another instance. Inside, it
895
+ // is denied only when it names a nested instance or matches a symlink into
896
+ // an instance.
897
+ function wildcardCheck(abs, ctx) {
898
+ const { prefix, lexicalPrefix, rest, climbs } = wildcardRoot(abs, ctx);
899
+ const hard = hardCheck(lexicalPrefix, ctx) || hardCheck(prefix, ctx);
900
+ if (hard) return hard;
901
+ if (!inOwnTree(prefix, ctx)) {
902
+ const inside = containedDenied(prefix, ctx);
903
+ if (inside) return wildcardBlock(plainText(abs).split(UNKNOWN).join("${…}"), prefix, inside);
904
+ } else {
905
+ for (const denied of ctx.boundary.denied) {
906
+ if (denied === prefix || !within(denied, prefix)) continue;
907
+ if (climbs || patternNames(rest, relativeParts(prefix, denied), { globstar: false })) return hardBlock(denied, denied);
908
+ }
909
+ }
910
+ return climbs ? null : linkedEntry(prefix, rest[0], ctx);
911
+ }
912
+
913
+ function checkCandidate(abs, ctx) {
914
+ return firstSpecial(abs) >= 0 ? wildcardCheck(abs, ctx) : hardCheck(plainText(abs), ctx);
915
+ }
916
+
917
+ // ── Shell text ───────────────────────────────────────────────────────────
918
+
919
+ // Brace lists expanded the way the shell expands them, innermost group first.
920
+ // A brace left over (no comma list, unbalanced) is literal, as in the shell.
921
+ // More than BRACE_LIMIT words returns null: the word stays a pattern, which
922
+ // the prefix rule then judges.
923
+ function braceGroup(text) {
924
+ const open = [];
925
+ for (let i = 0; i < text.length; i++) {
926
+ const c = text[i];
927
+ if (c === LITERAL) { i++; continue; }
928
+ if (c === "{") open.push({ start: i, commas: [] });
929
+ else if (c === "," && open.length) open[open.length - 1].commas.push(i);
930
+ else if (c === "}" && open.length) {
931
+ const group = open.pop();
932
+ if (group.commas.length) return { ...group, end: i };
933
+ }
934
+ }
935
+ return null;
936
+ }
937
+
938
+ function braceExpand(word) {
939
+ if (word.length > 4096) return null;
940
+ const done = [];
941
+ const work = [word];
942
+ let steps = 0;
943
+ while (work.length) {
944
+ const text = work.pop();
945
+ const group = braceGroup(text);
946
+ if (!group) {
947
+ done.push(text);
948
+ continue;
949
+ }
950
+ steps++;
951
+ if (steps > 256 || done.length + work.length + group.commas.length + 1 > BRACE_LIMIT) return null;
952
+ const cuts = [group.start, ...group.commas, group.end];
953
+ for (let k = 0; k + 1 < cuts.length; k++) {
954
+ work.push(text.slice(0, group.start) + text.slice(cuts[k] + 1, cuts[k + 1]) + text.slice(group.end + 1));
955
+ }
956
+ }
957
+ return done.map(literalBraces);
958
+ }
959
+
960
+ function literalBraces(text) {
961
+ let out = "";
962
+ for (let i = 0; i < text.length; i++) {
963
+ if (text[i] === LITERAL) { out += text[i] + (text[i + 1] || ""); i++; continue; }
964
+ out += text[i] === "{" ? LITERAL + "{" : text[i];
965
+ }
966
+ return out;
967
+ }
968
+
969
+ // A linear reading of shell text: simple commands in execution order, each
970
+ // with its words (quotes removed), input files and output targets. Subshells
971
+ // and substitutions are bracketed by push/pop markers so a directory change
972
+ // inside them does not leak out. Heredoc bodies are read as scripts of their
973
+ // own. It reads text; it runs nothing and expands no run-time value.
974
+ function lexShell(source, ctx, depth = 0) {
975
+ const s = String(source).replace(/[\u0000-\u0002]/g, "\uFFFD");
976
+ const items = [];
977
+ const stack = [];
978
+ const docs = [];
979
+ const fresh = () => ({ words: [], inputs: [], redirects: [] });
980
+ let cmd = fresh();
981
+ let word = null;
982
+ let pending = null; // what the next word is: write | read | skip | doc | doc-
983
+ let dq = false;
984
+
985
+ const add = (text) => { word = (word === null ? "" : word) + text; };
986
+ const lit = (char) => (WILD.includes(char) ? LITERAL + char : char);
987
+ const endWord = () => {
988
+ if (word === null) return;
989
+ if (pending === "write") cmd.redirects.push(word);
990
+ else if (pending === "read") cmd.inputs.push(word);
991
+ else if (pending === "doc" || pending === "doc-") docs.push({ delimiter: plainText(word), strip: pending === "doc-" });
992
+ else if (pending !== "skip") cmd.words.push(word);
993
+ pending = null;
994
+ word = null;
995
+ };
996
+ const endCommand = () => {
997
+ endWord();
998
+ pending = null;
999
+ if (cmd.words.length || cmd.inputs.length || cmd.redirects.length) items.push(cmd);
1000
+ cmd = fresh();
1001
+ };
1002
+ const openSubstitution = (type) => {
1003
+ add(UNKNOWN); // the outer word holds a run-time value here
1004
+ stack.push({ type, cmd, word, pending, dq });
1005
+ items.push({ push: true });
1006
+ cmd = fresh();
1007
+ word = null;
1008
+ pending = null;
1009
+ dq = false;
1010
+ };
1011
+ const closeSubstitution = () => {
1012
+ endCommand();
1013
+ const frame = stack.pop();
1014
+ items.push({ pop: true });
1015
+ ({ cmd, word, pending, dq } = frame);
1016
+ };
1017
+ const variable = (name) => {
1018
+ if (name === "HOME") return escapeLiteral(ctx.home);
1019
+ if (name === "PWD") return PWD_MARK;
1020
+ if (name === "CLAUDE_PROJECT_DIR") return escapeLiteral(ctx.projectDir);
1021
+ return UNKNOWN;
1022
+ };
1023
+ const top = () => stack[stack.length - 1];
1024
+ // Redirection operators; returns the index of the operator's last char.
1025
+ const redirect = (i) => {
1026
+ let j = i;
1027
+ let kind = "write";
1028
+ if (s[j] === "<") {
1029
+ if (s[j + 1] === "<") {
1030
+ j++;
1031
+ if (s[j + 1] === "<") { j++; kind = "skip"; } // here-string: a value
1032
+ else if (s[j + 1] === "-") { j++; kind = "doc-"; }
1033
+ else kind = "doc";
1034
+ } else if (s[j + 1] === ">") { j++; kind = "write"; }
1035
+ else if (s[j + 1] === "&") { j++; kind = "dup"; }
1036
+ else kind = "read";
1037
+ } else {
1038
+ if (s[j + 1] === ">") j++;
1039
+ if (s[j + 1] === "|") j++;
1040
+ else if (s[j + 1] === "&") { j++; kind = "dup"; }
1041
+ }
1042
+ if (kind === "dup") {
1043
+ // >&2, <&0, >&- name a descriptor; ">& file" writes the file.
1044
+ if (/[0-9-]/.test(s[j + 1] || "")) {
1045
+ while (j + 1 < s.length && /[0-9-]/.test(s[j + 1])) j++;
1046
+ pending = null;
1047
+ return j;
1048
+ }
1049
+ kind = s[i] === "<" ? "read" : "write";
1050
+ }
1051
+ pending = kind;
1052
+ return j;
1053
+ };
1054
+ // Heredoc bodies start after the newline that ends their command; each is
1055
+ // read as its own script (it often feeds a shell or an interpreter).
1056
+ const heredocs = (newline) => {
1057
+ let i = newline;
1058
+ while (docs.length) {
1059
+ const { delimiter, strip } = docs.shift();
1060
+ let start = i + 1;
1061
+ let end = s.length;
1062
+ let next = s.length;
1063
+ for (let line = start; line < s.length;) {
1064
+ const stop = s.indexOf("\n", line);
1065
+ const lineEnd = stop < 0 ? s.length : stop;
1066
+ const text = s.slice(line, lineEnd);
1067
+ if ((strip ? text.replace(/^\t+/, "") : text) === delimiter) {
1068
+ end = line;
1069
+ next = lineEnd;
1070
+ break;
1071
+ }
1072
+ line = lineEnd + 1;
1073
+ }
1074
+ if (depth < 4) {
1075
+ items.push({ push: true });
1076
+ items.push(...lexShell(s.slice(start, end), ctx, depth + 1));
1077
+ items.push({ pop: true });
1078
+ }
1079
+ i = next;
1080
+ }
1081
+ return i;
1082
+ };
1083
+
1084
+ for (let i = 0; i < s.length; i++) {
1085
+ const c = s[i];
1086
+ if (c === "$") {
1087
+ const n = s[i + 1];
1088
+ if (n === "(") { openSubstitution("subst"); i++; continue; }
1089
+ if (n === "{") {
1090
+ const end = s.indexOf("}", i + 2);
1091
+ add(variable(end < 0 ? "" : s.slice(i + 2, end)));
1092
+ i = end < 0 ? s.length : end;
1093
+ continue;
1094
+ }
1095
+ if (!dq && (n === "'" || n === '"')) continue; // $'…' and $"…": the quote follows
1096
+ if (n && /[A-Za-z_]/.test(n)) {
1097
+ let j = i + 1;
1098
+ while (j < s.length && /[A-Za-z0-9_]/.test(s[j])) j++;
1099
+ add(variable(s.slice(i + 1, j)));
1100
+ i = j - 1;
1101
+ continue;
1102
+ }
1103
+ if (n && /[0-9@*#?$!-]/.test(n)) { add(UNKNOWN); i++; continue; }
1104
+ add("$");
1105
+ continue;
1106
+ }
1107
+ if (c === "`") {
1108
+ if (top() && top().type === "tick") closeSubstitution();
1109
+ else openSubstitution("tick");
1110
+ continue;
1111
+ }
1112
+ if (dq) {
1113
+ if (c === '"') { dq = false; continue; }
1114
+ if (c === "\\" && i + 1 < s.length && '$`"\\\n'.includes(s[i + 1])) {
1115
+ i++;
1116
+ if (s[i] !== "\n") add(lit(s[i]));
1117
+ continue;
1118
+ }
1119
+ add(lit(c));
1120
+ continue;
1121
+ }
1122
+ if (c === "'") {
1123
+ const end = s.indexOf("'", i + 1);
1124
+ add(escapeLiteral(end < 0 ? s.slice(i + 1) : s.slice(i + 1, end)));
1125
+ i = end < 0 ? s.length : end;
1126
+ continue;
1127
+ }
1128
+ if (c === '"') { dq = true; add(""); continue; }
1129
+ if (c === "\\") {
1130
+ if (i + 1 < s.length) {
1131
+ i++;
1132
+ if (s[i] !== "\n") add(lit(s[i]));
1133
+ }
1134
+ continue;
1135
+ }
1136
+ if (c === " " || c === "\t" || c === "\r") { endWord(); continue; }
1137
+ if (c === "#" && word === null) {
1138
+ const newline = s.indexOf("\n", i);
1139
+ i = newline < 0 ? s.length : newline - 1;
1140
+ continue;
1141
+ }
1142
+ if (c === "&" && s[i + 1] === ">") { endWord(); i = redirect(i + 1); continue; }
1143
+ if (c === "\n") {
1144
+ endCommand();
1145
+ if (docs.length) i = heredocs(i);
1146
+ continue;
1147
+ }
1148
+ if (c === ";" || c === "&" || c === "|") { endCommand(); continue; }
1149
+ if ((c === "<" || c === ">") && s[i + 1] === "(") { openSubstitution("subst"); i++; continue; }
1150
+ if (c === "(") { endCommand(); stack.push({ type: "group" }); items.push({ push: true }); continue; }
1151
+ if (c === ")") {
1152
+ const frame = top();
1153
+ if (frame && frame.type === "group") { endCommand(); stack.pop(); items.push({ pop: true }); }
1154
+ else if (frame && frame.type === "subst") closeSubstitution();
1155
+ else endCommand();
1156
+ continue;
1157
+ }
1158
+ if (c === "<" || c === ">") {
1159
+ if (word !== null && /^[0-9]+$/.test(word)) word = null; // a descriptor number
1160
+ endWord();
1161
+ i = redirect(i);
1162
+ continue;
1163
+ }
1164
+ add(c);
1165
+ }
1166
+ while (stack.length) {
1167
+ if (top().type === "group") { endCommand(); stack.pop(); items.push({ pop: true }); }
1168
+ else closeSubstitution();
1169
+ }
1170
+ endCommand();
1171
+ return items;
1172
+ }
1173
+
1174
+ // The program a simple command runs, past assignments, wrappers and shell
1175
+ // keywords, with its arguments.
1176
+ function programOf(words) {
1177
+ let i = 0;
1178
+ while (i < words.length) {
1179
+ const text = plainText(words[i]);
1180
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(text)) { i++; continue; }
1181
+ const name = path.posix.basename(text);
1182
+ if (!WRAPPERS.has(name)) return { prog: name, args: words.slice(i + 1) };
1183
+ i++;
1184
+ while (i < words.length && plainText(words[i]).startsWith("-")) i++;
1185
+ if (name === "timeout" && i < words.length) i++;
1186
+ }
1187
+ return { prog: "", args: [] };
1188
+ }
1189
+
1190
+ // Absolute candidates a word names: the word itself and, for words carrying
1191
+ // code or options, every path-shaped piece of it, each resolved against the
1192
+ // working directory where the word runs (null: unknown, so relative names
1193
+ // are not resolved).
1194
+ function candidates(word, cwd, ctx, wholeOnly = false) {
1195
+ const out = [];
1196
+ for (const text of braceExpand(word) || [word]) {
1197
+ // NAME=value: the value is the candidate, not the assignment.
1198
+ const whole = /^[A-Za-z_][A-Za-z0-9_]*=/.test(plainText(text)) ? [] : [{ piece: text, start: 0 }];
1199
+ let offset = 0;
1200
+ const pieces = wholeOnly ? whole : [...whole, ...text.split(FRAGMENT).map((piece) => {
1201
+ const start = text.indexOf(piece, offset);
1202
+ offset = start + piece.length;
1203
+ return { piece, start };
1204
+ })];
1205
+ for (let { piece, start } of pieces) {
1206
+ if (piece.startsWith("@")) piece = piece.slice(1);
1207
+ if (!piece || piece[0] === UNKNOWN || piece[0] === "-") continue;
1208
+ // Only <scheme>:// introduces a URL authority. An assignment or
1209
+ // option value such as if=//home//user is still an absolute path.
1210
+ if (piece.startsWith("//") && text[start - 1] === ":") {
1211
+ let scheme = start - 2;
1212
+ while (scheme >= 0 && /[A-Za-z0-9+.-]/.test(text[scheme])) scheme--;
1213
+ if (scheme < start - 2) continue;
1214
+ }
1215
+ piece = piece.replace(/\/{2,}/g, "/");
1216
+ if (piece.includes(PWD_MARK)) {
1217
+ if (cwd === null) continue;
1218
+ piece = piece.split(PWD_MARK).join(escapeLiteral(cwd));
1219
+ }
1220
+ if (piece === "~" || piece.startsWith("~/")) piece = escapeLiteral(ctx.home) + piece.slice(1);
1221
+ if (piece.startsWith("/")) out.push(piece);
1222
+ else if (cwd !== null) {
1223
+ const candidate = `${escapeLiteral(cwd)}/${piece}`;
1224
+ // Keep the uncollapsed form too: resolving a logical cwd follows its
1225
+ // current symlinks, including after logical cd .. has diverged from
1226
+ // the physical scan. Ordinary file operands still use kernel '..'.
1227
+ out.push(candidate);
1228
+ if (ctx.logicalCwd && firstSpecial(candidate) < 0) out.push(lexicalPath(candidate));
1229
+ }
1230
+ }
1231
+ }
1232
+ return out;
1233
+ }
1234
+
1235
+ // The directory a literal word names, or null.
1236
+ function literalDir(word, cwd, ctx) {
1237
+ const text = plainText(word);
1238
+ if (text === "-" || firstSpecial(word) >= 0) return null;
1239
+ const [abs] = candidates(word, cwd, ctx, true);
1240
+ return abs ? (ctx.logicalCwd ? lexicalPath(plainText(abs)) : resolved(ctx, plainText(abs))) : null;
1241
+ }
1242
+
1243
+ function nextCwd(cmd, state, ctx) {
1244
+ const { cwd, oldcwd, dirs } = state;
1245
+ const move = (next, stack = dirs) => ({ cwd: next, oldcwd: cwd, dirs: stack });
1246
+ const { prog, args } = programOf(cmd.words);
1247
+ if (prog === "popd") return move(dirs ? dirs.cwd : null, dirs ? dirs.next : null);
1248
+ if (prog !== "cd" && prog !== "pushd") return state;
1249
+ const operands = args.filter((word) => !/^-[A-Za-z@-]+$/.test(plainText(word)));
1250
+ if (prog === "cd" && plainText(operands[0] || "") === "-") return move(oldcwd);
1251
+ if (prog === "pushd" && !operands.length) return move(dirs ? dirs.cwd : null, dirs ? { cwd, next: dirs.next } : null);
1252
+ const stack = prog === "pushd" ? { cwd, next: dirs } : dirs;
1253
+ let physicalCd = false;
1254
+ if (prog === "cd") {
1255
+ for (const word of args) {
1256
+ const flag = plainText(word);
1257
+ if (flag === "--" || !flag.startsWith("-")) break;
1258
+ for (const option of flag.slice(1)) {
1259
+ if (option === "P") physicalCd = true;
1260
+ if (option === "L") physicalCd = false;
1261
+ }
1262
+ }
1263
+ }
1264
+ // Explicit -P resets the logical PWD to the resolved directory. The last
1265
+ // -L/-P wins, matching shell option order; the physical scan stays intact.
1266
+ const nextCtx = ctx.logicalCwd && physicalCd ? { ...ctx, logicalCwd: false } : ctx;
1267
+ if (!operands.length) return move(nextCtx.logicalCwd ? lexicalPath(ctx.home) : resolved(ctx, ctx.home));
1268
+ return move(literalDir(operands[0], cwd, nextCtx), stack);
1269
+ }
1270
+
1271
+ const shortFlag = (flags, letters) => flags.some((flag) => /^-[A-Za-z0-9]+$/.test(flag)
1272
+ && [...letters].some((letter) => flag.slice(1).includes(letter)));
1273
+ const longFlag = (flags, ...names) => flags.some((flag) => names.some((name) => flag === name || flag.startsWith(`${name}=`)));
1274
+ const longPrefix = (flags, prefix) => flags.some((flag) => flag.startsWith(prefix));
1275
+
1276
+ // Does this command walk a tree? { pattern: its first operand is a search
1277
+ // pattern, bypass: it ignores ignore files, leading: only operands before
1278
+ // the first option are roots }.
1279
+ function walkOf(prog, flags) {
1280
+ const walk = (pattern, bypass, leading = false) => ({ pattern, bypass, leading });
1281
+ switch (prog) {
1282
+ case "rg":
1283
+ return walk(true, shortFlag(flags, "u") || longPrefix(flags, "--no-ignore") || longFlag(flags, "--unrestricted"));
1284
+ case "fd": case "fdfind":
1285
+ return walk(true, shortFlag(flags, "uI") || longPrefix(flags, "--no-ignore") || longFlag(flags, "--unrestricted"));
1286
+ case "ag":
1287
+ return walk(true, shortFlag(flags, "uU") || longFlag(flags, "--unrestricted", "--skip-vcs-ignores"));
1288
+ case "ack":
1289
+ return walk(true, true);
1290
+ case "find":
1291
+ return walk(false, true, true);
1292
+ case "tree": case "du": case "tar": case "gtar": case "bsdtar": case "rsync":
1293
+ return walk(false, true);
1294
+ case "grep": case "egrep": case "fgrep": case "ggrep": case "zgrep": {
1295
+ const recurse = flags.some((flag, k) => flag === "-d" && flags[k + 1] === "recurse");
1296
+ return shortFlag(flags, "rR") || recurse
1297
+ || longFlag(flags, "--recursive", "--dereference-recursive", "--directories=recurse") ? walk(true, true) : null;
1298
+ }
1299
+ case "ls": case "gls":
1300
+ return shortFlag(flags, "R") || longFlag(flags, "--recursive") ? walk(false, true) : null;
1301
+ case "cp": case "gcp": case "scp":
1302
+ return shortFlag(flags, "rRa") || longFlag(flags, "--recursive", "--archive") ? walk(false, true) : null;
1303
+ case "rm":
1304
+ return shortFlag(flags, "rR") || longFlag(flags, "--recursive") ? walk(false, true) : null;
1305
+ case "chmod": case "chown": case "chgrp":
1306
+ return shortFlag(flags, "R") ? walk(false, true) : null;
1307
+ case "zip":
1308
+ return shortFlag(flags, "r") ? walk(false, true) : null;
1309
+ default:
1310
+ return null;
1311
+ }
1312
+ }
1313
+
1314
+ function walkVerdict(cmd, cwd, ctx) {
1315
+ const { prog, args } = programOf(cmd.words);
1316
+ const flags = args.map(plainText);
1317
+ const walk = walkOf(prog, flags);
1318
+ if (!walk) return null;
1319
+ const roots = [];
1320
+ let operands = 0;
1321
+ for (let k = 0; k < args.length; k++) {
1322
+ if (flags[k].startsWith("-")) {
1323
+ if (walk.leading) break;
1324
+ continue;
1325
+ }
1326
+ operands++;
1327
+ for (const abs of candidates(args[k], cwd, ctx, true)) {
1328
+ const root = firstSpecial(abs) >= 0 ? wildcardRoot(abs, ctx).prefix : resolved(ctx, plainText(abs));
1329
+ if (isDirectory(root)) roots.push(root);
1330
+ }
1331
+ }
1332
+ // No directory operand: the walk starts from the working directory.
1333
+ if (!roots.length && operands <= (walk.pattern ? 1 : 0) && cwd !== null) roots.push(resolved(ctx, cwd));
1334
+ for (const root of unique(roots)) {
1335
+ const verdict = walkCheck(root, ctx, walk.bypass);
1336
+ if (verdict) return verdict;
1337
+ }
1338
+ return null;
1339
+ }
1340
+
1341
+ // Does this word name protected state? `deep` also counts a folder that holds
1342
+ // it (a delete or a move takes the folder's contents with it).
1343
+ function namesState(word, cwd, ctx, deep, wholeOnly) {
1344
+ for (const abs of candidates(word, cwd, ctx, wholeOnly)) {
1345
+ if (firstSpecial(abs) >= 0) {
1346
+ const { prefix, rest } = wildcardRoot(abs, ctx);
1347
+ for (const file of protectedFiles(ctx)) {
1348
+ if (file !== prefix && within(file, prefix) && patternNames(rest, relativeParts(prefix, file), { exact: !deep })) return file;
1349
+ }
1350
+ continue;
1351
+ }
1352
+ const target = resolved(ctx, plainText(abs));
1353
+ if (isProtected(target, ctx) || (deep && holdsProtected(target, ctx))) return target;
1354
+ }
1355
+ return null;
1356
+ }
1357
+
1358
+ // A copy or move into a folder lands at folder/basename(source).
1359
+ function landsOnState(sources, dest, cwd, ctx) {
1360
+ const dir = literalDir(dest, cwd, ctx);
1361
+ if (!dir || !isDirectory(dir)) return null;
1362
+ for (const source of sources) {
1363
+ const target = path.join(dir, path.posix.basename(plainText(source)));
1364
+ if (isProtected(target, ctx)) return target;
1365
+ }
1366
+ return null;
1367
+ }
1368
+
1369
+ // Changes to the registry and the boundary cache: redirections, deletes,
1370
+ // moves, copies over, in-place edits, and any interpreter naming them.
1371
+ function stateVerdict(cmd, cwd, ctx) {
1372
+ for (const word of cmd.redirects) {
1373
+ const hit = namesState(word, cwd, ctx, false, true);
1374
+ if (hit) return stateBlock(hit);
1375
+ }
1376
+ const { prog, args } = programOf(cmd.words);
1377
+ const flags = args.map(plainText);
1378
+ const operands = args.filter((_, k) => !flags[k].startsWith("-"));
1379
+ const check = (words, deep, wholeOnly = true) => {
1380
+ for (const word of words) {
1381
+ const hit = namesState(word, cwd, ctx, deep, wholeOnly);
1382
+ if (hit) return stateBlock(hit);
1383
+ }
1384
+ return null;
1385
+ };
1386
+ if (DELETERS.has(prog)) return check(operands, true);
1387
+ if (prog === "mv" || COPIERS.has(prog)) {
1388
+ const sources = operands.slice(0, -1);
1389
+ const dest = operands[operands.length - 1];
1390
+ const deepDest = prog === "rsync" && longPrefix(flags, "--delete");
1391
+ const moved = prog === "mv" ? check(sources, true) : null;
1392
+ if (moved) return moved;
1393
+ if (!dest) return null;
1394
+ const over = check([dest], deepDest);
1395
+ if (over) return over;
1396
+ const landed = landsOnState(sources, dest, cwd, ctx);
1397
+ return landed ? stateBlock(landed) : null;
1398
+ }
1399
+ if (OVERWRITERS.has(prog)) return check(operands, false, false);
1400
+ if (prog === "sed" && flags.some((flag) => /^-[A-Za-z]*i/.test(flag) || flag.startsWith("--in-place"))) {
1401
+ return check(operands, false);
1402
+ }
1403
+ if (prog === "find" && flags.some((flag) => /^-(?:delete|exec|execdir|ok|okdir)$/.test(flag))) {
1404
+ const roots = [];
1405
+ for (let k = 0; k < args.length && !flags[k].startsWith("-"); k++) roots.push(args[k]);
1406
+ return check(roots, true);
1407
+ }
1408
+ if (INTERPRETERS.has(prog)) {
1409
+ const registryDir = path.join(ctx.home, ".egregore");
1410
+ for (const word of cmd.words) {
1411
+ const hit = namesState(word, cwd, ctx, false, false);
1412
+ if (hit) return stateBlock(hit);
1413
+ for (const abs of candidates(word, cwd, ctx)) {
1414
+ if (firstSpecial(abs) < 0 && resolved(ctx, plainText(abs)) === resolved(ctx, registryDir)) return stateBlock(registryDir);
1415
+ }
1416
+ }
1417
+ }
1418
+ return null;
1419
+ }
1420
+
1421
+ // Shell text a command runs in turn: `bash -c '…'`, `eval …`.
1422
+ function innerScript(cmd) {
1423
+ const { prog, args } = programOf(cmd.words);
1424
+ const source = (word) => word.split(LITERAL).join("").split(UNKNOWN).join("${__egregore_unknown}").split(PWD_MARK).join("$PWD");
1425
+ if (prog === "eval") return args.map(source).join(" ");
1426
+ if (SHELLS.has(prog)) {
1427
+ const k = args.findIndex((word) => /^-[A-Za-z]*c[A-Za-z]*$/.test(plainText(word)));
1428
+ if (k >= 0 && k + 1 < args.length) return source(args[k + 1]);
1429
+ }
1430
+ return null;
1431
+ }
1432
+
1433
+ function checkCommand(cmd, cwd, ctx, depth) {
1434
+ for (const word of [...cmd.words, ...cmd.inputs, ...cmd.redirects]) {
1435
+ for (const abs of candidates(word, cwd, ctx)) {
1436
+ const verdict = checkCandidate(abs, ctx);
1437
+ if (verdict) return verdict;
1438
+ }
1439
+ }
1440
+ const inner = depth < 3 ? innerScript(cmd) : null;
1441
+ if (inner !== null) {
1442
+ const verdict = checkScript(inner, ctx, cwd, depth + 1);
1443
+ if (verdict) return verdict;
1444
+ }
1445
+ return walkVerdict(cmd, cwd, ctx) || stateVerdict(cmd, cwd, ctx);
1446
+ }
1447
+
1448
+ // Each scan carries one cwd spelling through cd / pushd / popd and restores
1449
+ // it across subshells. A logical scan keeps symlink names until '..' has
1450
+ // collapsed; a physical scan follows the filesystem. Both must pass.
1451
+ function checkScriptItems(items, ctx, baseDir, depth) {
1452
+ // Immutable directory state makes subshell snapshots O(1), including
1453
+ // OLDPWD and the pushd stack. Each scan owns its own history.
1454
+ let state = { cwd: baseDir, oldcwd: null, dirs: null };
1455
+ const frames = [];
1456
+ for (const item of items) {
1457
+ if (item.push) { frames.push(state); continue; }
1458
+ if (item.pop) { if (frames.length) state = frames.pop(); continue; }
1459
+ const verdict = checkCommand(item, state.cwd, ctx, depth);
1460
+ if (verdict) return verdict;
1461
+ const next = nextCwd(item, state, ctx);
1462
+ if (next.cwd !== null && next.cwd !== state.cwd) {
1463
+ const hard = hardCheck(next.cwd, ctx);
1464
+ if (hard) return hard;
1465
+ }
1466
+ state = next;
1467
+ }
1468
+ return null;
1469
+ }
1470
+
1471
+ function checkScript(text, ctx, baseDir, depth = 0) {
1472
+ const items = lexShell(text, ctx);
1473
+ // Nested shell scripts keep their caller's mode, so nested scans do not
1474
+ // branch exponentially. Parse once and run at most two linear scans.
1475
+ if (Object.prototype.hasOwnProperty.call(ctx, "logicalCwd")) return checkScriptItems(items, ctx, baseDir, depth);
1476
+ return checkScriptItems(items, { ...ctx, logicalCwd: false }, baseDir === null ? null : resolved(ctx, baseDir), depth)
1477
+ || checkScriptItems(items, { ...ctx, logicalCwd: true }, baseDir === null ? null : lexicalPath(baseDir), depth);
1478
+ }
1479
+
1480
+ // Another instance's absolute path written anywhere in the text: in code, a
1481
+ // heredoc, a quoted string. Matched on a path boundary so a longer sibling
1482
+ // name sharing the prefix never matches.
1483
+ function literalDenial(command, ctx) {
1484
+ const h = ctx.home;
1485
+ const known = (text) => text.replace(/\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/g, h);
1486
+ const texts = unique([known(command), known(command.replace(/\\(.)/g, "$1").replace(/["']/g, ""))]);
1487
+ for (const denied of ctx.boundary.denied) {
1488
+ if (enclosesProject(denied, ctx)) continue;
1489
+ const pattern = new RegExp(`${escapeRegex(denied)}(?:$|[^A-Za-z0-9._-])`, "m");
1490
+ if (texts.some((text) => pattern.test(text))) return hardBlock(denied, denied);
1491
+ }
1492
+ return null;
1493
+ }
1494
+
1495
+ function softShell(command, ctx, baseDir) {
1496
+ const text = command.replace(/\\(.)/g, "$1").replace(/["']/g, "").replace(/\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/g, ctx.home)
1497
+ .replace(/\/{2,}/g, "/");
1498
+ for (const token of unique(text.match(SOFT_TOKEN) || [])) {
1499
+ if (/[*?[{]/.test(token)) continue;
1500
+ const named = absolute(token, baseDir, ctx.home);
1501
+ const hard = hardCheck(named, ctx);
1502
+ if (hard) return hard;
1503
+ if (ctx.softOff || ctx.boundary.posture === "open" || ctx.relaxed) continue;
1504
+ const target = resolved(ctx, named);
1505
+ if (inCore(target, ctx) || inReadRoots(target, ctx) || isConsented(target, ctx)) continue;
1506
+ return softBlock(target, "shell", ctx);
1507
+ }
1508
+ return null;
1509
+ }
1510
+
1511
+ function checkShell(command, ctx, baseDir) {
1512
+ return literalDenial(command, ctx) || checkScript(command, ctx, baseDir) || softShell(command, ctx, baseDir);
1513
+ }
1514
+
1515
+ // ── Operations ───────────────────────────────────────────────────────────
1516
+
1517
+ function extractApplyPatchPaths(patch) {
1518
+ const paths = [];
1519
+ for (const line of String(patch || "").split(/\r?\n/)) {
1520
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/) || line.match(/^\*\*\* Move to: (.+)$/);
1521
+ if (match && match[1]) paths.push(match[1].trim());
1522
+ }
1523
+ return paths;
1524
+ }
1525
+
1526
+ function textOf(value) {
1527
+ if (Array.isArray(value)) return value.map(String).join(" ");
1528
+ return typeof value === "string" ? value : "";
1529
+ }
1530
+
1531
+ // An argv array (Codex `shell`) as the shell text that runs it: each element
1532
+ // quoted, so `["bash", "-lc", "cd .. && …"]` keeps its script intact.
1533
+ function shellText(value) {
1534
+ if (Array.isArray(value)) return value.map((item) => `'${String(item).replace(/'/g, "'\\''")}'`).join(" ");
1535
+ return typeof value === "string" ? value : "";
1536
+ }
1537
+
1538
+ // Pi accepts "@path" for a file; check the path both ways.
1539
+ function pathForms(value) {
1540
+ if (typeof value !== "string" || !value) return [];
1541
+ return value.startsWith("@") && value.length > 1 ? [value, value.slice(1)] : [value];
1542
+ }
1543
+
1544
+ function pathKey(key) {
1545
+ return PATH_KEY.test(String(key).replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase());
1546
+ }
1547
+
1548
+ // Every string under a path-named key, at any depth: directly, in an array
1549
+ // (paths: [...]), or below it in nested objects (files: [{ path }],
1550
+ // options: { target: { dir } }).
1551
+ function pathFields(value) {
1552
+ const found = [];
1553
+ const seen = new Set();
1554
+ const visit = (item, named) => {
1555
+ if (typeof item === "string") {
1556
+ if (named) found.push(...pathForms(item));
1557
+ return;
1558
+ }
1559
+ if (!item || typeof item !== "object" || seen.has(item)) return;
1560
+ seen.add(item);
1561
+ if (Array.isArray(item)) {
1562
+ for (const entry of item) visit(entry, named);
1563
+ return;
1564
+ }
1565
+ for (const [key, entry] of Object.entries(item)) visit(entry, pathKey(key));
1566
+ };
1567
+ visit(value, false);
1568
+ return found;
1569
+ }
1570
+
1571
+ // One canonical operation from Claude Code, Codex, Pi, or Prime tool input.
1572
+ function normalizeOperation(input, projectDir) {
1573
+ const name = String(input.tool_name || input.toolName || "");
1574
+ let toolInput = input.tool_input ?? input.input ?? {};
1575
+ if (typeof toolInput === "string") toolInput = { command: toolInput };
1576
+ if (!toolInput || typeof toolInput !== "object") toolInput = {};
1577
+ const workdir = [toolInput.workdir, toolInput.cwd, input.cwd].find((value) => typeof value === "string" && value) || ".";
1578
+ // Preserve symlink/.. traversal until the hard tier has inspected BOTH
1579
+ // spellings. path.resolve here would discard the physical destination.
1580
+ const baseDir = absolute(workdir, projectDir, home());
1581
+ if (READ_TOOLS.has(name)) {
1582
+ return { kind: "read", targets: pathForms(name === "Read" ? toolInput.file_path : toolInput.path), baseDir };
1583
+ }
1584
+ if (SEARCH_TOOLS.has(name)) {
1585
+ // Glob (Claude) and find (Pi) take a glob `pattern`; Grep's `pattern` is a regex.
1586
+ const pattern = name === "Glob" || name === "find" ? String(toolInput.pattern || "") : "";
1587
+ return { kind: "search", targets: pathForms(toolInput.path), pattern, baseDir };
1588
+ }
1589
+ if (WRITE_TOOLS.has(name)) {
1590
+ return { kind: "write", targets: pathForms(toolInput.file_path || toolInput.notebook_path || toolInput.path), baseDir };
1591
+ }
1592
+ if (PATCH_TOOLS.has(name)) {
1593
+ const patch = textOf(toolInput.command) || textOf(toolInput.patch) || textOf(toolInput.input);
1594
+ return { kind: "write", targets: extractApplyPatchPaths(patch), baseDir };
1595
+ }
1596
+ if (SHELL_TOOLS.has(name)) {
1597
+ const command = shellText(toolInput.command) || shellText(toolInput.cmd) || textOf(toolInput.code) || textOf(toolInput.chars);
1598
+ return { kind: "shell", command, baseDir };
1599
+ }
1600
+ const known = TOOL_PATHS[name];
1601
+ if (known) {
1602
+ return { kind: known.kind, targets: known.fields.flatMap((field) => pathForms(toolInput[field])), baseDir };
1603
+ }
1604
+ return { kind: "other", targets: pathFields(toolInput), baseDir };
1605
+ }
1606
+
1607
+ // Structured searches honour ignore files: a nested instance inside the
1608
+ // project does not stop them; an instance below a root outside it does.
1609
+ function checkSearch(op, ctx) {
1610
+ for (const target of op.targets) {
1611
+ const verdict = checkPath(target, "read", ctx, op.baseDir);
1612
+ if (verdict) return verdict;
1613
+ }
1614
+ const root = op.targets.length ? resolvePath(op.targets[op.targets.length - 1], op.baseDir) : resolved(ctx, op.baseDir);
1615
+ const walk = walkCheck(root, ctx, false);
1616
+ if (walk) return walk;
1617
+ for (const pattern of op.pattern ? braceExpand(op.pattern) || [op.pattern] : []) {
1618
+ const candidate = absolute(pattern, root, ctx.home);
1619
+ if (firstSpecial(candidate) >= 0) {
1620
+ const verdict = wildcardCheck(candidate, ctx);
1621
+ if (verdict) return verdict;
1622
+ }
1623
+ const prefix = firstSpecial(candidate) >= 0 ? wildcardRoot(candidate, ctx).prefix : resolved(ctx, plainText(candidate));
1624
+ const fromPrefix = hardCheck(firstSpecial(candidate) < 0 ? plainText(candidate) : prefix, ctx)
1625
+ || walkCheck(prefix, ctx, false);
1626
+ if (fromPrefix) return fromPrefix;
1627
+ // An absolute pattern names its own root, which needs consent like a path.
1628
+ if (prefix !== root && !within(prefix, root)) {
1629
+ const soft = checkPath(prefix, "read", ctx, op.baseDir);
1630
+ if (soft) return soft;
1631
+ }
1632
+ }
1633
+ return null;
1634
+ }
1635
+
1636
+ function decideInner(input, projectDir) {
1637
+ if (process.env.EGREGORE_BOUNDARY_TEST_FAULT === "1") throw new Error("injected test fault");
1638
+ const op = normalizeOperation(input, projectDir);
1639
+ const ctx = context(projectDir, input);
1640
+ // The effective working directory is itself an access.
1641
+ const cwd = hardCheck(op.baseDir, ctx);
1642
+ if (cwd) return cwd;
1643
+ if (op.kind === "shell") return checkShell(op.command, ctx, op.baseDir) || allow();
1644
+ if (op.kind === "search") return checkSearch(op, ctx) || allow();
1645
+ for (const target of op.targets) {
1646
+ const verdict = op.kind === "other"
1647
+ ? hardCheck(absolute(target, op.baseDir, ctx.home), ctx)
1648
+ : checkPath(target, op.kind, ctx, op.baseDir);
1649
+ if (verdict) return verdict;
1650
+ }
1651
+ return allow();
1652
+ }
1653
+
1654
+ // An unexpected error must neither open the wall nor block normal work. Known
1655
+ // denials (registry ∪ cache, read again here) are enforced first; then
1656
+ // anything that cannot be shown to sit inside the project, memory, managed
1657
+ // repositories, or system paths is denied, and the rest allowed.
1658
+ function failSafe(input, projectDir, error) {
1659
+ const denial = (subject) => ({
1660
+ outcome: "deny",
1661
+ reason: "internal-error",
1662
+ message: `Environment isolation: the boundary check failed (${error && error.message ? error.message : error}), so ${subject} cannot be verified and access outside this project is denied. Paths inside the project still work. Do not retry and do not attempt via other tools; ask the user to report this.`,
1663
+ });
1664
+ try {
1665
+ const op = normalizeOperation(input, projectDir);
1666
+ let fresh = {};
1667
+ try { fresh = computeBoundary(projectDir); } catch {}
1668
+ const cached = readJson(cachePath(projectDir));
1669
+ const listed = [...strings(fresh.denied_paths), ...(usableCache(cached) ? strings(cached.denied_paths) : [])];
1670
+ let denied = listed;
1671
+ try { denied = deniedForms(listed); } catch {}
1672
+ let realProject = projectDir;
1673
+ try { realProject = fs.realpathSync.native(projectDir); } catch {}
1674
+ const verified = {
1675
+ memoryDir: fresh.memory_dir || "",
1676
+ managedRepos: strings(fresh.managed_repos),
1677
+ mainCheckout: fresh.main_checkout || "",
1678
+ };
1679
+ const registryUnreadable = fresh.registry_unreadable === true
1680
+ || (usableCache(cached) && cached.registry_unreadable === true);
1681
+ const ctx = { projectDir, realProject, home: home(), boundary: { projectDir, denied, registryUnreadable, ...verified, verified } };
1682
+ const h = ctx.home;
1683
+ let targets = op.targets || [];
1684
+ if (op.kind === "search" && op.pattern) {
1685
+ const root = targets.length ? absolute(targets[targets.length - 1], op.baseDir, h) : op.baseDir;
1686
+ for (const pattern of braceExpand(op.pattern) || [op.pattern]) {
1687
+ const candidate = absolute(pattern, root, h);
1688
+ const hard = checkCandidate(candidate, ctx);
1689
+ if (hard) return hard;
1690
+ const prefix = firstSpecial(candidate) >= 0 ? wildcardRoot(candidate, ctx).prefix : physicalPath(candidate);
1691
+ if (!inCore(prefix, ctx, true)) return denial(prefix);
1692
+ const walk = walkCheck(prefix, ctx, false);
1693
+ if (walk) return walk;
1694
+ }
1695
+ }
1696
+ if (op.kind === "shell") {
1697
+ const named = literalDenial(op.command, ctx);
1698
+ if (named) return named;
1699
+ const text = op.command.replace(/\\(.)/g, "$1").replace(/["']/g, "").replace(/\$\{HOME\}|\$HOME(?![A-Za-z0-9_])/g, h);
1700
+ // Retain walk containment and protected-state writes in the error
1701
+ // path too; these use the same hard tier and no consent decisions.
1702
+ const hard = checkScript(op.command, ctx, op.baseDir);
1703
+ if (hard) return hard;
1704
+ // Nothing run-time can be verified here: a variable, substitution,
1705
+ // glob or directory change outside plain project paths is refused.
1706
+ if (/[$`*?[{]|(?:^|[\s;&|(])(?:cd|pushd)\s/.test(text) && /[/~]|\.\./.test(text)) return denial("this command");
1707
+ targets = text.split(/[\s;&|()<>=:,]+/).filter((token) => token.includes("/") || token === ".." || token.startsWith("~"));
1708
+ }
1709
+ for (const candidate of [op.baseDir, ...targets.map((target) => absolute(target, op.baseDir, h))]) {
1710
+ const target = physicalPath(candidate);
1711
+ const entry = deniedEntry(candidate, ctx);
1712
+ if (entry) return hardBlock(target, entry);
1713
+ if (op.kind === "write" && isProtected(target, ctx)) return stateBlock(target);
1714
+ // An internal error only trusts roots verified on disk, regardless of
1715
+ // whether the registry itself is damaged. Keep that failure distinct
1716
+ // from actual registry failure so all adapters give the same reason.
1717
+ if (!inCore(target, ctx, true)) return denial(target);
1718
+ if (op.kind === "search") {
1719
+ const walk = walkCheck(target, ctx, false);
1720
+ if (walk) return walk;
1721
+ }
1722
+ }
1723
+ return allow();
1724
+ } catch {
1725
+ return {
1726
+ outcome: "deny",
1727
+ reason: "internal-error",
1728
+ message: "Environment isolation: the boundary check failed and could not verify this action, so it is denied. Do not retry and do not attempt via other tools; ask the user to report this.",
1729
+ };
1730
+ }
1731
+ }
1732
+
1733
+ function defaultProjectDir() {
1734
+ return process.env.CLAUDE_PROJECT_DIR || path.resolve(__dirname, "..");
1735
+ }
1736
+
1737
+ // Returns { outcome: "allow" | "consent" | "deny", reason, message }.
1738
+ function decide(input, options = {}) {
1739
+ const projectDir = options.projectDir || defaultProjectDir();
1740
+ const payload = input && typeof input === "object" ? input : {};
1741
+ try {
1742
+ return decideInner(payload, projectDir);
1743
+ } catch (error) {
1744
+ return failSafe(payload, projectDir, error);
1745
+ }
1746
+ }
1747
+
1748
+ // Pi and Prime tool_call adapter: block the same way the branch policy does.
1749
+ function toolCallBlock(event, ctx, projectDir) {
1750
+ const verdict = decide(
1751
+ { tool_name: event && event.toolName, tool_input: event && event.input, cwd: ctx && ctx.cwd },
1752
+ { projectDir: projectDir || (ctx && ctx.cwd) || process.cwd() },
1753
+ );
1754
+ return verdict.outcome === "allow" ? undefined : { block: true, reason: verdict.message };
1755
+ }
1756
+
1757
+ function readStdin() {
1758
+ try {
1759
+ return fs.readFileSync(0, "utf8");
1760
+ } catch {
1761
+ return "";
1762
+ }
1763
+ }
1764
+
1765
+ // PreToolUse hook contract shared by Claude Code and Codex: exit 0 allows;
1766
+ // exit 2 with the reason on stderr blocks (deny and consent alike).
1767
+ function main(options = {}) {
1768
+ let input;
1769
+ try {
1770
+ input = JSON.parse(readStdin() || "{}");
1771
+ } catch {
1772
+ input = {};
1773
+ }
1774
+ const verdict = decide(input, options);
1775
+ if (verdict.outcome === "allow") return;
1776
+ process.stderr.write(`${verdict.message}\n`);
1777
+ process.exitCode = 2;
1778
+ }
1779
+
1780
+ function cli(argv) {
1781
+ const [mode, ...rest] = argv;
1782
+ const flag = (name) => {
1783
+ const index = rest.indexOf(name);
1784
+ return index >= 0 ? rest.splice(index, 2)[1] : "";
1785
+ };
1786
+ if (mode === "write-cache") {
1787
+ if (!rest[0]) throw new Error("usage: boundary-guard.cjs write-cache <project_dir>");
1788
+ process.stdout.write(`${writeCache(rest[0])}\n`);
1789
+ return;
1790
+ }
1791
+ if (mode === "native-denials") {
1792
+ if (!rest[0]) throw new Error("usage: boundary-guard.cjs native-denials <project_dir>");
1793
+ process.stdout.write(`${JSON.stringify(nativeDenials(rest[0]))}\n`);
1794
+ return;
1795
+ }
1796
+ if (mode === "compute-denied") {
1797
+ const [registry, projectDir, orgId = "", allowPath = ""] = rest;
1798
+ if (!registry || !projectDir) throw new Error("usage: boundary-guard.cjs compute-denied <registry> <project_dir> [org_id] [allow_path]");
1799
+ const result = computeDenied(registry, projectDir, orgId, allowPath);
1800
+ if (result.unreadable) {
1801
+ process.stderr.write(`boundary-guard: instance registry ${registry} cannot be read\n`);
1802
+ process.exitCode = 1;
1803
+ return;
1804
+ }
1805
+ process.stdout.write(`${JSON.stringify(result.paths, null, 2)}\n`);
1806
+ return;
1807
+ }
1808
+ if (mode === "check") {
1809
+ const projectDir = flag("--project-dir") || defaultProjectDir();
1810
+ if (!rest[0]) throw new Error("usage: boundary-guard.cjs check <path> [--project-dir DIR]");
1811
+ const ctx = context(projectDir, {});
1812
+ const target = absolute(rest[0], projectDir, ctx.home);
1813
+ const entry = deniedEntry(target, ctx);
1814
+ if (entry) {
1815
+ process.stdout.write(entry === "registry"
1816
+ ? `BLOCKED: ${target} cannot be verified (instance registry unreadable)\n`
1817
+ : `${hardBlock(target, entry).message}\n`);
1818
+ process.exitCode = 1;
1819
+ } else if (!inCore(resolved(ctx, target), ctx)) {
1820
+ process.stdout.write(`BLOCKED: ${target} is outside the session boundary\n`);
1821
+ process.exitCode = 1;
1822
+ }
1823
+ return;
1824
+ }
1825
+ const projectDir = mode === "--project-dir" ? rest[0] : "";
1826
+ main(projectDir ? { projectDir } : {});
1827
+ }
1828
+
1829
+ module.exports = {
1830
+ EGREGORE_ALLOWED_FAMILIES,
1831
+ cachePath,
1832
+ computeBoundary,
1833
+ computeDenied,
1834
+ decide,
1835
+ main,
1836
+ nativeDenials,
1837
+ toolCallBlock,
1838
+ writeCache,
1839
+ };
1840
+
1841
+ if (require.main === module) {
1842
+ try {
1843
+ cli(process.argv.slice(2));
1844
+ } catch (error) {
1845
+ process.stderr.write(`${error && error.message ? error.message : error}\n`);
1846
+ process.exitCode = 1;
1847
+ }
1848
+ }