secondbrain-py 0.2.1__py3-none-any.whl

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 (273) hide show
  1. brain/__init__.py +0 -0
  2. brain/__main__.py +18 -0
  3. brain/_capture_command.py +445 -0
  4. brain/_compose.py +52 -0
  5. brain/activity.py +206 -0
  6. brain/ask.py +631 -0
  7. brain/audio.py +591 -0
  8. brain/backfill/__init__.py +12 -0
  9. brain/backfill/search_extras.py +141 -0
  10. brain/backfill/source_rows.py +101 -0
  11. brain/bin/__init__.py +1 -0
  12. brain/bin/_launcher.py +107 -0
  13. brain/bin/down.py +8 -0
  14. brain/bin/launchd.py +268 -0
  15. brain/bin/monitor.py +570 -0
  16. brain/bin/rebuild.py +8 -0
  17. brain/bin/status.py +8 -0
  18. brain/bin/up.py +8 -0
  19. brain/brief.py +272 -0
  20. brain/capture.py +49 -0
  21. brain/chat.py +293 -0
  22. brain/cli.py +9760 -0
  23. brain/cli_claude.py +81 -0
  24. brain/cli_connect.py +285 -0
  25. brain/cli_demo.py +266 -0
  26. brain/config.py +1949 -0
  27. brain/connect.py +925 -0
  28. brain/db.py +540 -0
  29. brain/demo/__init__.py +452 -0
  30. brain/demo/corpus/manifest.json +403 -0
  31. brain/demo/embedder.py +74 -0
  32. brain/durations.py +84 -0
  33. brain/edit_session.py +156 -0
  34. brain/editor.py +67 -0
  35. brain/elicit/__init__.py +16 -0
  36. brain/elicit/detectors.py +250 -0
  37. brain/elicit/drafter.py +70 -0
  38. brain/elicit/queue.py +220 -0
  39. brain/elicit/schema.py +48 -0
  40. brain/elicit/session.py +445 -0
  41. brain/embedding_targets.py +54 -0
  42. brain/embeddings.py +424 -0
  43. brain/enrichment.py +808 -0
  44. brain/errors.py +357 -0
  45. brain/eval/__init__.py +129 -0
  46. brain/eval/answer_eval.py +281 -0
  47. brain/eval/baseline.py +265 -0
  48. brain/eval/concept_extraction.py +378 -0
  49. brain/eval/corpus.py +152 -0
  50. brain/eval/errors.py +19 -0
  51. brain/eval/graph_baseline.py +226 -0
  52. brain/eval/graph_retrieval.py +202 -0
  53. brain/eval/graph_runner.py +319 -0
  54. brain/eval/metrics.py +101 -0
  55. brain/eval/runner.py +223 -0
  56. brain/format.py +783 -0
  57. brain/gaps.py +390 -0
  58. brain/graph_rag/__init__.py +94 -0
  59. brain/graph_rag/_retrieval_common.py +113 -0
  60. brain/graph_rag/aggregates.py +303 -0
  61. brain/graph_rag/aliases/__init__.py +583 -0
  62. brain/graph_rag/backends/__init__.py +10 -0
  63. brain/graph_rag/backends/_age_helpers.py +473 -0
  64. brain/graph_rag/backends/age.py +782 -0
  65. brain/graph_rag/backends/base.py +272 -0
  66. brain/graph_rag/build.py +344 -0
  67. brain/graph_rag/communities.py +644 -0
  68. brain/graph_rag/communities_summary.py +437 -0
  69. brain/graph_rag/concepts.py +202 -0
  70. brain/graph_rag/cooccur.py +193 -0
  71. brain/graph_rag/cross_type.py +312 -0
  72. brain/graph_rag/extract.py +885 -0
  73. brain/graph_rag/fuse.py +371 -0
  74. brain/graph_rag/global_.py +412 -0
  75. brain/graph_rag/grouping.py +372 -0
  76. brain/graph_rag/person_resolver.py +167 -0
  77. brain/graph_rag/reconcile.py +792 -0
  78. brain/graph_rag/relational.py +353 -0
  79. brain/graph_rag/retrieve.py +526 -0
  80. brain/graph_rag/router.py +288 -0
  81. brain/graph_rag/schema.py +320 -0
  82. brain/graph_rag/sync.py +237 -0
  83. brain/graph_rag/tenancy.py +43 -0
  84. brain/graph_rag/themes.py +501 -0
  85. brain/graph_rag/weighting.py +202 -0
  86. brain/ingest/__init__.py +1926 -0
  87. brain/ingest/chunker.py +249 -0
  88. brain/ingest/docx.py +40 -0
  89. brain/ingest/gmail.py +621 -0
  90. brain/ingest/markdown.py +37 -0
  91. brain/ingest/pdf.py +61 -0
  92. brain/ingest/stdin.py +22 -0
  93. brain/ingest/sub_tokens.py +91 -0
  94. brain/ingest/text.py +16 -0
  95. brain/interactions.py +205 -0
  96. brain/maintenance.py +355 -0
  97. brain/mcp_server.py +3405 -0
  98. brain/migrations/001_init.sql +43 -0
  99. brain/migrations/002_qwen3_embedding.sql +17 -0
  100. brain/migrations/003_vault_model.sql +41 -0
  101. brain/migrations/004_relax_content_hash_uniqueness.sql +18 -0
  102. brain/migrations/005_derived_links.sql +67 -0
  103. brain/migrations/006_dedup_file_by_source_path.sql +25 -0
  104. brain/migrations/007_email_thread_and_draft.sql +15 -0
  105. brain/migrations/008_gmail_thread_unique.sql +11 -0
  106. brain/migrations/009_chunks_weighted_tsv.sql +28 -0
  107. brain/migrations/010_interactions.sql +30 -0
  108. brain/migrations/011_documents_summary.sql +23 -0
  109. brain/migrations/012_graphrag.sql +171 -0
  110. brain/migrations/013_graphrag_communities.sql +125 -0
  111. brain/migrations/014_graphrag_community_summary_hash.sql +33 -0
  112. brain/migrations/015_interactions_graph_targets.sql +89 -0
  113. brain/migrations/016_index_hygiene.sql +61 -0
  114. brain/migrations/017_elicit.sql +30 -0
  115. brain/migrations/018_review_gap_signal_kinds.sql +40 -0
  116. brain/migrations/019_search_queries.sql +35 -0
  117. brain/migrations/020_link_suggestions.sql +40 -0
  118. brain/migrations/021_timeline_doc_date.sql +34 -0
  119. brain/migrations/022_link_suggestions_undirected.sql +84 -0
  120. brain/migrations/023_search_queries_fts_count.sql +28 -0
  121. brain/quartz_overrides/__init__.py +8 -0
  122. brain/quartz_overrides/quartz/bootstrap-cli.mjs +65 -0
  123. brain/quartz_overrides/quartz/build.ts +568 -0
  124. brain/quartz_overrides/quartz/cli/args.js +152 -0
  125. brain/quartz_overrides/quartz/cli/build_partial_handler.js +544 -0
  126. brain/quartz_overrides/quartz/cli/handlers.js +636 -0
  127. brain/quartz_overrides/quartz/components/CommandPalette.tsx +172 -0
  128. brain/quartz_overrides/quartz/components/Explorer.tsx +198 -0
  129. brain/quartz_overrides/quartz/components/Footer.tsx +27 -0
  130. brain/quartz_overrides/quartz/components/Graph.tsx +468 -0
  131. brain/quartz_overrides/quartz/components/PageTitle.tsx +72 -0
  132. brain/quartz_overrides/quartz/components/RelatedDocs.tsx +38 -0
  133. brain/quartz_overrides/quartz/components/Search.tsx +161 -0
  134. brain/quartz_overrides/quartz/components/SummaryLede.tsx +72 -0
  135. brain/quartz_overrides/quartz/components/index.ts +92 -0
  136. brain/quartz_overrides/quartz/components/pages/TagContent.tsx +272 -0
  137. brain/quartz_overrides/quartz/components/scripts/commandPalette.inline.ts +665 -0
  138. brain/quartz_overrides/quartz/components/scripts/explorer.inline.ts +768 -0
  139. brain/quartz_overrides/quartz/components/scripts/graph.inline.ts +2302 -0
  140. brain/quartz_overrides/quartz/components/scripts/relatedDocs.inline.ts +163 -0
  141. brain/quartz_overrides/quartz/components/scripts/search.inline.ts +1011 -0
  142. brain/quartz_overrides/quartz/plugins/emitters/contentIndex.ts +546 -0
  143. brain/quartz_overrides/quartz/plugins/transformers/codeCopy.ts +94 -0
  144. brain/quartz_overrides/quartz/plugins/transformers/derivedFenceMark.ts +302 -0
  145. brain/quartz_overrides/quartz/plugins/transformers/emailThread.ts +148 -0
  146. brain/quartz_overrides/quartz/plugins/transformers/emptyDoorFilter.ts +213 -0
  147. brain/quartz_overrides/quartz/plugins/transformers/index.ts +114 -0
  148. brain/quartz_overrides/quartz/plugins/transformers/linkKindMark.ts +205 -0
  149. brain/quartz_overrides/quartz/plugins/transformers/linkSourceTag.ts +104 -0
  150. brain/quartz_overrides/quartz/plugins/transformers/relativeDate.ts +100 -0
  151. brain/quartz_overrides/quartz/plugins/transformers/reloadSignal.ts +131 -0
  152. brain/quartz_overrides/quartz/processors/parse.ts +371 -0
  153. brain/quartz_overrides/quartz/processors/parser_cache.ts +78 -0
  154. brain/quartz_overrides/quartz/static/brain-logo-dark.png +0 -0
  155. brain/quartz_overrides/quartz/static/brain-logo-light.png +0 -0
  156. brain/quartz_overrides/quartz/static/codeCopy.js +196 -0
  157. brain/quartz_overrides/quartz/static/emailThread.js +334 -0
  158. brain/quartz_overrides/quartz/static/favicon.ico +0 -0
  159. brain/quartz_overrides/quartz/static/icon.png +0 -0
  160. brain/quartz_overrides/quartz/static/linkSourceTag.js +104 -0
  161. brain/quartz_overrides/quartz/static/relativeDate.js +142 -0
  162. brain/quartz_overrides/quartz/static/reload.js +168 -0
  163. brain/quartz_overrides/quartz/styles/brain/_article.scss +252 -0
  164. brain/quartz_overrides/quartz/styles/brain/_atmosphere.scss +113 -0
  165. brain/quartz_overrides/quartz/styles/brain/_callouts.scss +180 -0
  166. brain/quartz_overrides/quartz/styles/brain/_cmdk.scss +7 -0
  167. brain/quartz_overrides/quartz/styles/brain/_code.scss +208 -0
  168. brain/quartz_overrides/quartz/styles/brain/_command_palette.scss +369 -0
  169. brain/quartz_overrides/quartz/styles/brain/_email_thread.scss +228 -0
  170. brain/quartz_overrides/quartz/styles/brain/_explorer.scss +142 -0
  171. brain/quartz_overrides/quartz/styles/brain/_home.scss +182 -0
  172. brain/quartz_overrides/quartz/styles/brain/_links.scss +322 -0
  173. brain/quartz_overrides/quartz/styles/brain/_marginalia.scss +117 -0
  174. brain/quartz_overrides/quartz/styles/brain/_motion.scss +175 -0
  175. brain/quartz_overrides/quartz/styles/brain/_people_hub.scss +100 -0
  176. brain/quartz_overrides/quartz/styles/brain/_related_docs.scss +137 -0
  177. brain/quartz_overrides/quartz/styles/brain/_search.scss +252 -0
  178. brain/quartz_overrides/quartz/styles/brain/_sidebar.scss +468 -0
  179. brain/quartz_overrides/quartz/styles/brain/_summary_lede.scss +56 -0
  180. brain/quartz_overrides/quartz/styles/brain/_surface.scss +43 -0
  181. brain/quartz_overrides/quartz/styles/brain/_tag_content.scss +118 -0
  182. brain/quartz_overrides/quartz/styles/brain/_tokens.scss +197 -0
  183. brain/quartz_overrides/quartz/styles/brain/_typography.scss +92 -0
  184. brain/quartz_overrides/quartz/styles/custom.scss +89 -0
  185. brain/quartz_overrides/quartz/styles/graph.scss +505 -0
  186. brain/quartz_overrides/quartz/util/ctx.ts +92 -0
  187. brain/quartz_overrides/quartz/util/fastpath_manifest.ts +608 -0
  188. brain/quartz_overrides/quartz/util/path.ts +358 -0
  189. brain/quartz_overrides/quartz/util/sourceIcons.ts +55 -0
  190. brain/quartz_overrides/quartz.config.ts +270 -0
  191. brain/quartz_overrides/quartz.layout.ts +314 -0
  192. brain/queries.py +1188 -0
  193. brain/rank_fusion.py +8 -0
  194. brain/resurface.py +210 -0
  195. brain/review/__init__.py +26 -0
  196. brain/review/emit.py +27 -0
  197. brain/review/queries.py +436 -0
  198. brain/review/render.py +196 -0
  199. brain/review/scans.py +355 -0
  200. brain/review/weekly.py +413 -0
  201. brain/search.py +704 -0
  202. brain/set_similarity.py +15 -0
  203. brain/setup.py +1205 -0
  204. brain/tags.py +56 -0
  205. brain/templates/Caddyfile.j2 +9 -0
  206. brain/templates/__init__.py +1 -0
  207. brain/templates/bin/__init__.py +1 -0
  208. brain/templates/bin/_brain-brief-fg.sh +25 -0
  209. brain/templates/bin/_brain-build-fg.sh +53 -0
  210. brain/templates/bin/_brain-watcher-fg.sh +65 -0
  211. brain/templates/bin/brain-down.sh +89 -0
  212. brain/templates/bin/brain-status.sh +83 -0
  213. brain/templates/bin/brain-up.sh +221 -0
  214. brain/templates/docker/age/Dockerfile +79 -0
  215. brain/templates/docker-compose.stock.yml.j2 +26 -0
  216. brain/templates/docker-compose.yml.j2 +34 -0
  217. brain/templates/env.example +190 -0
  218. brain/templates/launchd/__init__.py +1 -0
  219. brain/templates/launchd/com.brain.brief.plist.j2 +45 -0
  220. brain/templates/launchd/com.brain.build.plist.j2 +46 -0
  221. brain/templates/launchd/com.brain.watcher.plist.j2 +46 -0
  222. brain/templates/skill/SKILL.md +63 -0
  223. brain/templates/skill/__init__.py +1 -0
  224. brain/timeline.py +834 -0
  225. brain/todo.py +124 -0
  226. brain/uninstall.py +185 -0
  227. brain/vault/__init__.py +115 -0
  228. brain/vault/_atomic.py +25 -0
  229. brain/vault/daily_index.py +228 -0
  230. brain/vault/derived_links/__init__.py +50 -0
  231. brain/vault/derived_links/directory.py +683 -0
  232. brain/vault/derived_links/fence.py +408 -0
  233. brain/vault/derived_links/gws.py +64 -0
  234. brain/vault/derived_links/participants.py +143 -0
  235. brain/vault/derived_links/pass_runner.py +362 -0
  236. brain/vault/derived_links/rules.py +137 -0
  237. brain/vault/export.py +683 -0
  238. brain/vault/frontmatter.py +165 -0
  239. brain/vault/graph.py +620 -0
  240. brain/vault/graph_format.py +388 -0
  241. brain/vault/link_rewrite.py +235 -0
  242. brain/vault/links.py +260 -0
  243. brain/vault/note_builder.py +211 -0
  244. brain/vault/paths.py +55 -0
  245. brain/vault/quartz_overlay.py +236 -0
  246. brain/vault/rename.py +591 -0
  247. brain/vault/resolver.py +304 -0
  248. brain/vault/slug.py +127 -0
  249. brain/vault/sync.py +1513 -0
  250. brain/vault/sync_summaries.py +264 -0
  251. brain/vault/templates.py +145 -0
  252. brain/vault/watch.py +1052 -0
  253. brain/wiki/__init__.py +6 -0
  254. brain/wiki/_github_slugger.py +76 -0
  255. brain/wiki/_person_name.py +314 -0
  256. brain/wiki/build_homepage.py +541 -0
  257. brain/wiki/build_partial.py +273 -0
  258. brain/wiki/build_people.py +934 -0
  259. brain/wiki/build_related.py +758 -0
  260. brain/wiki/build_swap.py +585 -0
  261. brain/wiki/build_watcher.py +975 -0
  262. brain/wiki/edit_classifier.py +215 -0
  263. brain/wiki/errors.py +10 -0
  264. brain/wiki/fastpath_manifest.py +475 -0
  265. brain/wiki/fastpath_state.py +174 -0
  266. brain/wiki/install.py +296 -0
  267. brain/wiki/slug.py +111 -0
  268. secondbrain_py-0.2.1.dist-info/METADATA +195 -0
  269. secondbrain_py-0.2.1.dist-info/RECORD +273 -0
  270. secondbrain_py-0.2.1.dist-info/WHEEL +5 -0
  271. secondbrain_py-0.2.1.dist-info/entry_points.txt +11 -0
  272. secondbrain_py-0.2.1.dist-info/licenses/LICENSE +21 -0
  273. secondbrain_py-0.2.1.dist-info/top_level.txt +1 -0
brain/tags.py ADDED
@@ -0,0 +1,56 @@
1
+ """Tag normalization rules used across brain (DB writes, file writes, ingest).
2
+
3
+ Single source of truth for the canonical tag form: lowercase
4
+ (``str.casefold`` for proper Unicode handling), hyphen-separated, deduped,
5
+ empty inputs dropped. Every tag-write boundary in the codebase
6
+ (``ingest.apply_tags``, ``vault.frontmatter.rewrite_tags``,
7
+ ``vault.sync._coerce_tag_list``, the ``brain backfill normalize-tags``
8
+ subcommand) feeds inputs through :func:`normalize_tags` before persisting,
9
+ so future writes can never re-introduce uppercase / separator / duplicate
10
+ drift.
11
+ """
12
+ import re
13
+ from collections.abc import Iterable
14
+
15
+ _SEPARATOR_RE = re.compile(r"[\s_]+")
16
+ _HYPHEN_RUN_RE = re.compile(r"-+")
17
+
18
+
19
+ def normalize_tag(tag: str) -> str:
20
+ """Canonicalize a single tag string.
21
+
22
+ Steps, in order:
23
+
24
+ 1. ``str.strip`` to drop leading/trailing whitespace.
25
+ 2. ``str.casefold`` so Unicode edge cases collapse correctly
26
+ (German ``ß`` → ``ss``, Greek final sigma → ``σ``, etc.).
27
+ 3. Replace runs of whitespace + underscores with a single hyphen so
28
+ ``Interview Prep`` and ``interview_prep`` both canonicalize to
29
+ ``interview-prep``.
30
+ 4. Collapse runs of hyphens to a single hyphen, then strip leading /
31
+ trailing hyphens — ``" --foo-- "`` → ``"foo"``.
32
+
33
+ An all-whitespace input (or an input that becomes empty after the
34
+ cleanup, e.g. ``"---"``) returns the empty string. Callers that work
35
+ with collections should use :func:`normalize_tags` which drops empties
36
+ automatically.
37
+ """
38
+ folded = tag.strip().casefold()
39
+ hyphenated = _SEPARATOR_RE.sub("-", folded)
40
+ return _HYPHEN_RUN_RE.sub("-", hyphenated).strip("-")
41
+
42
+
43
+ def normalize_tags(tags: Iterable[str]) -> list[str]:
44
+ """Canonicalize, dedupe, and drop empties from an iterable of tags.
45
+
46
+ Order is the first-seen order of the canonicalized form: the first
47
+ occurrence of each canonical tag wins, later duplicates (regardless of
48
+ casing or separator variant) are silently dropped. Empty / whitespace-
49
+ only entries — and entries that canonicalize to the empty string — are
50
+ dropped.
51
+
52
+ The return value is always a fresh ``list[str]``; callers can mutate it
53
+ safely.
54
+ """
55
+ cleaned = (normalize_tag(t) for t in tags)
56
+ return [t for t in dict.fromkeys(cleaned) if t]
@@ -0,0 +1,9 @@
1
+ # Jinja variables: wiki_port, vault_path
2
+ :{{ wiki_port }} {
3
+ root * {{ vault_path }}/.quartz/current
4
+ file_server
5
+ encode gzip
6
+ # ETag-driven reload (P6.1) — keep cache short on the build-id file
7
+ @buildid path /.build-id
8
+ header @buildid Cache-Control "max-age=2, must-revalidate"
9
+ }
@@ -0,0 +1 @@
1
+ """Marker so importlib.resources can resolve this subdirectory in pipx-installed wheels."""
@@ -0,0 +1 @@
1
+ """Marker so importlib.resources can resolve this subdirectory in pipx-installed wheels."""
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # One-shot wrapper for the daily brief, intended to run under launchd's
3
+ # StartCalendarInterval (07:00 local). Unlike _brain-watcher-fg / _brain-build-fg
4
+ # this does NOT supervise a long-running daemon and writes no pid file — it runs
5
+ # `brain brief --wiki` once and exits. launchd's StandardOut/ErrorPath capture
6
+ # the digest + any warnings.
7
+ #
8
+ # Env contract:
9
+ # BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed through to brief.
10
+ # BRAIN_PY pins the Python interpreter that has `brain` importable
11
+ # (exported by the launchd plist); falls back to python3.
12
+
13
+ set -euo pipefail
14
+
15
+ # Pick the Python interpreter once. BRAIN_PY wins; otherwise prefer python3.
16
+ if [[ -n "${BRAIN_PY:-}" ]]; then
17
+ PY="$BRAIN_PY"
18
+ elif command -v python3 >/dev/null 2>&1; then
19
+ PY="$(command -v python3)"
20
+ else
21
+ echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
22
+ exit 1
23
+ fi
24
+
25
+ exec "$PY" -m brain brief --wiki
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # Foreground wrapper for the Quartz build watcher, intended to run under
3
+ # launchd. Mirrors `_brain-watcher-fg`'s contract — see that file's header
4
+ # for the rationale + pid-file semantics.
5
+ #
6
+ # This wrapper specifically supervises `python -m brain.wiki.build_watcher`,
7
+ # which re-renders the Quartz site into a fresh `<vault>/.quartz/builds/<id>/`
8
+ # and atomically retargets `current/` on every vault change. Pre-launchd it
9
+ # was started via `nohup ... &` from `bin/brain-up`, with the same drift
10
+ # problem (silent death + no supervisor).
11
+ #
12
+ # Env contract:
13
+ # BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed to `--vault`.
14
+ # BRAIN_WIKI_KEEP_BUILDS (default: 3) — old build dirs retained per swap.
15
+ # The wrapper sets BRAIN_WIKI_RELOAD=1 itself, matching the bin/brain-up
16
+ # invocation, so the LaunchAgent plist doesn't have to know about it.
17
+
18
+ set -euo pipefail
19
+
20
+ VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
21
+ KEEP="${BRAIN_WIKI_KEEP_BUILDS:-3}"
22
+ # Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
23
+ # pipx install, $repo_root/bin/ in a dev checkout) so the pid file lands in a
24
+ # stable, non-reaped dir. Honor an explicit BRAIN_HOME override if set.
25
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
26
+ BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
27
+ # Pid path defaults to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
28
+ # files left untouched for 3 days; this daemon writes the pid once at launch
29
+ # then runs for weeks, so a /tmp pid file silently vanishes and brain-status
30
+ # misreports the live daemon as stopped. Overridable (unset = unchanged) so
31
+ # tests run this wrapper hermetically against a tmp pid dir.
32
+ BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
33
+
34
+ # Pick the Python interpreter once. BRAIN_PY (env var) wins; otherwise
35
+ # prefer python3 on PATH. The installed-shim flow (in $BRAIN_HOME/bin/)
36
+ # always has BRAIN_PY exported by the Python launcher in
37
+ # src/brain/bin/_launcher.py — but we keep the python3 fallback for
38
+ # users who invoke the shims directly outside the launcher (e.g. via
39
+ # launchd plists that themselves set PATH but not BRAIN_PY).
40
+ if [[ -n "${BRAIN_PY:-}" ]]; then
41
+ PY="$BRAIN_PY"
42
+ elif command -v python3 >/dev/null 2>&1; then
43
+ PY="$(command -v python3)"
44
+ else
45
+ echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
46
+ exit 1
47
+ fi
48
+
49
+ export BRAIN_WIKI_RELOAD=1
50
+
51
+ mkdir -p "$(dirname "$BUILD_PID")"
52
+ echo "$$" >"$BUILD_PID"
53
+ exec "$PY" -m brain.wiki.build_watcher --vault "$VAULT" --keep "$KEEP"
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # Foreground wrapper for the vault sync watcher, intended to run under launchd.
3
+ #
4
+ # Why a wrapper:
5
+ # The historical `bin/brain-up` flow uses `nohup ... &` to background-spawn
6
+ # the watcher, then writes the spawned pid into the pid file for
7
+ # `brain-status` to pick up. That works in a shell session but doesn't
8
+ # survive the next "what kills my daemons silently?" round (terminal close,
9
+ # sleep/wake, OOM, …) because nothing supervises the daemon.
10
+ #
11
+ # This script flips the pattern: it stays in the foreground, writes the
12
+ # pid file, then `exec`s the watcher so the watcher inherits the same pid.
13
+ # A LaunchAgent (com.brain.watcher) wraps THIS script with `KeepAlive=true`
14
+ # so launchd respawns us within `ThrottleInterval` seconds whenever the
15
+ # underlying watcher dies — no manual `brain-up` after every crash.
16
+ #
17
+ # PID-file contract:
18
+ # We write `$$` into the pid file ($BRAIN_HOME/run/brain-watch.pid) BEFORE
19
+ # the exec. `exec` replaces
20
+ # the current process image but keeps the pid, so the file points at the
21
+ # still-running watcher. `brain-status`'s `kill -0 $(cat …)` check then
22
+ # reports "running" exactly as it does for the legacy nohup path. When
23
+ # launchd kills us out-of-band the file becomes stale; that's fine, the
24
+ # `kill -0` check fails and the row flips to "stopped".
25
+ #
26
+ # Env contract:
27
+ # BRAIN_VAULT_PATH (default: $HOME/brain-vault) — passed to `--vault`.
28
+ # PATH must include the pipx bin dir (or wherever `brain` is installed)
29
+ # so `brain` resolves; the LaunchAgent plist's EnvironmentVariables block
30
+ # sets this explicitly.
31
+
32
+ set -euo pipefail
33
+
34
+ VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
35
+
36
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
37
+ PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
38
+ VENV_BIN="$PROJECT_ROOT/.venv/bin"
39
+ # Pid path defaults to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
40
+ # files left untouched for 3 days; this daemon writes the pid once at launch
41
+ # then runs for weeks, so a /tmp pid file silently vanishes and brain-status
42
+ # misreports the live daemon as stopped. $BRAIN_HOME resolves to an explicit
43
+ # BRAIN_HOME override if set, else PROJECT_ROOT (== the script's parent: the
44
+ # script lives in $BRAIN_HOME/.shims/ or $repo_root/bin/); kept consistent with
45
+ # the build fg wrapper and brain-{status,down,up}. Overridable (unset =
46
+ # unchanged) so tests run this wrapper hermetically against a tmp pid dir.
47
+ WATCH_PID="${BRAIN_WATCH_PID:-${BRAIN_HOME:-$PROJECT_ROOT}/run/brain-watch.pid}"
48
+ # brain: tests set BRAIN_SKIP_VENV_AUTOLOAD=1 to keep the wrapper from
49
+ # silently prepending the developer's real `.venv/bin` (and thus the real
50
+ # `brain`) ahead of the test stub on PATH. Same shape as the BRAIN_PY
51
+ # override in `bin/brain-up`. In production the variable is unset and the
52
+ # venv prepend just works (for dev-checkout users with a local .venv;
53
+ # pipx-installed users have brain on PATH via the launchd plist already).
54
+ if [[ "${BRAIN_SKIP_VENV_AUTOLOAD:-0}" != "1" && -d "$VENV_BIN" ]]; then
55
+ export PATH="$VENV_BIN:$PATH"
56
+ fi
57
+
58
+ if ! command -v brain >/dev/null 2>&1; then
59
+ echo "brain CLI not on PATH (expected via launchd PATH or local .venv)" >&2
60
+ exit 1
61
+ fi
62
+
63
+ mkdir -p "$(dirname "$WATCH_PID")"
64
+ echo "$$" >"$WATCH_PID"
65
+ exec brain vault sync --watch --vault "$VAULT"
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # Stop the second-brain wiki build watcher + sync watcher (blue/green serve edition).
3
+ # Idempotent — safe to re-run.
4
+ #
5
+ # Caddy is intentionally left running: it keeps serving the last-good build
6
+ # from <vault>/.quartz/current so brain.test stays healthy across restarts.
7
+ #
8
+ # Env overrides (default to the production values when unset — production
9
+ # behavior is unchanged unless you explicitly set these; mirrors the
10
+ # BRAIN_LAUNCHCTL / BRAIN_LAUNCHD_DIR pattern below). They exist so the test
11
+ # suite can run this shim HERMETICALLY — pointing the pid files at a tmp dir
12
+ # and the orphan-killer at a stub — so the real `brain-up` watchers on a dev
13
+ # machine are never touched:
14
+ # BRAIN_WATCH_PID (default: $BRAIN_HOME/run/brain-watch.pid)
15
+ # BRAIN_BUILD_PID (default: $BRAIN_HOME/run/brain-build.pid)
16
+ # BRAIN_WIKI_PID (default: $BRAIN_HOME/run/brain-wiki.pid)
17
+ # BRAIN_PKILL (default: pkill) — binary used for the orphan sweep
18
+
19
+ set -euo pipefail
20
+
21
+ # Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
22
+ # pipx install, $repo_root/bin/ in a dev checkout) so the pid paths match where
23
+ # the fg wrappers write them. Honor an explicit BRAIN_HOME override if set.
24
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
25
+ BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
26
+
27
+ # Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
28
+ # files left untouched for 3 days, which silently vanishes a long-lived
29
+ # daemon's pid file; $BRAIN_HOME/run/ is not reaped. Must match the fg
30
+ # wrappers' defaults. Overridable (unset = unchanged) for hermetic tests.
31
+ WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}" # `brain vault sync --watch`
32
+ BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}" # `python -m brain.wiki.build_watcher`
33
+ WIKI_PID="${BRAIN_WIKI_PID:-$BRAIN_HOME_RESOLVED/run/brain-wiki.pid}" # legacy: pre-blue/green `npx quartz --serve`
34
+ PKILL="${BRAIN_PKILL:-pkill}" # orphan-sweep binary (tests stub it)
35
+
36
+ LAUNCHD_DIR="${BRAIN_LAUNCHD_DIR:-$HOME/Library/LaunchAgents}"
37
+ LAUNCHCTL="${BRAIN_LAUNCHCTL:-launchctl}"
38
+
39
+ # brain: if the user installed launchd supervision via
40
+ # `bin/brain-install-launchd`, plain `kill $pid` is ineffective — launchd
41
+ # respawns each daemon within ThrottleInterval seconds. Bootout the
42
+ # LaunchAgents first so the kills below actually stick. We don't remove
43
+ # the .plist files here (that's `bin/brain-uninstall-launchd`'s job);
44
+ # bootout-only means a subsequent `bin/brain-up` won't re-supervise, but
45
+ # `launchctl bootstrap gui/$UID <plist>` (or rerunning the install
46
+ # script) will restore supervision without rewriting the plists.
47
+ bootout_one() {
48
+ local label="$1"
49
+ local domain="gui/$UID/${label}"
50
+ if "$LAUNCHCTL" print "$domain" >/dev/null 2>&1; then
51
+ "$LAUNCHCTL" bootout "$domain" 2>/dev/null || true
52
+ echo "unloaded launchd: $domain"
53
+ fi
54
+ }
55
+
56
+ bootout_one com.brain.watcher
57
+ bootout_one com.brain.build
58
+
59
+ stop_one() {
60
+ local pid_file="$1"
61
+ local label="$2"
62
+ if [[ ! -f "$pid_file" ]]; then
63
+ return 0
64
+ fi
65
+ local pid
66
+ pid=$(cat "$pid_file")
67
+ if kill -0 "$pid" 2>/dev/null; then
68
+ kill "$pid" 2>/dev/null || true
69
+ sleep 1
70
+ if kill -0 "$pid" 2>/dev/null; then
71
+ kill -9 "$pid" 2>/dev/null || true
72
+ fi
73
+ echo "stopped $label (pid $pid)"
74
+ fi
75
+ rm -f "$pid_file"
76
+ }
77
+
78
+ stop_one "$WATCH_PID" 'watcher (vault sync)'
79
+ stop_one "$BUILD_PID" 'build watcher'
80
+ # Belt-and-suspenders: only stop a legacy wiki pid file if a stale install
81
+ # left one behind. Brand-new clones won't have this file.
82
+ stop_one "$WIKI_PID" 'wiki (legacy quartz --serve)'
83
+
84
+ # Belt + suspenders: any orphaned processes from prior crashed runs.
85
+ "$PKILL" -f 'brain.wiki.build_watcher' 2>/dev/null || true
86
+ "$PKILL" -f 'brain vault sync --watch' 2>/dev/null || true
87
+
88
+ echo '🧠 brain is down'
89
+ echo ' (caddy left running — brain.test continues to serve last good build)'
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # Report on what the brain is currently running (blue/green serve edition).
3
+
4
+ set -euo pipefail
5
+
6
+ VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
7
+ QUARTZ_DIR="$VAULT/.quartz"
8
+ PORT="${BRAIN_WIKI_PORT:-8080}"
9
+ URL="http://localhost:$PORT"
10
+
11
+ # Shim is installed at $BRAIN_HOME/.shims/brain-status, so $BRAIN_HOME is
12
+ # two parent-traversals up (.shims/ → $BRAIN_HOME). Honor an explicit
13
+ # BRAIN_HOME override if the launcher set one.
14
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
15
+ BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
16
+
17
+ # Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
18
+ # files left untouched for 3 days; the build/watch daemons write their pid file
19
+ # once at launch and then run for weeks, so a /tmp pid file silently vanishes
20
+ # and this very script misreports a live daemon as "stopped". $BRAIN_HOME/run/
21
+ # is not reaped. Overridable (unset = unchanged) so the test suite can run this
22
+ # shim hermetically against a tmp pid dir, never colliding with a live
23
+ # brain-up install on a dev machine.
24
+ WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}"
25
+ BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
26
+ WIKI_PID="${BRAIN_WIKI_PID:-$BRAIN_HOME_RESOLVED/run/brain-wiki.pid}" # legacy quartz --serve install
27
+ # T1.7 plist templates redirect StandardOut to $BRAIN_HOME/logs/com.brain.*.out.log,
28
+ # not the legacy /tmp paths the pre-launchd nohup era used.
29
+ WATCH_LOG="$BRAIN_HOME_RESOLVED/logs/com.brain.watcher.out.log"
30
+ BUILD_LOG="$BRAIN_HOME_RESOLVED/logs/com.brain.build.out.log"
31
+
32
+ check_one() {
33
+ local pid_file="$1"
34
+ local label="$2"
35
+ local extra="$3"
36
+ if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null; then
37
+ printf ' %-9s ✅ running pid=%-7s %s\n' "$label:" "$(cat "$pid_file")" "$extra"
38
+ else
39
+ printf ' %-9s ⛔ stopped %s\n' "$label:" "$extra"
40
+ fi
41
+ }
42
+
43
+ echo '🧠 brain status'
44
+ check_one "$WATCH_PID" 'watcher' "(logs: $WATCH_LOG)"
45
+ check_one "$BUILD_PID" 'build' "(logs: $BUILD_LOG)"
46
+
47
+ # Legacy row — only show if a stale pid file from the pre-blue/green
48
+ # install is still on disk. Helps diagnose half-migrated machines.
49
+ if [[ -f "$WIKI_PID" ]]; then
50
+ if kill -0 "$(cat "$WIKI_PID")" 2>/dev/null; then
51
+ printf ' %-9s ⚠️ legacy quartz --serve still running pid=%s — kill with brain-down\n' \
52
+ 'wiki:' "$(cat "$WIKI_PID")"
53
+ else
54
+ printf ' %-9s ⛔ stopped (legacy quartz --serve was here — pid file %s)\n' \
55
+ 'wiki:' "$WIKI_PID"
56
+ fi
57
+ fi
58
+
59
+ # Current build pointer — what Caddy is serving right now.
60
+ CURRENT="$QUARTZ_DIR/current"
61
+ if [[ -L "$CURRENT" ]]; then
62
+ target="$(readlink "$CURRENT")"
63
+ if resolved="$(cd "$CURRENT" 2>/dev/null && pwd -P)"; then
64
+ if [[ -f "$resolved/.build-id" ]]; then
65
+ build_id="$(cat "$resolved/.build-id")"
66
+ else
67
+ build_id='(no .build-id)'
68
+ fi
69
+ printf ' current → %s\n' "$target"
70
+ printf ' build-id %s\n' "$build_id"
71
+ else
72
+ printf ' current ⚠️ symlink dangles → %s\n' "$target"
73
+ fi
74
+ else
75
+ printf ' current ⛔ no symlink at %s (run brain-up to bootstrap)\n' "$CURRENT"
76
+ fi
77
+
78
+ # Quick health check on the wiki socket — Caddy now answers, not Quartz.
79
+ if curl -fsS -o /dev/null --max-time 2 "$URL/" 2>/dev/null; then
80
+ echo ' wiki url reachable ✅'
81
+ else
82
+ echo ' wiki url unreachable ⚠️ '
83
+ fi
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env bash
2
+ # Start the second-brain wiki + sync watcher (blue/green serve edition).
3
+ # Idempotent — safe to re-run.
4
+ #
5
+ # Architecture (post blue/green migration + 2026-05-08 launchd handoff):
6
+ # - Caddy serves <vault>/.quartz/current/ statically on :8080 + brain.test.
7
+ # This script does NOT spawn Quartz's --serve dev server anymore.
8
+ # - Watcher A: `brain vault sync --watch` — DB <-> vault mirror.
9
+ # - Cold start: if <vault>/.quartz/current is missing or unhealthy, run a
10
+ # synchronous one-shot `python -m brain.wiki.build_swap` so the wiki has
11
+ # something to serve before Watcher B comes up.
12
+ # - Watcher B: `python -m brain.wiki.build_watcher` — re-renders the site
13
+ # into a fresh build dir and atomically retargets `current` on every
14
+ # vault change.
15
+ #
16
+ # Daemon supervision: BOTH watchers run under launchd via the LaunchAgents
17
+ # installed by `bin/brain-install-launchd` (KeepAlive=true → respawn within
18
+ # ~10s of any death). The pre-launchd nohup paths were removed because
19
+ # they silently lost daemons to terminal close / sleep-wake / unexplained
20
+ # crashes — `brain-status` would just go quiet and the user had to notice.
21
+ # This script delegates to install-launchd unconditionally; install-launchd
22
+ # is idempotent (rewrites + reboots the plists every run, no-ops if already
23
+ # loaded), so re-running brain-up is safe and self-healing if launchd has
24
+ # booted out the agents for any reason.
25
+ #
26
+ # Applies the brain Quartz overlay (`quartz_overrides/`) into the workspace
27
+ # BEFORE the cold-start build so the reload-signal transformer is wired in.
28
+ # Set BRAIN_NO_OVERLAY=1 to skip the overlay (use whatever's already in the
29
+ # workspace).
30
+ #
31
+ # Env overrides:
32
+ # BRAIN_VAULT_PATH (default: ~/brain-vault)
33
+ # BRAIN_WIKI_PORT (default: 8080 — for the URL we curl/open)
34
+ # BRAIN_OPEN_BROWSER (default: 1; set 0 to skip)
35
+ # BRAIN_NO_OVERLAY (default: 0; set 1 to skip the overlay step)
36
+ # BRAIN_WIKI_KEEP_BUILDS (default: 3 — old build dirs retained per swap)
37
+ # BRAIN_NO_BUILD_WATCHER (default: 0; set 1 to skip launchd supervision —
38
+ # used by the bin-script tests so they don't
39
+ # install LaunchAgents on the developer's
40
+ # machine. Name kept for backward compat with
41
+ # the pre-launchd nohup era.)
42
+ # BRAIN_LAUNCHCTL (default: launchctl on PATH) — passed through
43
+ # to install-launchd; tests stub it.
44
+ # BRAIN_LAUNCHD_DIR (default: ~/Library/LaunchAgents) — same.
45
+
46
+ set -euo pipefail
47
+
48
+ VAULT="${BRAIN_VAULT_PATH:-$HOME/brain-vault}"
49
+ QUARTZ_DIR="$VAULT/.quartz"
50
+ PORT="${BRAIN_WIKI_PORT:-8080}"
51
+ URL="http://localhost:$PORT"
52
+ KEEP="${BRAIN_WIKI_KEEP_BUILDS:-3}"
53
+
54
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
55
+ # Resolve $BRAIN_HOME from this script's location ($BRAIN_HOME/.shims/ under a
56
+ # pipx install, $repo_root/bin/ in a dev checkout). Honor an explicit
57
+ # BRAIN_HOME override if the launcher set one.
58
+ BRAIN_HOME_RESOLVED="${BRAIN_HOME:-$( cd "$SCRIPT_DIR/.." && pwd )}"
59
+
60
+ # Pid paths default to $BRAIN_HOME/run/ — NOT /tmp. macOS's tmp_cleaner reaps
61
+ # files left untouched for 3 days, silently vanishing a long-lived daemon's
62
+ # pid file so brain-status misreports it as stopped; $BRAIN_HOME/run/ is not
63
+ # reaped, and must match the fg wrappers' defaults. Log paths keep their legacy
64
+ # /tmp fallback (the launchd plists redirect real output to $BRAIN_HOME/logs/).
65
+ # Overridable (unset = unchanged) so the test suite can run this shim
66
+ # hermetically against a tmp dir, never colliding with a live brain-up install.
67
+ WATCH_PID="${BRAIN_WATCH_PID:-$BRAIN_HOME_RESOLVED/run/brain-watch.pid}"
68
+ WATCH_LOG="${BRAIN_WATCH_LOG:-/tmp/brain-watch.log}"
69
+ BUILD_PID="${BRAIN_BUILD_PID:-$BRAIN_HOME_RESOLVED/run/brain-build.pid}"
70
+ BUILD_LOG="${BRAIN_BUILD_LOG:-/tmp/brain-build.log}"
71
+
72
+ # Pick the Python interpreter once. BRAIN_PY (env var) wins; otherwise
73
+ # prefer python3 on PATH. The installed-shim flow (in $BRAIN_HOME/bin/)
74
+ # always has BRAIN_PY exported by the Python launcher in
75
+ # src/brain/bin/_launcher.py — but we keep the python3 fallback for
76
+ # users who invoke the shims directly outside the launcher (e.g. via
77
+ # launchd plists that themselves set PATH but not BRAIN_PY).
78
+ if [[ -n "${BRAIN_PY:-}" ]]; then
79
+ PY="$BRAIN_PY"
80
+ elif command -v python3 >/dev/null 2>&1; then
81
+ PY="$(command -v python3)"
82
+ else
83
+ echo "no python interpreter found (set BRAIN_PY=… to override)" >&2
84
+ exit 1
85
+ fi
86
+
87
+ apply_overlay() {
88
+ if [[ "${BRAIN_NO_OVERLAY:-0}" == '1' ]]; then
89
+ echo 'overlay: skipped (BRAIN_NO_OVERLAY=1)'
90
+ return 0
91
+ fi
92
+ if ! command -v brain >/dev/null 2>&1; then
93
+ echo 'overlay: skipped (brain CLI not on PATH)'
94
+ return 0
95
+ fi
96
+ # `--no-build` applies the overlay to the workspace without running
97
+ # `npx quartz build` — the build watcher does its own builds on every
98
+ # vault change, and we run a cold-start build below if needed.
99
+ if ! brain vault render --overlay --no-build --vault "$VAULT" 2>&1; then
100
+ echo 'overlay: failed — wiki will start with whatever is in the workspace' >&2
101
+ return 0
102
+ fi
103
+ }
104
+
105
+ current_healthy() {
106
+ # Healthy = `current` is a symlink, resolves to a directory, and that
107
+ # directory contains both `index.html` AND `.build-id`. Anything else
108
+ # (no symlink, dangling target, empty target, missing `.build-id`)
109
+ # means we need a cold-start build before the watcher comes up.
110
+ local cur="$QUARTZ_DIR/current"
111
+ if [[ ! -L "$cur" ]]; then
112
+ return 1
113
+ fi
114
+ local resolved
115
+ resolved="$(cd "$cur" 2>/dev/null && pwd -P)" || return 1
116
+ if [[ -z "$resolved" || ! -d "$resolved" ]]; then
117
+ return 1
118
+ fi
119
+ if [[ ! -f "$resolved/index.html" || ! -f "$resolved/.build-id" ]]; then
120
+ return 1
121
+ fi
122
+ return 0
123
+ }
124
+
125
+ cold_start_build() {
126
+ if current_healthy; then
127
+ echo 'cold-start build: skipping cold-start build (current/ is healthy)'
128
+ return 0
129
+ fi
130
+ if [[ ! -d "$QUARTZ_DIR" ]]; then
131
+ echo "quartz workspace not found at $QUARTZ_DIR" >&2
132
+ echo 'set up once with:' >&2
133
+ echo " git clone https://github.com/jackyzha0/quartz.git \"$QUARTZ_DIR\"" >&2
134
+ echo " cd \"$QUARTZ_DIR\" && npm install" >&2
135
+ echo " cp \"$SCRIPT_DIR/../quartz.config.ts\" ./quartz.config.ts # optional brain-tuned config" >&2
136
+ return 1
137
+ fi
138
+ echo 'cold-start build: running first-time build, this takes ~40s...'
139
+ local started
140
+ started="$(date +%s)"
141
+ if ! BRAIN_WIKI_RELOAD=1 "$PY" -m brain.wiki.build_swap \
142
+ --vault "$VAULT" --keep "$KEEP"; then
143
+ echo 'cold-start build: failed — aborting brain-up' >&2
144
+ return 1
145
+ fi
146
+ local elapsed=$(( $(date +%s) - started ))
147
+ echo "cold-start build: first build done in ${elapsed}s"
148
+ }
149
+
150
+ ensure_supervised() {
151
+ # brain (2026-05-08): single source of truth for daemon lifecycle.
152
+ # `bin/brain-install-launchd` writes both LaunchAgent plists, boots out
153
+ # any prior incarnation (so the script self-heals if launchd has the
154
+ # labels in a half-loaded state), and bootstraps them into the
155
+ # gui/$UID domain. From there on launchd's KeepAlive=true keeps both
156
+ # daemons alive across crashes, terminal-close, sleep/wake — the very
157
+ # failure modes the pre-launchd nohup paths used to lose.
158
+ #
159
+ # `BRAIN_NO_BUILD_WATCHER=1` keeps its name (back-compat with tests
160
+ # that use the pre-launchd flag) but its meaning is now "don't install
161
+ # LaunchAgents during this brain-up run." Tests rely on this so they
162
+ # don't accidentally drop plists into the developer's
163
+ # ~/Library/LaunchAgents.
164
+ if [[ "${BRAIN_NO_BUILD_WATCHER:-0}" == '1' ]]; then
165
+ echo 'launchd supervision: skipped (BRAIN_NO_BUILD_WATCHER=1)'
166
+ return 0
167
+ fi
168
+ if ! "${BRAIN_INSTALL_LAUNCHD:-brain-install-launchd}"; then
169
+ echo 'launchd supervision: brain-install-launchd failed — daemons NOT supervised' >&2
170
+ return 1
171
+ fi
172
+ }
173
+
174
+ wait_for_url() {
175
+ local tries=30
176
+ while ((tries-- > 0)); do
177
+ if curl -fsS -o /dev/null "$URL/"; then
178
+ return 0
179
+ fi
180
+ sleep 1
181
+ done
182
+ echo "warning: $URL did not respond within 30s — is Caddy running and serving $QUARTZ_DIR/current?" >&2
183
+ return 1
184
+ }
185
+
186
+ apply_overlay
187
+ cold_start_build
188
+ ensure_supervised
189
+
190
+ if [[ "${BRAIN_OPEN_BROWSER:-1}" == '1' ]]; then
191
+ if wait_for_url; then
192
+ if command -v open >/dev/null 2>&1; then
193
+ open "$URL"
194
+ elif command -v xdg-open >/dev/null 2>&1; then
195
+ xdg-open "$URL" >/dev/null 2>&1 &
196
+ fi
197
+ fi
198
+ fi
199
+
200
+ WATCH_PID_VALUE='?'
201
+ BUILD_PID_VALUE='?'
202
+ [[ -f "$WATCH_PID" ]] && WATCH_PID_VALUE="$(cat "$WATCH_PID")"
203
+ [[ -f "$BUILD_PID" ]] && BUILD_PID_VALUE="$(cat "$BUILD_PID")"
204
+
205
+ # brain (2026-05-08): note the supervisor in the report so the user
206
+ # knows their daemons survive terminal close + crashes. The pid '?'
207
+ # fallback handles the brief race between launchctl bootstrap and the
208
+ # wrapper writing the pid file (it's almost always populated by the
209
+ # time wait_for_url succeeds, but the fallback keeps the report
210
+ # readable in the unlikely race).
211
+ SUPERVISOR='launchd (KeepAlive)'
212
+ [[ "${BRAIN_NO_BUILD_WATCHER:-0}" == '1' ]] && SUPERVISOR='unsupervised (BRAIN_NO_BUILD_WATCHER=1)'
213
+
214
+ echo
215
+ echo '🧠 brain is up:'
216
+ echo " wiki: $URL (also brain.test if Caddy is configured)"
217
+ echo " vault: re-syncing on file changes (pid $WATCH_PID_VALUE)"
218
+ echo " build: re-rendering on vault changes (pid $BUILD_PID_VALUE)"
219
+ echo " supervisor: $SUPERVISOR"
220
+ echo ' stop: brain-down'
221
+ echo ' status: brain-status'