shellbrain 0.1.0__tar.gz

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 (170) hide show
  1. shellbrain-0.1.0/PKG-INFO +130 -0
  2. shellbrain-0.1.0/README.md +116 -0
  3. shellbrain-0.1.0/app/__init__.py +1 -0
  4. shellbrain-0.1.0/app/__main__.py +7 -0
  5. shellbrain-0.1.0/app/boot/__init__.py +1 -0
  6. shellbrain-0.1.0/app/boot/admin_db.py +88 -0
  7. shellbrain-0.1.0/app/boot/config.py +14 -0
  8. shellbrain-0.1.0/app/boot/create_policy.py +52 -0
  9. shellbrain-0.1.0/app/boot/db.py +70 -0
  10. shellbrain-0.1.0/app/boot/embeddings.py +55 -0
  11. shellbrain-0.1.0/app/boot/home.py +45 -0
  12. shellbrain-0.1.0/app/boot/migrations.py +61 -0
  13. shellbrain-0.1.0/app/boot/read_policy.py +179 -0
  14. shellbrain-0.1.0/app/boot/repos.py +15 -0
  15. shellbrain-0.1.0/app/boot/retrieval.py +3 -0
  16. shellbrain-0.1.0/app/boot/thresholds.py +19 -0
  17. shellbrain-0.1.0/app/boot/update_policy.py +34 -0
  18. shellbrain-0.1.0/app/boot/use_cases.py +22 -0
  19. shellbrain-0.1.0/app/config/__init__.py +1 -0
  20. shellbrain-0.1.0/app/config/defaults/create_policy.yaml +7 -0
  21. shellbrain-0.1.0/app/config/defaults/read_policy.yaml +25 -0
  22. shellbrain-0.1.0/app/config/defaults/runtime.yaml +10 -0
  23. shellbrain-0.1.0/app/config/defaults/thresholds.yaml +3 -0
  24. shellbrain-0.1.0/app/config/defaults/update_policy.yaml +5 -0
  25. shellbrain-0.1.0/app/config/loader.py +58 -0
  26. shellbrain-0.1.0/app/core/__init__.py +1 -0
  27. shellbrain-0.1.0/app/core/contracts/__init__.py +1 -0
  28. shellbrain-0.1.0/app/core/contracts/errors.py +29 -0
  29. shellbrain-0.1.0/app/core/contracts/requests.py +211 -0
  30. shellbrain-0.1.0/app/core/contracts/responses.py +15 -0
  31. shellbrain-0.1.0/app/core/entities/__init__.py +1 -0
  32. shellbrain-0.1.0/app/core/entities/associations.py +58 -0
  33. shellbrain-0.1.0/app/core/entities/episodes.py +66 -0
  34. shellbrain-0.1.0/app/core/entities/evidence.py +29 -0
  35. shellbrain-0.1.0/app/core/entities/facts.py +30 -0
  36. shellbrain-0.1.0/app/core/entities/guidance.py +47 -0
  37. shellbrain-0.1.0/app/core/entities/identity.py +48 -0
  38. shellbrain-0.1.0/app/core/entities/memory.py +34 -0
  39. shellbrain-0.1.0/app/core/entities/runtime_context.py +19 -0
  40. shellbrain-0.1.0/app/core/entities/session_state.py +31 -0
  41. shellbrain-0.1.0/app/core/entities/telemetry.py +152 -0
  42. shellbrain-0.1.0/app/core/entities/utility.py +14 -0
  43. shellbrain-0.1.0/app/core/interfaces/__init__.py +1 -0
  44. shellbrain-0.1.0/app/core/interfaces/clock.py +12 -0
  45. shellbrain-0.1.0/app/core/interfaces/config.py +28 -0
  46. shellbrain-0.1.0/app/core/interfaces/embeddings.py +12 -0
  47. shellbrain-0.1.0/app/core/interfaces/idgen.py +11 -0
  48. shellbrain-0.1.0/app/core/interfaces/repos.py +279 -0
  49. shellbrain-0.1.0/app/core/interfaces/retrieval.py +20 -0
  50. shellbrain-0.1.0/app/core/interfaces/session_state_store.py +33 -0
  51. shellbrain-0.1.0/app/core/interfaces/unit_of_work.py +50 -0
  52. shellbrain-0.1.0/app/core/policies/__init__.py +1 -0
  53. shellbrain-0.1.0/app/core/policies/_shared/__init__.py +1 -0
  54. shellbrain-0.1.0/app/core/policies/_shared/executor.py +132 -0
  55. shellbrain-0.1.0/app/core/policies/_shared/side_effects.py +9 -0
  56. shellbrain-0.1.0/app/core/policies/create_policy/__init__.py +1 -0
  57. shellbrain-0.1.0/app/core/policies/create_policy/pipeline.py +96 -0
  58. shellbrain-0.1.0/app/core/policies/read_policy/__init__.py +1 -0
  59. shellbrain-0.1.0/app/core/policies/read_policy/bm25.py +114 -0
  60. shellbrain-0.1.0/app/core/policies/read_policy/context_pack_builder.py +140 -0
  61. shellbrain-0.1.0/app/core/policies/read_policy/expansion.py +132 -0
  62. shellbrain-0.1.0/app/core/policies/read_policy/fusion_rrf.py +34 -0
  63. shellbrain-0.1.0/app/core/policies/read_policy/lexical_query.py +101 -0
  64. shellbrain-0.1.0/app/core/policies/read_policy/pipeline.py +93 -0
  65. shellbrain-0.1.0/app/core/policies/read_policy/scenario_lift.py +11 -0
  66. shellbrain-0.1.0/app/core/policies/read_policy/scoring.py +61 -0
  67. shellbrain-0.1.0/app/core/policies/read_policy/seed_retrieval.py +54 -0
  68. shellbrain-0.1.0/app/core/policies/read_policy/utility_prior.py +11 -0
  69. shellbrain-0.1.0/app/core/policies/update_policy/__init__.py +1 -0
  70. shellbrain-0.1.0/app/core/policies/update_policy/pipeline.py +80 -0
  71. shellbrain-0.1.0/app/core/use_cases/__init__.py +1 -0
  72. shellbrain-0.1.0/app/core/use_cases/build_guidance.py +85 -0
  73. shellbrain-0.1.0/app/core/use_cases/create_memory.py +26 -0
  74. shellbrain-0.1.0/app/core/use_cases/manage_session_state.py +159 -0
  75. shellbrain-0.1.0/app/core/use_cases/read_memory.py +21 -0
  76. shellbrain-0.1.0/app/core/use_cases/record_episode_sync_telemetry.py +19 -0
  77. shellbrain-0.1.0/app/core/use_cases/record_operation_telemetry.py +32 -0
  78. shellbrain-0.1.0/app/core/use_cases/sync_episode.py +162 -0
  79. shellbrain-0.1.0/app/core/use_cases/update_memory.py +40 -0
  80. shellbrain-0.1.0/app/migrations/__init__.py +1 -0
  81. shellbrain-0.1.0/app/migrations/env.py +65 -0
  82. shellbrain-0.1.0/app/migrations/versions/20260226_0001_initial_schema.py +232 -0
  83. shellbrain-0.1.0/app/migrations/versions/20260312_0002_add_hard_invariants.py +60 -0
  84. shellbrain-0.1.0/app/migrations/versions/20260312_0003_drop_create_confidence.py +40 -0
  85. shellbrain-0.1.0/app/migrations/versions/20260313_0004_episode_sync_hardening.py +71 -0
  86. shellbrain-0.1.0/app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +45 -0
  87. shellbrain-0.1.0/app/migrations/versions/20260318_0006_usage_telemetry_schema.py +175 -0
  88. shellbrain-0.1.0/app/migrations/versions/20260319_0007_identity_session_guidance.py +49 -0
  89. shellbrain-0.1.0/app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +31 -0
  90. shellbrain-0.1.0/app/migrations/versions/__init__.py +1 -0
  91. shellbrain-0.1.0/app/periphery/__init__.py +1 -0
  92. shellbrain-0.1.0/app/periphery/admin/__init__.py +1 -0
  93. shellbrain-0.1.0/app/periphery/admin/backup.py +360 -0
  94. shellbrain-0.1.0/app/periphery/admin/destructive_guard.py +32 -0
  95. shellbrain-0.1.0/app/periphery/admin/doctor.py +192 -0
  96. shellbrain-0.1.0/app/periphery/admin/init.py +996 -0
  97. shellbrain-0.1.0/app/periphery/admin/instance_guard.py +211 -0
  98. shellbrain-0.1.0/app/periphery/admin/machine_state.py +354 -0
  99. shellbrain-0.1.0/app/periphery/admin/privileges.py +42 -0
  100. shellbrain-0.1.0/app/periphery/admin/repo_state.py +266 -0
  101. shellbrain-0.1.0/app/periphery/admin/restore.py +30 -0
  102. shellbrain-0.1.0/app/periphery/cli/__init__.py +1 -0
  103. shellbrain-0.1.0/app/periphery/cli/handlers.py +830 -0
  104. shellbrain-0.1.0/app/periphery/cli/hydration.py +119 -0
  105. shellbrain-0.1.0/app/periphery/cli/main.py +710 -0
  106. shellbrain-0.1.0/app/periphery/cli/presenter_json.py +10 -0
  107. shellbrain-0.1.0/app/periphery/cli/schema_validation.py +201 -0
  108. shellbrain-0.1.0/app/periphery/db/__init__.py +1 -0
  109. shellbrain-0.1.0/app/periphery/db/engine.py +10 -0
  110. shellbrain-0.1.0/app/periphery/db/models/__init__.py +1 -0
  111. shellbrain-0.1.0/app/periphery/db/models/associations.py +55 -0
  112. shellbrain-0.1.0/app/periphery/db/models/episodes.py +55 -0
  113. shellbrain-0.1.0/app/periphery/db/models/evidence.py +19 -0
  114. shellbrain-0.1.0/app/periphery/db/models/experiences.py +33 -0
  115. shellbrain-0.1.0/app/periphery/db/models/instance_metadata.py +17 -0
  116. shellbrain-0.1.0/app/periphery/db/models/memories.py +39 -0
  117. shellbrain-0.1.0/app/periphery/db/models/metadata.py +6 -0
  118. shellbrain-0.1.0/app/periphery/db/models/registry.py +18 -0
  119. shellbrain-0.1.0/app/periphery/db/models/telemetry.py +174 -0
  120. shellbrain-0.1.0/app/periphery/db/models/utility.py +19 -0
  121. shellbrain-0.1.0/app/periphery/db/models/views.py +154 -0
  122. shellbrain-0.1.0/app/periphery/db/repos/__init__.py +1 -0
  123. shellbrain-0.1.0/app/periphery/db/repos/relational/__init__.py +1 -0
  124. shellbrain-0.1.0/app/periphery/db/repos/relational/associations_repo.py +117 -0
  125. shellbrain-0.1.0/app/periphery/db/repos/relational/episodes_repo.py +188 -0
  126. shellbrain-0.1.0/app/periphery/db/repos/relational/evidence_repo.py +82 -0
  127. shellbrain-0.1.0/app/periphery/db/repos/relational/experiences_repo.py +41 -0
  128. shellbrain-0.1.0/app/periphery/db/repos/relational/memories_repo.py +99 -0
  129. shellbrain-0.1.0/app/periphery/db/repos/relational/read_policy_repo.py +202 -0
  130. shellbrain-0.1.0/app/periphery/db/repos/relational/telemetry_repo.py +161 -0
  131. shellbrain-0.1.0/app/periphery/db/repos/relational/utility_repo.py +30 -0
  132. shellbrain-0.1.0/app/periphery/db/repos/semantic/__init__.py +1 -0
  133. shellbrain-0.1.0/app/periphery/db/repos/semantic/keyword_retrieval_repo.py +63 -0
  134. shellbrain-0.1.0/app/periphery/db/repos/semantic/semantic_retrieval_repo.py +111 -0
  135. shellbrain-0.1.0/app/periphery/db/session.py +10 -0
  136. shellbrain-0.1.0/app/periphery/db/uow.py +75 -0
  137. shellbrain-0.1.0/app/periphery/embeddings/__init__.py +1 -0
  138. shellbrain-0.1.0/app/periphery/embeddings/local_provider.py +35 -0
  139. shellbrain-0.1.0/app/periphery/embeddings/query_vector_search.py +18 -0
  140. shellbrain-0.1.0/app/periphery/episodes/__init__.py +1 -0
  141. shellbrain-0.1.0/app/periphery/episodes/claude_code.py +387 -0
  142. shellbrain-0.1.0/app/periphery/episodes/codex.py +423 -0
  143. shellbrain-0.1.0/app/periphery/episodes/launcher.py +66 -0
  144. shellbrain-0.1.0/app/periphery/episodes/normalization.py +31 -0
  145. shellbrain-0.1.0/app/periphery/episodes/poller.py +299 -0
  146. shellbrain-0.1.0/app/periphery/episodes/source_discovery.py +66 -0
  147. shellbrain-0.1.0/app/periphery/episodes/tool_filter.py +165 -0
  148. shellbrain-0.1.0/app/periphery/identity/__init__.py +1 -0
  149. shellbrain-0.1.0/app/periphery/identity/claude_hook_install.py +67 -0
  150. shellbrain-0.1.0/app/periphery/identity/claude_runtime.py +83 -0
  151. shellbrain-0.1.0/app/periphery/identity/codex_runtime.py +32 -0
  152. shellbrain-0.1.0/app/periphery/identity/compatibility.py +38 -0
  153. shellbrain-0.1.0/app/periphery/identity/resolver.py +163 -0
  154. shellbrain-0.1.0/app/periphery/session_state/__init__.py +1 -0
  155. shellbrain-0.1.0/app/periphery/session_state/file_store.py +100 -0
  156. shellbrain-0.1.0/app/periphery/telemetry/__init__.py +33 -0
  157. shellbrain-0.1.0/app/periphery/telemetry/operation_summary.py +299 -0
  158. shellbrain-0.1.0/app/periphery/telemetry/session_selection.py +156 -0
  159. shellbrain-0.1.0/app/periphery/telemetry/sync_summary.py +65 -0
  160. shellbrain-0.1.0/app/periphery/validation/__init__.py +1 -0
  161. shellbrain-0.1.0/app/periphery/validation/integrity_validation.py +253 -0
  162. shellbrain-0.1.0/app/periphery/validation/semantic_validation.py +94 -0
  163. shellbrain-0.1.0/pyproject.toml +33 -0
  164. shellbrain-0.1.0/setup.cfg +4 -0
  165. shellbrain-0.1.0/shellbrain.egg-info/PKG-INFO +130 -0
  166. shellbrain-0.1.0/shellbrain.egg-info/SOURCES.txt +168 -0
  167. shellbrain-0.1.0/shellbrain.egg-info/dependency_links.txt +1 -0
  168. shellbrain-0.1.0/shellbrain.egg-info/entry_points.txt +2 -0
  169. shellbrain-0.1.0/shellbrain.egg-info/requires.txt +7 -0
  170. shellbrain-0.1.0/shellbrain.egg-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellbrain
3
+ Version: 0.1.0
4
+ Summary: Repo-scoped Shellbrain CLI with explicit evidence-backed writes.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: SQLAlchemy<3.0,>=2.0
8
+ Requires-Dist: alembic<2.0,>=1.13
9
+ Requires-Dist: pydantic<3.0,>=2.7
10
+ Requires-Dist: PyYAML<7.0,>=6.0
11
+ Requires-Dist: psycopg[binary]<4.0,>=3.1
12
+ Requires-Dist: pgvector<1.0,>=0.3
13
+ Requires-Dist: sentence-transformers<4.0,>=3.0
14
+
15
+ # Building a Brain
16
+
17
+ `shellbrain` is a repo-scoped long-term context system for agent sessions.
18
+
19
+ Think of it as a case-based memory system with two layers:
20
+
21
+ - durable memories:
22
+ - `problem`
23
+ - `solution`
24
+ - `failed_tactic`
25
+ - `fact`
26
+ - `preference`
27
+ - `change`
28
+ - episodic evidence:
29
+ - transcript-derived `episode_event` records that ground writes
30
+
31
+ It exposes four agent-facing operations:
32
+
33
+ - `read` recalls durable memories related to the current problem or subproblem
34
+ - `events` inspects recent episodic evidence
35
+ - `create` adds new durable Shellbrain entries with explicit `evidence_refs`
36
+ - `update` records utility, links, and lifecycle changes on existing entries
37
+
38
+ ## Install Once Per Machine
39
+
40
+ Treat `shellbrain` as a machine-level CLI, not a per-repo dependency. The normal product path is a one-time global install, then `shellbrain init` from any repo you want to register.
41
+
42
+ Preferred install with `pipx`:
43
+
44
+ ```bash
45
+ pipx install shellbrain
46
+ ```
47
+
48
+ Secondary install path:
49
+
50
+ ```bash
51
+ python3 -m pip install shellbrain
52
+ ```
53
+
54
+ Editable installs remain a development/operator path and are intentionally omitted from the normal user-facing flow.
55
+
56
+ ## Bootstrap
57
+
58
+ From the repo you are working in:
59
+
60
+ ```bash
61
+ shellbrain init
62
+ shellbrain admin doctor
63
+ ```
64
+
65
+ `shellbrain init` is the normal bootstrap and repair path. In the managed-local happy path it owns Docker, Postgres provisioning, migrations, grants, embedding prewarm, repo registration, and Claude integration when eligible.
66
+
67
+ **Claude integration is conservative.** Shellbrain installs the repo-local Claude hook automatically only when the repo looks Claude-managed *and* `init` is running with a real Claude runtime signal. Otherwise it does nothing unless you pass `--host claude`.
68
+
69
+ When running Shellbrain from Codex Desktop or a similar tool shell, if direct calls fail in the current session, retry through a login shell first:
70
+
71
+ ```bash
72
+ zsh -lc 'source ~/.zprofile >/dev/null 2>&1; shellbrain --help'
73
+ ```
74
+
75
+ Then use the same wrapper shape for actual invocations if needed:
76
+
77
+ ```bash
78
+ zsh -lc "source ~/.zprofile >/dev/null 2>&1; shellbrain read --json '{\"query\":\"Have we seen this migration lock timeout before?\",\"kinds\":[\"problem\",\"solution\",\"failed_tactic\"]}'"
79
+ ```
80
+
81
+ If `doctor` reports `repair_needed`, rerun `shellbrain init`.
82
+
83
+ ## Typical Workflow
84
+
85
+ 1. Start with focused retrieval queries about the concrete problem, subsystem, constraint, or decision you are working on. Do not start with vague prompts like "what should I know about this repo?"
86
+ 2. Use `read` again during the task whenever the search shifts or a memory might become useful midway through the work.
87
+ 3. Before every evidence-bearing write, run `shellbrain events --json '{"limit":10}'` so you can inspect concrete `episode_event` ids.
88
+ 4. At session end, normalize what happened into durable memories:
89
+ - the `problem`
90
+ - each `failed_tactic`
91
+ - the `solution`
92
+ - any durable `fact`, `preference`, or `change`
93
+ - `utility_vote` updates for memories that helped or misled, using a `-1.0` to `1.0` scale where negative votes mean unhelpful and positive votes mean helpful
94
+
95
+ Never invent `evidence_refs`. If `events` returns nothing useful or the evidence is ambiguous, skip the write and try again later.
96
+
97
+ Use `--repo-root` when your current working directory is not the repo you want to target.
98
+
99
+ **Repo identity is remote-first.** Shellbrain prefers the normalized `origin` fetch URL. If `origin` is absent but there is exactly one remote, it uses that. If there are multiple remotes and none is `origin`, `init` stops and asks for `--repo-id`. If there is no usable remote, Shellbrain falls back to a weak-local identity tied to the current path.
100
+
101
+ ## Backups and Recovery
102
+
103
+ Shellbrain exposes first-class logical backups:
104
+
105
+ ```bash
106
+ shellbrain admin backup create
107
+ shellbrain admin backup list
108
+ shellbrain admin backup verify
109
+ shellbrain admin backup restore --target-db shellbrain_restore_001
110
+ shellbrain admin doctor
111
+ ```
112
+
113
+ Backups default to `$SHELLBRAIN_HOME/backups`, which is `~/.shellbrain/backups` unless `SHELLBRAIN_HOME` is set. The Docker bind-mounted Postgres data dir protects against container loss, but it is not a backup strategy by itself.
114
+
115
+ ## Advanced / Operator Notes
116
+
117
+ The normal product path should not require users to think about:
118
+
119
+ - raw DSNs
120
+ - manual `docker compose up`
121
+ - manual `shellbrain admin migrate`
122
+ - editable installs
123
+ - manual Claude hook edits
124
+
125
+ Those topics belong in the advanced/operator guide: [`docs/external-quickstart.md`](docs/external-quickstart.md)
126
+
127
+ ## More
128
+
129
+ - Advanced/operator guide: [`docs/external-quickstart.md`](docs/external-quickstart.md)
130
+ - Session-start skill: [`skills/shellbrain-session-start/SKILL.md`](skills/shellbrain-session-start/SKILL.md)
@@ -0,0 +1,116 @@
1
+ # Building a Brain
2
+
3
+ `shellbrain` is a repo-scoped long-term context system for agent sessions.
4
+
5
+ Think of it as a case-based memory system with two layers:
6
+
7
+ - durable memories:
8
+ - `problem`
9
+ - `solution`
10
+ - `failed_tactic`
11
+ - `fact`
12
+ - `preference`
13
+ - `change`
14
+ - episodic evidence:
15
+ - transcript-derived `episode_event` records that ground writes
16
+
17
+ It exposes four agent-facing operations:
18
+
19
+ - `read` recalls durable memories related to the current problem or subproblem
20
+ - `events` inspects recent episodic evidence
21
+ - `create` adds new durable Shellbrain entries with explicit `evidence_refs`
22
+ - `update` records utility, links, and lifecycle changes on existing entries
23
+
24
+ ## Install Once Per Machine
25
+
26
+ Treat `shellbrain` as a machine-level CLI, not a per-repo dependency. The normal product path is a one-time global install, then `shellbrain init` from any repo you want to register.
27
+
28
+ Preferred install with `pipx`:
29
+
30
+ ```bash
31
+ pipx install shellbrain
32
+ ```
33
+
34
+ Secondary install path:
35
+
36
+ ```bash
37
+ python3 -m pip install shellbrain
38
+ ```
39
+
40
+ Editable installs remain a development/operator path and are intentionally omitted from the normal user-facing flow.
41
+
42
+ ## Bootstrap
43
+
44
+ From the repo you are working in:
45
+
46
+ ```bash
47
+ shellbrain init
48
+ shellbrain admin doctor
49
+ ```
50
+
51
+ `shellbrain init` is the normal bootstrap and repair path. In the managed-local happy path it owns Docker, Postgres provisioning, migrations, grants, embedding prewarm, repo registration, and Claude integration when eligible.
52
+
53
+ **Claude integration is conservative.** Shellbrain installs the repo-local Claude hook automatically only when the repo looks Claude-managed *and* `init` is running with a real Claude runtime signal. Otherwise it does nothing unless you pass `--host claude`.
54
+
55
+ When running Shellbrain from Codex Desktop or a similar tool shell, if direct calls fail in the current session, retry through a login shell first:
56
+
57
+ ```bash
58
+ zsh -lc 'source ~/.zprofile >/dev/null 2>&1; shellbrain --help'
59
+ ```
60
+
61
+ Then use the same wrapper shape for actual invocations if needed:
62
+
63
+ ```bash
64
+ zsh -lc "source ~/.zprofile >/dev/null 2>&1; shellbrain read --json '{\"query\":\"Have we seen this migration lock timeout before?\",\"kinds\":[\"problem\",\"solution\",\"failed_tactic\"]}'"
65
+ ```
66
+
67
+ If `doctor` reports `repair_needed`, rerun `shellbrain init`.
68
+
69
+ ## Typical Workflow
70
+
71
+ 1. Start with focused retrieval queries about the concrete problem, subsystem, constraint, or decision you are working on. Do not start with vague prompts like "what should I know about this repo?"
72
+ 2. Use `read` again during the task whenever the search shifts or a memory might become useful midway through the work.
73
+ 3. Before every evidence-bearing write, run `shellbrain events --json '{"limit":10}'` so you can inspect concrete `episode_event` ids.
74
+ 4. At session end, normalize what happened into durable memories:
75
+ - the `problem`
76
+ - each `failed_tactic`
77
+ - the `solution`
78
+ - any durable `fact`, `preference`, or `change`
79
+ - `utility_vote` updates for memories that helped or misled, using a `-1.0` to `1.0` scale where negative votes mean unhelpful and positive votes mean helpful
80
+
81
+ Never invent `evidence_refs`. If `events` returns nothing useful or the evidence is ambiguous, skip the write and try again later.
82
+
83
+ Use `--repo-root` when your current working directory is not the repo you want to target.
84
+
85
+ **Repo identity is remote-first.** Shellbrain prefers the normalized `origin` fetch URL. If `origin` is absent but there is exactly one remote, it uses that. If there are multiple remotes and none is `origin`, `init` stops and asks for `--repo-id`. If there is no usable remote, Shellbrain falls back to a weak-local identity tied to the current path.
86
+
87
+ ## Backups and Recovery
88
+
89
+ Shellbrain exposes first-class logical backups:
90
+
91
+ ```bash
92
+ shellbrain admin backup create
93
+ shellbrain admin backup list
94
+ shellbrain admin backup verify
95
+ shellbrain admin backup restore --target-db shellbrain_restore_001
96
+ shellbrain admin doctor
97
+ ```
98
+
99
+ Backups default to `$SHELLBRAIN_HOME/backups`, which is `~/.shellbrain/backups` unless `SHELLBRAIN_HOME` is set. The Docker bind-mounted Postgres data dir protects against container loss, but it is not a backup strategy by itself.
100
+
101
+ ## Advanced / Operator Notes
102
+
103
+ The normal product path should not require users to think about:
104
+
105
+ - raw DSNs
106
+ - manual `docker compose up`
107
+ - manual `shellbrain admin migrate`
108
+ - editable installs
109
+ - manual Claude hook edits
110
+
111
+ Those topics belong in the advanced/operator guide: [`docs/external-quickstart.md`](docs/external-quickstart.md)
112
+
113
+ ## More
114
+
115
+ - Advanced/operator guide: [`docs/external-quickstart.md`](docs/external-quickstart.md)
116
+ - Session-start skill: [`skills/shellbrain-session-start/SKILL.md`](skills/shellbrain-session-start/SKILL.md)
@@ -0,0 +1 @@
1
+ """This package contains the shellbrain system application code."""
@@ -0,0 +1,7 @@
1
+ """Allow `python -m app` to invoke the public CLI."""
2
+
3
+ from app.periphery.cli.main import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """This package contains factory functions that wire core to periphery."""
@@ -0,0 +1,88 @@
1
+ """Boot helpers for privileged admin database actions and safety settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from app.boot.config import get_config_provider
9
+ from app.boot.home import get_machine_backups_dir
10
+ from app.periphery.admin.machine_state import try_load_machine_config
11
+
12
+
13
+ def get_admin_db_dsn() -> str:
14
+ """Resolve the privileged admin DSN from environment-backed runtime config."""
15
+
16
+ machine_config, machine_error = try_load_machine_config()
17
+ if machine_error:
18
+ raise RuntimeError(
19
+ "Shellbrain machine config is unreadable. Rerun `shellbrain init` to repair it."
20
+ )
21
+ if machine_config is not None:
22
+ return machine_config.database.admin_dsn
23
+
24
+ runtime = get_config_provider().get_runtime()
25
+ database = runtime.get("database")
26
+ if not isinstance(database, dict):
27
+ raise RuntimeError("runtime.database must be configured")
28
+ admin_dsn_env = database.get("admin_dsn_env")
29
+ if not isinstance(admin_dsn_env, str) or not admin_dsn_env:
30
+ raise RuntimeError("runtime.database.admin_dsn_env must be configured")
31
+ dsn = os.getenv(admin_dsn_env)
32
+ if not dsn:
33
+ raise RuntimeError(f"{admin_dsn_env} is not set")
34
+ return dsn
35
+
36
+
37
+ def get_optional_admin_db_dsn() -> str | None:
38
+ """Resolve the privileged admin DSN when present, otherwise return None."""
39
+
40
+ machine_config, machine_error = try_load_machine_config()
41
+ if machine_error:
42
+ return None
43
+ if machine_config is not None:
44
+ return machine_config.database.admin_dsn
45
+
46
+ runtime = get_config_provider().get_runtime()
47
+ database = runtime.get("database")
48
+ if not isinstance(database, dict):
49
+ return None
50
+ admin_dsn_env = database.get("admin_dsn_env")
51
+ if not isinstance(admin_dsn_env, str) or not admin_dsn_env:
52
+ return None
53
+ return os.getenv(admin_dsn_env)
54
+
55
+
56
+ def get_backup_dir() -> Path:
57
+ """Resolve the on-disk backup directory, defaulting outside the repo tree."""
58
+
59
+ machine_config, machine_error = try_load_machine_config()
60
+ if machine_error:
61
+ return get_machine_backups_dir()
62
+ if machine_config is not None:
63
+ return Path(machine_config.backups.root).expanduser().resolve()
64
+ return Path(os.getenv("SHELLBRAIN_BACKUP_DIR", str(get_machine_backups_dir()))).expanduser().resolve()
65
+
66
+
67
+ def get_backup_mirror_dir() -> Path | None:
68
+ """Resolve the optional mirrored backup directory."""
69
+
70
+ configured = os.getenv("SHELLBRAIN_BACKUP_MIRROR_DIR")
71
+ if not configured:
72
+ return None
73
+ return Path(configured).expanduser().resolve()
74
+
75
+
76
+ def should_fail_on_unsafe_app_role() -> bool:
77
+ """Return whether app commands should fail instead of warning on unsafe DB roles."""
78
+
79
+ configured = os.getenv("SHELLBRAIN_FAIL_ON_UNSAFE_DB_ROLE")
80
+ if configured is None or not configured.strip():
81
+ return True
82
+ return configured.strip().lower() not in {"0", "false", "no", "off"}
83
+
84
+
85
+ def get_instance_mode_default() -> str:
86
+ """Resolve the default instance mode used when stamping metadata for the current DB."""
87
+
88
+ return os.getenv("SHELLBRAIN_INSTANCE_MODE", "live").strip().lower() or "live"
@@ -0,0 +1,14 @@
1
+ """This module defines boot-time helpers that load YAML-backed configuration providers."""
2
+
3
+ from functools import lru_cache
4
+ from pathlib import Path
5
+
6
+ from app.config.loader import YamlConfigProvider
7
+
8
+
9
+ @lru_cache(maxsize=1)
10
+ def get_config_provider() -> YamlConfigProvider:
11
+ """This function returns the shared YAML configuration provider instance."""
12
+
13
+ defaults_dir = Path(__file__).resolve().parents[1] / "config" / "defaults"
14
+ return YamlConfigProvider(defaults_dir)
@@ -0,0 +1,52 @@
1
+ """Boot-time helpers for normalized create-policy settings."""
2
+
3
+ from typing import Any
4
+
5
+ from app.boot.config import get_config_provider
6
+ from app.core.contracts.errors import ErrorCode, ErrorDetail
7
+
8
+
9
+ _SUPPORTED_GATES = ("schema", "semantic", "integrity")
10
+ _SUPPORTED_SCOPES = ("repo", "global")
11
+
12
+
13
+ def get_create_policy_settings() -> dict[str, Any]:
14
+ """Return normalized create-policy settings from YAML config."""
15
+
16
+ policy = get_config_provider().get_create_policy()
17
+ configured_gates = policy.get("gates")
18
+ if not isinstance(configured_gates, list) or not configured_gates:
19
+ raise ValueError("create_policy.gates must be a non-empty list")
20
+ gates = [str(gate) for gate in configured_gates if str(gate) in _SUPPORTED_GATES]
21
+ if len(gates) != len(configured_gates):
22
+ raise ValueError("create_policy.gates contains unsupported values")
23
+ if "schema" not in gates:
24
+ raise ValueError("create_policy.gates must include schema")
25
+ configured_defaults = policy.get("defaults")
26
+ if not isinstance(configured_defaults, dict):
27
+ raise ValueError("create_policy.defaults must be a mapping")
28
+ scope = configured_defaults.get("scope")
29
+ if not isinstance(scope, str) or scope not in _SUPPORTED_SCOPES:
30
+ raise ValueError("create_policy.defaults.scope must be repo or global")
31
+ return {
32
+ "gates": gates,
33
+ "defaults": {
34
+ "scope": scope,
35
+ },
36
+ }
37
+
38
+
39
+ def get_create_hydration_defaults() -> dict[str, Any]:
40
+ """Return normalized create defaults used by CLI hydration."""
41
+
42
+ return dict(get_create_policy_settings()["defaults"])
43
+
44
+
45
+ def validate_create_policy_settings() -> list[ErrorDetail]:
46
+ """Return structured config errors for unsupported create-policy settings."""
47
+
48
+ try:
49
+ get_create_policy_settings()
50
+ except ValueError as exc:
51
+ return [ErrorDetail(code=ErrorCode.INTERNAL_ERROR, message=str(exc), field="create_policy.gates")]
52
+ return []
@@ -0,0 +1,70 @@
1
+ """This module defines boot-time factory helpers for database engine and sessions."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ from app.boot.config import get_config_provider
7
+ from app.periphery.admin.machine_state import try_load_machine_config
8
+ from app.periphery.db.engine import get_engine
9
+ from app.periphery.db.session import get_session_factory
10
+
11
+
12
+ def get_db_dsn() -> str:
13
+ """This function resolves the database DSN from environment configuration."""
14
+
15
+ machine_config, machine_error = try_load_machine_config()
16
+ if machine_error:
17
+ raise RuntimeError(
18
+ "Shellbrain machine config is unreadable. Rerun `shellbrain init` to repair it."
19
+ )
20
+ if machine_config is not None:
21
+ return machine_config.database.app_dsn
22
+
23
+ runtime = get_config_provider().get_runtime()
24
+ database = runtime.get("database")
25
+ if not isinstance(database, dict):
26
+ raise RuntimeError("runtime.database must be configured")
27
+ dsn_env = database.get("dsn_env")
28
+ if not isinstance(dsn_env, str) or not dsn_env:
29
+ raise RuntimeError("runtime.database.dsn_env must be configured")
30
+ dsn = os.getenv(dsn_env)
31
+ if not dsn:
32
+ raise RuntimeError(f"{dsn_env} is not set")
33
+ return dsn
34
+
35
+
36
+ def get_optional_db_dsn() -> str | None:
37
+ """Resolve the application DSN when present, otherwise return None."""
38
+
39
+ machine_config, machine_error = try_load_machine_config()
40
+ if machine_error:
41
+ return None
42
+ if machine_config is not None:
43
+ return machine_config.database.app_dsn
44
+
45
+ runtime = get_config_provider().get_runtime()
46
+ database = runtime.get("database")
47
+ if not isinstance(database, dict):
48
+ return None
49
+ dsn_env = database.get("dsn_env")
50
+ if not isinstance(dsn_env, str) or not dsn_env:
51
+ return None
52
+ return os.getenv(dsn_env)
53
+
54
+
55
+ def get_engine_instance():
56
+ """This function builds a shared SQLAlchemy engine for the application."""
57
+
58
+ return get_engine(get_db_dsn())
59
+
60
+
61
+ def get_session_factory_instance():
62
+ """This function builds a reusable SQLAlchemy session factory for the app."""
63
+
64
+ return get_session_factory(get_engine_instance())
65
+
66
+
67
+ def get_defaults_dir() -> Path:
68
+ """This function returns the path to bundled YAML default configuration files."""
69
+
70
+ return Path(__file__).resolve().parents[1] / "config" / "defaults"
@@ -0,0 +1,55 @@
1
+ """This module defines boot-time wiring for embedding provider construction."""
2
+
3
+ from app.boot.home import get_machine_models_dir
4
+ from app.boot.config import get_config_provider
5
+ from app.core.interfaces.embeddings import IEmbeddingProvider
6
+ from app.periphery.admin.machine_state import load_machine_config
7
+ from app.periphery.embeddings.local_provider import SentenceTransformersEmbeddingProvider
8
+
9
+
10
+ def _get_embedding_config() -> dict:
11
+ """This function returns runtime embedding configuration values."""
12
+
13
+ runtime = get_config_provider().get_runtime()
14
+ values = runtime.get("embeddings")
15
+ if not isinstance(values, dict):
16
+ raise ValueError("runtime.embeddings must be configured")
17
+ return values
18
+
19
+
20
+ def get_embedding_model_name() -> str:
21
+ """This function resolves the model name persisted alongside embedding vectors."""
22
+
23
+ config = _get_embedding_config()
24
+ provider = config.get("provider")
25
+ model = config.get("model")
26
+ if not isinstance(provider, str) or not provider:
27
+ raise ValueError("runtime.embeddings.provider must be configured")
28
+ if not isinstance(model, str) or not model:
29
+ raise ValueError("runtime.embeddings.model must be configured")
30
+ if provider == "sentence_transformers":
31
+ return model
32
+ raise ValueError(f"Unsupported embedding provider: {provider}")
33
+
34
+
35
+ def get_embedding_provider() -> IEmbeddingProvider:
36
+ """This function constructs the configured local embedding provider."""
37
+
38
+ config = _get_embedding_config()
39
+ provider = config.get("provider")
40
+ model = config.get("model")
41
+ if not isinstance(provider, str) or not provider:
42
+ raise ValueError("runtime.embeddings.provider must be configured")
43
+ if not isinstance(model, str) or not model:
44
+ raise ValueError("runtime.embeddings.model must be configured")
45
+ if provider == "sentence_transformers":
46
+ machine_config = load_machine_config()
47
+ cache_folder = str(get_machine_models_dir())
48
+ if machine_config is not None:
49
+ cache_folder = machine_config.embeddings.cache_path
50
+ if machine_config.embeddings.readiness_state != "ready":
51
+ raise RuntimeError(
52
+ "Shellbrain embeddings are not ready. Rerun `shellbrain init` to finish model setup."
53
+ )
54
+ return SentenceTransformersEmbeddingProvider(model=model, cache_folder=cache_folder)
55
+ raise ValueError(f"Unsupported embedding provider: {provider}")
@@ -0,0 +1,45 @@
1
+ """Helpers for locating Shellbrain machine-owned runtime directories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def get_shellbrain_home() -> Path:
10
+ """Return the machine-owned Shellbrain home root."""
11
+
12
+ configured = os.getenv("SHELLBRAIN_HOME")
13
+ if configured:
14
+ return Path(configured).expanduser().resolve()
15
+ return Path("~/.shellbrain").expanduser().resolve()
16
+
17
+
18
+ def get_machine_config_path() -> Path:
19
+ """Return the machine configuration file path."""
20
+
21
+ return get_shellbrain_home() / "config.toml"
22
+
23
+
24
+ def get_machine_lock_path() -> Path:
25
+ """Return the machine-scoped init lock path."""
26
+
27
+ return get_shellbrain_home() / "init.lock"
28
+
29
+
30
+ def get_machine_models_dir() -> Path:
31
+ """Return the machine-owned embedding model cache path."""
32
+
33
+ return get_shellbrain_home() / "models"
34
+
35
+
36
+ def get_machine_backups_dir() -> Path:
37
+ """Return the machine-owned default backup directory."""
38
+
39
+ return get_shellbrain_home() / "backups"
40
+
41
+
42
+ def get_machine_postgres_data_dir() -> Path:
43
+ """Return the managed Postgres bind-mounted data directory."""
44
+
45
+ return get_shellbrain_home() / "postgres-data"
@@ -0,0 +1,61 @@
1
+ """Packaged Alembic bootstrap helpers for installed-shellbrain database migrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.resources import as_file, files
6
+
7
+ from alembic import command
8
+ from alembic.config import Config
9
+
10
+ from app.boot.admin_db import get_admin_db_dsn, get_backup_dir, get_backup_mirror_dir, get_instance_mode_default
11
+ from app.boot.db import get_optional_db_dsn
12
+ from app.periphery.admin.destructive_guard import backup_and_verify_before_destructive_action
13
+ from app.periphery.admin.instance_guard import ensure_instance_metadata, fetch_instance_metadata
14
+ from app.periphery.admin.privileges import reconcile_app_role_privileges
15
+
16
+
17
+ def upgrade_database(revision: str = "head") -> None:
18
+ """Apply packaged Alembic migrations to the configured database."""
19
+
20
+ config = Config()
21
+ admin_dsn = get_admin_db_dsn()
22
+ if _database_has_shellbrain_objects(admin_dsn):
23
+ backup_and_verify_before_destructive_action(
24
+ admin_dsn=admin_dsn,
25
+ backup_root=get_backup_dir(),
26
+ mirror_root=get_backup_mirror_dir(),
27
+ )
28
+ config.set_main_option("sqlalchemy.url", admin_dsn)
29
+ with as_file(files("app").joinpath("migrations")) as migrations_path:
30
+ config.set_main_option("script_location", str(migrations_path))
31
+ command.upgrade(config, revision)
32
+ if fetch_instance_metadata(admin_dsn) is None:
33
+ ensure_instance_metadata(
34
+ admin_dsn,
35
+ instance_mode=get_instance_mode_default(),
36
+ created_by="app.admin.migrate",
37
+ notes="Stamped by packaged migration runner.",
38
+ )
39
+ app_dsn = get_optional_db_dsn()
40
+ if app_dsn:
41
+ reconcile_app_role_privileges(admin_dsn=admin_dsn, app_dsn=app_dsn)
42
+
43
+
44
+ def _database_has_shellbrain_objects(admin_dsn: str) -> bool:
45
+ """Return whether the target database already contains Shellbrain-managed tables."""
46
+
47
+ import psycopg
48
+
49
+ with psycopg.connect(admin_dsn.replace("+psycopg", "")) as conn:
50
+ with conn.cursor() as cur:
51
+ cur.execute(
52
+ """
53
+ SELECT EXISTS (
54
+ SELECT 1
55
+ FROM information_schema.tables
56
+ WHERE table_schema = 'public'
57
+ AND table_name IN ('memories', 'episodes', 'episode_events', 'operation_invocations')
58
+ )
59
+ """
60
+ )
61
+ return bool(cur.fetchone()[0])