brainiac-cli 0.16.0__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 (77) hide show
  1. brain/__init__.py +39 -0
  2. brain/__main__.py +14 -0
  3. brain/_assets/AGENTS.md +564 -0
  4. brain/_assets/overlay/template/brand/brand-guide.md +24 -0
  5. brain/_assets/overlay/template/keywords/glossary.md +15 -0
  6. brain/_assets/overlay/template/people/roster.md +14 -0
  7. brain/_assets/overlay/template/voice/voice-profile.md +34 -0
  8. brain/_assets/routines/manifest.json +257 -0
  9. brain/_assets/scripts/brain-brief-mac.plist +59 -0
  10. brain/_assets/scripts/brain-brief.sh +74 -0
  11. brain/_assets/scripts/brain-synthesis-mac.plist +48 -0
  12. brain/_assets/scripts/brain-synthesis.sh +214 -0
  13. brain/_assets/scripts/install-brief-mac.sh +152 -0
  14. brain/_assets/scripts/install-brief-windows.ps1 +97 -0
  15. brain/_assets/scripts/register_tasks.py +386 -0
  16. brain/_assets/templates/company.md +21 -0
  17. brain/_assets/templates/concept.md +27 -0
  18. brain/_assets/templates/daily.md +20 -0
  19. brain/_assets/templates/decision.md +42 -0
  20. brain/_assets/templates/meeting.md +33 -0
  21. brain/_assets/templates/person.md +20 -0
  22. brain/_assets/templates/project.md +23 -0
  23. brain/_assets/templates/state-moc.md +40 -0
  24. brain/_version.py +12 -0
  25. brain/anchor.py +121 -0
  26. brain/audit.py +422 -0
  27. brain/backup.py +210 -0
  28. brain/brief.py +417 -0
  29. brain/capture.py +117 -0
  30. brain/chunk.py +249 -0
  31. brain/classification.py +134 -0
  32. brain/cli.py +1906 -0
  33. brain/config.py +368 -0
  34. brain/connect.py +362 -0
  35. brain/context.py +108 -0
  36. brain/core.py +3018 -0
  37. brain/doctor.py +1161 -0
  38. brain/egress.py +148 -0
  39. brain/embed.py +857 -0
  40. brain/encryption.py +217 -0
  41. brain/frontmatter.py +102 -0
  42. brain/golden_probe.py +678 -0
  43. brain/graph.py +369 -0
  44. brain/graphify.py +352 -0
  45. brain/index.py +1576 -0
  46. brain/ingest/__init__.py +19 -0
  47. brain/ingest/handlers/__init__.py +43 -0
  48. brain/ingest/handlers/base.py +95 -0
  49. brain/ingest/handlers/docx.py +78 -0
  50. brain/ingest/handlers/email.py +228 -0
  51. brain/ingest/handlers/html.py +142 -0
  52. brain/ingest/handlers/image.py +91 -0
  53. brain/ingest/handlers/pdf.py +99 -0
  54. brain/ingest/handlers/pptx.py +69 -0
  55. brain/ingest/handlers/tables.py +41 -0
  56. brain/ingest/handlers/text.py +43 -0
  57. brain/ingest/handlers/xlsx.py +100 -0
  58. brain/ingest/handlers/zip.py +163 -0
  59. brain/ingest/pipeline.py +839 -0
  60. brain/ingest/transcript.py +158 -0
  61. brain/init.py +870 -0
  62. brain/maintenance.py +2266 -0
  63. brain/mcp_adapter.py +217 -0
  64. brain/multihop.py +232 -0
  65. brain/notes.py +195 -0
  66. brain/overlay.py +183 -0
  67. brain/projection.py +79 -0
  68. brain/rerank.py +425 -0
  69. brain/snapshot.py +231 -0
  70. brain/update.py +743 -0
  71. brain/vectors.py +225 -0
  72. brainiac_cli-0.16.0.dist-info/METADATA +306 -0
  73. brainiac_cli-0.16.0.dist-info/RECORD +77 -0
  74. brainiac_cli-0.16.0.dist-info/WHEEL +5 -0
  75. brainiac_cli-0.16.0.dist-info/entry_points.txt +4 -0
  76. brainiac_cli-0.16.0.dist-info/licenses/LICENSE +202 -0
  77. brainiac_cli-0.16.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env bash
2
+ # brain-synthesis — the SECOND sanctioned host scheduled task (weekly).
3
+ #
4
+ # Owner decision 2026-07-11: metadata automation (the nightly maintain folds)
5
+ # closed every organization gap EXCEPT synthesis — state/MOC notes drift
6
+ # behind raw/ because writing prose needs a model, which the engine
7
+ # deliberately does not hold (no API keys in brain). This task closes that
8
+ # gap: once a week it runs a HEADLESS kb-curator session (Claude Code CLI)
9
+ # against EVERY vault registered in ~/.brainiac/workspaces.json — vault-
10
+ # generic by construction, never hardcoded to one vault.
11
+ #
12
+ # Task name: brain-synthesis (launchd label com.brainiac.synthesis)
13
+ # Schedule: weekly, Sunday 08:00 local (one hour after the 07:00 nightly,
14
+ # so the week's promote-scan + digest output is already fresh)
15
+ # Budget: routines/manifest.json locked_counts.host_os_scheduled == 2
16
+ # (amended 2026-07-11 for exactly this task — THE LOCK rule
17
+ # says amend the manifest first; this is that amendment's task)
18
+ # Runtime: host only. Each session is bounded (--max-turns) and scoped
19
+ # to the workspace folder; it uses the same `brain` CLI +
20
+ # skills the interactive sessions use.
21
+ # Logs: $BRAIN_LOG_DIR/synthesis-YYYY-MM-DD.log (30-day rotation)
22
+ # Skip rules: no claude CLI -> skip loudly; a workspace without a
23
+ # kb-curator skill -> skipped and logged, never an error.
24
+ set -u
25
+
26
+ LOG_DIR="${BRAIN_LOG_DIR:-$HOME/.brain/logs}"
27
+ mkdir -p "$LOG_DIR"
28
+ LOG="$LOG_DIR/synthesis-$(date +%F).log"
29
+ REGISTRY="${BRAIN_WORKSPACES_JSON:-$HOME/.brainiac/workspaces.json}"
30
+ CLAUDE_BIN="${BRAIN_CLAUDE_BIN:-$(command -v claude || echo "$HOME/.local/bin/claude")}"
31
+ MAX_TURNS="${BRAIN_SYNTHESIS_MAX_TURNS:-60}"
32
+
33
+ log() { printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$LOG"; }
34
+
35
+ # 30-day log rotation (same posture as brain-brief.sh)
36
+ find "$LOG_DIR" -name 'synthesis-*.log' -mtime +30 -delete 2>/dev/null
37
+
38
+ if [ ! -x "$CLAUDE_BIN" ]; then
39
+ log "SKIP-ALL: claude CLI not found ($CLAUDE_BIN) — synthesis needs a model"
40
+ exit 0
41
+ fi
42
+ if [ ! -f "$REGISTRY" ]; then
43
+ log "SKIP-ALL: no workspace registry at $REGISTRY"
44
+ exit 0
45
+ fi
46
+
47
+ PROMPT="$(cat <<'EOF'
48
+ Run the kb-curator skill for this vault, weekly synthesis pass. FIRST run
49
+ `brain status --json` and `brain doctor`: if the maintain heartbeat is stale
50
+ (daily branch > 48h) or any required surface is flagged, queue a dated entry
51
+ to .brain/memory/hot.md describing what is broken — the scheduled umbrella
52
+ cannot report its own death, so this weekly session is its watchdog. Then
53
+ the synthesis scope:
54
+ (1) refresh any state/MOC note whose content lags this week's raw/ ingests
55
+ (check `brain recent` and the freshness signal on `brain search`); supersede
56
+ rather than edit when the old claim was true-then; (2) review the Sunday
57
+ promote-scan candidates in .brain/memory/hot.md and promote the ones that
58
+ meet the one-idea-per-note bar into typed brain/ notes; (3) update index.md
59
+ zone stamps. Follow AGENTS.md conventions exactly (frontmatter, wikilinks,
60
+ classification explicit on every new note). Finish with `brain sync --publish`
61
+ and a one-paragraph summary of what changed. Stay inside this workspace.
62
+ EOF
63
+ )"
64
+
65
+ # Iterate REGISTERED host workspaces (dedup by vault_path) — vault-generic:
66
+ # a new `brainiac-install`/`brainiac-cowork-setup` registration is picked up
67
+ # on the next Sunday with zero extra wiring.
68
+ python3 - "$REGISTRY" <<'PY' | while IFS=$'\t' read -r VAULT WS; do
69
+ import json, sys
70
+ seen = set()
71
+ for e in json.load(open(sys.argv[1])).get("entries", []):
72
+ if e.get("target") != "host":
73
+ continue
74
+ v, w = e.get("vault_path", ""), e.get("workspace_path", "")
75
+ if not v or v in seen:
76
+ continue
77
+ seen.add(v)
78
+ print(f"{v}\t{w or v}")
79
+ PY
80
+ if [ ! -d "$WS/.claude/skills/kb-curator" ] && [ ! -d "$WS/.agents/skills/kb-curator" ]; then
81
+ log "SKIP $VAULT: no kb-curator skill in $WS"
82
+ continue
83
+ fi
84
+ log "START synthesis: vault=$VAULT workspace=$WS"
85
+ # OBS-04: capture the CLI's `--output-format stream-json` NDJSON to OUT_JSON
86
+ # via a DIRECT redirect — NEVER a `tee` pipe (finding [6]: piping claude's
87
+ # stdout couples its liveness to the log sink, so a full/unwritable $LOG
88
+ # would SIGPIPE-kill an otherwise-healthy run and trip a false "synthesis
89
+ # died" watchdog). The direct-to-file NDJSON is durable and survives a kill
90
+ # (each line is flushed as emitted); cost/duration/usage are lifted below
91
+ # from its single `"type":"result"` line — STRUCTURED data only, never
92
+ # scraped human text (a scraped format drifts silently to a wrong zero —
93
+ # HARDENED correction 4). The CLI requires --verbose alongside stream-json.
94
+ # A per-run unique name (date + pid + two $RANDOM draws) avoids the
95
+ # same-second collision the old date-second+pid name allowed (finding [9]).
96
+ OUT_JSON="$LOG_DIR/synthesis-out-$(date +%F)-$$-${RANDOM}${RANDOM}.json"
97
+ ( cd "$WS" && BRAIN_VAULT="$VAULT" "$CLAUDE_BIN" -p "$PROMPT" \
98
+ --max-turns "$MAX_TURNS" \
99
+ --permission-mode acceptEdits \
100
+ --output-format stream-json \
101
+ --verbose \
102
+ --allowedTools "Read,Grep,Glob,Edit,Write,Bash(brain *),Bash(python3 tools/*)" \
103
+ ) > "$OUT_JSON" 2>>"$LOG"
104
+ RC=$?
105
+ log "END synthesis: vault=$VAULT rc=$RC"
106
+ # Render a human-readable transcript from the structured NDJSON into $LOG
107
+ # (finding [5]): the prior `tee`'d raw NDJSON REPLACED the readable session
108
+ # transcript the weekly-watchdog prompt tells an operator to open. Structured
109
+ # fields only (assistant text turns + the final result) — never scraped
110
+ # prose. A killed run renders whatever streamed before the kill; the full
111
+ # machine NDJSON stays in $OUT_JSON (30-day retention) regardless.
112
+ python3 - "$OUT_JSON" >> "$LOG" 2>>"$LOG" <<'PY' || true
113
+ import json, sys
114
+ try:
115
+ with open(sys.argv[1]) as fh:
116
+ for raw in fh:
117
+ raw = raw.strip()
118
+ if not raw:
119
+ continue
120
+ try:
121
+ obj = json.loads(raw)
122
+ except Exception:
123
+ continue
124
+ if not isinstance(obj, dict):
125
+ continue
126
+ if obj.get("type") == "assistant":
127
+ for b in (obj.get("message") or {}).get("content", []) or []:
128
+ if isinstance(b, dict) and b.get("type") == "text":
129
+ t = (b.get("text") or "").strip()
130
+ if t:
131
+ print(t)
132
+ elif obj.get("type") == "result":
133
+ r = (obj.get("result") or "").strip()
134
+ if r:
135
+ print("--- result ---")
136
+ print(r)
137
+ except Exception:
138
+ pass
139
+ PY
140
+ # Heartbeat (WATCHDOG-01): the hourly maintain umbrella reads this file and
141
+ # raises action_required when the last successful synthesis is > 8 days old
142
+ # — nobody should have to read synthesis logs to learn the task died.
143
+ # OBS-04: the same entry is extended with {tokens, duration_s, est_cost_usd}
144
+ # so src/brain/maintenance.py's collect_health_metrics can lift the latest
145
+ # metered cost into health-history.jsonl. Honors $BRAIN_SYNTHESIS_STATE (the
146
+ # SAME override the Python readers consult — finding [3]: the writer used to
147
+ # hardcode $HOME, so a set env silently split writer and reader and the
148
+ # watchdog never fired).
149
+ STATE="${BRAIN_SYNTHESIS_STATE:-$HOME/.brain/synthesis-state.json}"
150
+ # Finding [3] companion: create the state file's parent dir before writing —
151
+ # a $BRAIN_SYNTHESIS_STATE override can point at a not-yet-existing dir, and
152
+ # a silent write failure there would freeze last_success and trip a FALSE
153
+ # "synthesis stale" watchdog even though synthesis ran fine.
154
+ mkdir -p "$(dirname "$STATE")" 2>>"$LOG" || true
155
+ python3 - "$STATE" "$VAULT" "$RC" "$OUT_JSON" <<'PY'
156
+ import json, sys, datetime
157
+ path, vault, rc, out_json = sys.argv[1], sys.argv[2], int(sys.argv[3]), sys.argv[4]
158
+ try:
159
+ state = json.load(open(path))
160
+ except Exception:
161
+ state = {}
162
+ entry = state.setdefault(vault, {})
163
+ entry["last_attempt"] = datetime.date.today().isoformat()
164
+ entry["rc"] = rc
165
+ if rc == 0:
166
+ entry["last_success"] = entry["last_attempt"]
167
+
168
+ # OBS-04 — meter from the structured --output-format stream-json NDJSON
169
+ # stream ONLY: scan every line, keep the (last) line with "type":"result" —
170
+ # that's the CLI's own final usage/cost summary for the run. Absent/zero/
171
+ # unparseable all record as null (never a scraped guess, never a bare 0
172
+ # masquerading as "measured").
173
+ cost = tokens = duration_s = None
174
+ try:
175
+ result_obj = None
176
+ with open(out_json) as fh:
177
+ for raw_line in fh:
178
+ raw_line = raw_line.strip()
179
+ if not raw_line:
180
+ continue
181
+ try:
182
+ obj = json.loads(raw_line)
183
+ except Exception:
184
+ continue
185
+ if isinstance(obj, dict) and obj.get("type") == "result":
186
+ result_obj = obj
187
+ if result_obj is not None:
188
+ c = result_obj.get("total_cost_usd")
189
+ if isinstance(c, (int, float)) and c > 0:
190
+ cost = round(c, 4)
191
+ dm = result_obj.get("duration_ms")
192
+ if isinstance(dm, (int, float)):
193
+ duration_s = round(dm / 1000, 1)
194
+ usage = result_obj.get("usage") or {}
195
+ parts = [usage.get(k) for k in (
196
+ "input_tokens", "output_tokens",
197
+ "cache_creation_input_tokens", "cache_read_input_tokens")]
198
+ nums = [p for p in parts if isinstance(p, (int, float))]
199
+ if nums:
200
+ tokens = int(sum(nums))
201
+ except Exception:
202
+ pass
203
+ entry["tokens"] = tokens
204
+ entry["duration_s"] = duration_s
205
+ entry["est_cost_usd"] = cost
206
+ json.dump(state, open(path, "w"), indent=1)
207
+ PY
208
+ done
209
+
210
+ # 30-day rotation for the per-run usage-json captures (same posture as logs).
211
+ find "$LOG_DIR" -name 'synthesis-out-*.json' -mtime +30 -delete 2>/dev/null
212
+
213
+ log "synthesis run complete"
214
+ exit 0
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env bash
2
+ # install-brief-mac.sh — install brain-daily-brief as a macOS launchd LaunchAgent (UX-02)
3
+ #
4
+ # Usage:
5
+ # BRAIN_VAULT=/path/to/vault bash scripts/install-brief-mac.sh
6
+ #
7
+ # Prerequisites:
8
+ # - brain binary must be on PATH (test: brain --version)
9
+ # - BRAIN_AUDIT_KEY_PEM must be set OR the key stored in the macOS Keychain
10
+ # under service "profile-a-brain-audit-key" (security find-generic-password -s profile-a-brain-audit-key -w)
11
+ #
12
+ # Uninstall (per-vault label — the script prints the exact paths on install):
13
+ # launchctl unload ~/Library/LaunchAgents/com.brainiac.nightly.<id>.plist
14
+ # rm ~/Library/LaunchAgents/com.brainiac.nightly.<id>.plist
15
+ set -euo pipefail
16
+
17
+ VAULT="${BRAIN_VAULT:?BRAIN_VAULT must be set (path to your brain vault)}"
18
+ SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ PLIST_SRC="$SCRIPTS/brain-brief-mac.plist"
20
+
21
+ # Per-vault launchd label (SINGLE SOURCE OF TRUTH: brain.config.nightly_label),
22
+ # so two registered vaults never install to one shared label and clobber each
23
+ # other's nightly job. Prefer the installed package; fall back to a shell
24
+ # reimplementation of vault_slug8 (sha256 of the resolved vault path, first 8).
25
+ LABEL=""
26
+ BRAIN_BIN="$(command -v brain || true)"
27
+ if [ -n "$BRAIN_BIN" ]; then
28
+ VENV_PY="$(dirname "$BRAIN_BIN")/python3"
29
+ [ -x "$VENV_PY" ] || VENV_PY="python3"
30
+ LABEL="$(BRAIN_VAULT="$VAULT" "$VENV_PY" - <<'PY' 2>/dev/null || true
31
+ import os
32
+ from brain.config import nightly_label
33
+ print(nightly_label(os.environ["BRAIN_VAULT"]))
34
+ PY
35
+ )"
36
+ fi
37
+ if [ -z "$LABEL" ]; then
38
+ _vr="$(python3 -c 'import os,sys; print(os.path.realpath(os.path.expanduser(sys.argv[1])))' "$VAULT" 2>/dev/null || echo "$VAULT")"
39
+ _h="$(printf '%s' "$_vr" | shasum -a 256 | cut -c1-8)"
40
+ LABEL="com.brainiac.nightly.$_h"
41
+ fi
42
+
43
+ LEGACY_LABEL="com.profile-a-brain.daily-brief"
44
+ LEGACY_PLIST="$HOME/Library/LaunchAgents/$LEGACY_LABEL.plist"
45
+ PLIST_DST="$HOME/Library/LaunchAgents/$LABEL.plist"
46
+ LOGDIR="${BRAIN_LOG_DIR:-$HOME/.brain/logs}"
47
+
48
+ # Audit signing key: the drain resolves it from the macOS Keychain AT RUNTIME
49
+ # (src/brain/audit.py resolve_signing_key(), env -> keychain fallthrough), so
50
+ # the plist does NOT carry the key material. Baking the PEM into the plist
51
+ # (the old behavior) left the private key in a plaintext file under
52
+ # ~/Library/LaunchAgents. We only bake a value when the operator explicitly
53
+ # injected BRAIN_AUDIT_KEY_PEM (custom custody / unattended box) — and the
54
+ # plist is chmod 600 below either way.
55
+ KEYCHAIN_SERVICE="${BRAIN_AUDIT_KEYCHAIN_SERVICE:-profile-a-brain-audit-key}"
56
+ AUDIT_KEY="${BRAIN_AUDIT_KEY_PEM:-}"
57
+ if [ -z "$AUDIT_KEY" ]; then
58
+ if ! security find-generic-password -s "$KEYCHAIN_SERVICE" -a "$USER" >/dev/null 2>&1; then
59
+ echo "WARNING: No BRAIN_AUDIT_KEY_PEM and no Keychain entry for '$KEYCHAIN_SERVICE'." >&2
60
+ echo " Captures will not be signed (drain fails closed). Run 'brain audit-key'" >&2
61
+ echo " (create-if-absent, never rotates) — no reinstall needed after." >&2
62
+ fi
63
+ fi
64
+
65
+ mkdir -p "$LOGDIR" "$HOME/Library/LaunchAgents"
66
+
67
+ # Render the plist to a scratch file first so we can diff against whatever is
68
+ # already installed — the nightly-diff contract (docs/install/plugin-distribution.md
69
+ # §3): reinstall/reload iff the rendered body actually changed, otherwise report
70
+ # unchanged and touch nothing (idempotent re-runs, no needless launchctl churn).
71
+ PLIST_NEW="$(mktemp "${TMPDIR:-/tmp}/brain-brief-mac.plist.XXXXXX")"
72
+ trap 'rm -f "$PLIST_NEW"' EXIT
73
+
74
+ sed -e "s|LABEL_PLACEHOLDER|$LABEL|g" \
75
+ -e "s|SCRIPTS_DIR|$SCRIPTS|g" \
76
+ -e "s|VAULT_PATH|$VAULT|g" \
77
+ -e "s|HOME_DIR|$HOME|g" \
78
+ -e "s|AUDIT_KEY_PEM_PLACEHOLDER|$AUDIT_KEY|g" \
79
+ -e "s|SWEEP_DIRS_PLACEHOLDER|${BRAIN_WORKSPACE_SWEEP_DIRS:-}|g" \
80
+ "$PLIST_SRC" > "$PLIST_NEW"
81
+
82
+ # ponytail: real launchctl talks to the login launchd domain by uid, not by
83
+ # $HOME — a sandboxed HOME does NOT sandbox launchctl. BRAIN_LAUNCHD_DRY_RUN=1
84
+ # (or a non-default HOME, detected below) skips the real bootstrap/load calls
85
+ # so test/sandbox runs never touch the real user's launchd domain.
86
+ if [ -z "${BRAIN_LAUNCHD_DRY_RUN:-}" ] && [ "$HOME" != "$(eval echo ~"$USER")" ]; then
87
+ BRAIN_LAUNCHD_DRY_RUN=1
88
+ fi
89
+
90
+ # One-time migration off the legacy SHARED label: if the old single-label plist
91
+ # exists (and isn't this vault's per-vault plist), unload + remove it so the two
92
+ # host vaults stop competing for one launchd job. Idempotent — absent after the
93
+ # first migrated install.
94
+ if [ "$LEGACY_PLIST" != "$PLIST_DST" ] && [ -f "$LEGACY_PLIST" ]; then
95
+ if [ "${BRAIN_LAUNCHD_DRY_RUN:-0}" = "1" ]; then
96
+ echo "migration DRY-RUN: would retire legacy shared label $LEGACY_LABEL"
97
+ else
98
+ launchctl unload "$LEGACY_PLIST" 2>/dev/null || true
99
+ rm -f "$LEGACY_PLIST"
100
+ echo "migrated: retired legacy shared nightly label $LEGACY_LABEL (now per-vault)"
101
+ fi
102
+ fi
103
+
104
+ if [ -f "$PLIST_DST" ] && cmp -s "$PLIST_NEW" "$PLIST_DST"; then
105
+ echo "nightly: unchanged (label: $LABEL) — nothing to reload"
106
+ echo " Status: launchctl list $LABEL"
107
+ exit 0
108
+ fi
109
+
110
+ cp "$PLIST_NEW" "$PLIST_DST"
111
+ chmod 600 "$PLIST_DST"
112
+
113
+ if [ "${BRAIN_LAUNCHD_DRY_RUN:-0}" = "1" ]; then
114
+ echo "DRY-RUN (sandboxed HOME or BRAIN_LAUNCHD_DRY_RUN=1) — plist written, launchd NOT reloaded. Would run:"
115
+ echo " launchctl unload '$PLIST_DST'"
116
+ echo " launchctl load -w '$PLIST_DST'"
117
+ echo " To load it for real, re-run this from a normal (non-sandboxed) terminal, or run the two commands above."
118
+ else
119
+ # (Re)load the agent.
120
+ launchctl unload "$PLIST_DST" 2>/dev/null || true
121
+ launchctl load -w "$PLIST_DST"
122
+ fi
123
+
124
+ echo "✓ Installed: $LABEL (hourly umbrella, vault: $VAULT)"
125
+
126
+ # --- brain-synthesis (the SECOND sanctioned task; THE LOCK host=2) ---------
127
+ # One per HOST (label has no vault id): the script iterates every registered
128
+ # vault in ~/.brainiac/workspaces.json, so re-running this installer for a
129
+ # second vault just refreshes the same host-level agent. Weekly Sun 08:00.
130
+ SYNTH_SRC="$SCRIPTS/brain-synthesis-mac.plist"
131
+ SYNTH_DST="$HOME/Library/LaunchAgents/com.brainiac.synthesis.plist"
132
+ if [ -f "$SYNTH_SRC" ]; then
133
+ SYNTH_NEW="$(mktemp "${TMPDIR:-/tmp}/brain-synthesis-mac.plist.XXXXXX")"
134
+ sed -e "s|SCRIPTS_DIR|$SCRIPTS|g" -e "s|HOME_DIR|$HOME|g" \
135
+ "$SYNTH_SRC" > "$SYNTH_NEW"
136
+ if ! cmp -s "$SYNTH_NEW" "$SYNTH_DST" 2>/dev/null; then
137
+ cp "$SYNTH_NEW" "$SYNTH_DST"
138
+ chmod 600 "$SYNTH_DST"
139
+ if [ "${BRAIN_LAUNCHD_DRY_RUN:-0}" = "1" ]; then
140
+ echo "DRY-RUN — synthesis plist written, launchd NOT reloaded."
141
+ else
142
+ launchctl unload "$SYNTH_DST" 2>/dev/null || true
143
+ launchctl load -w "$SYNTH_DST"
144
+ fi
145
+ echo "✓ Installed: com.brainiac.synthesis (weekly Sun 08:00, all registered vaults)"
146
+ fi
147
+ rm -f "$SYNTH_NEW"
148
+ fi
149
+ echo " Logs: $LOGDIR/brief-YYYY-MM-DD.log"
150
+ echo " Status: launchctl list $LABEL"
151
+ echo " Dry-run: launchctl start $LABEL"
152
+ echo " Uninstall: launchctl unload '$PLIST_DST' && rm '$PLIST_DST'"
@@ -0,0 +1,97 @@
1
+ # install-brief-windows.ps1 — install brain-daily-brief as a Windows Scheduled Task (UX-02)
2
+ #
3
+ # Usage (run as the same user who will run brain, NOT as Administrator):
4
+ # .\scripts\install-brief-windows.ps1 -VaultPath C:\Users\you\brain\vault
5
+ #
6
+ # Prerequisites:
7
+ # - brain.exe must be on PATH (test: brain --version)
8
+ #
9
+ # Uninstall:
10
+ # Unregister-ScheduledTask -TaskName "brain-daily-brief" -Confirm:$false
11
+ #
12
+ # Threat model: the signing key lives in Windows Credential Manager (service
13
+ # profile-a-brain-audit-key) and is resolved AT RUNTIME by the brain process
14
+ # itself (audit.py resolve_signing_key(), env -> keyring fallthrough). It is
15
+ # NEVER embedded in this script, the task action, or the task XML. See
16
+ # docs/operations/s09-evidence.md § Scheduled-task threat model.
17
+ param(
18
+ [Parameter(Mandatory=$true)]
19
+ [string]$VaultPath,
20
+
21
+ [string]$BrainExe = "brain",
22
+ [string]$TaskName = "",
23
+ [string]$LogDir = "$env:USERPROFILE\.brain\logs"
24
+ )
25
+ $ErrorActionPreference = "Stop"
26
+
27
+ # Per-vault task name (mirrors brain.config.nightly_label's sha256(resolved-vault)[:8]
28
+ # slug) so two registered vaults don't install to one shared Scheduled Task and
29
+ # clobber each other's job. Pass -TaskName explicitly to override.
30
+ $LegacyTask = "brain-daily-brief"
31
+ if ([string]::IsNullOrEmpty($TaskName)) {
32
+ $resolved = Resolve-Path -LiteralPath $VaultPath -ErrorAction SilentlyContinue
33
+ $vpath = if ($resolved) { $resolved.Path } else { $VaultPath }
34
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($vpath)
35
+ $hash = ([System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes) |
36
+ ForEach-Object { $_.ToString('x2') }) -join ''
37
+ $TaskName = "brain-daily-brief-" + $hash.Substring(0, 8)
38
+ }
39
+
40
+ # One-time migration off the legacy SHARED task name.
41
+ if ($TaskName -ne $LegacyTask) {
42
+ $existing = Get-ScheduledTask -TaskName $LegacyTask -ErrorAction SilentlyContinue
43
+ if ($existing) {
44
+ Unregister-ScheduledTask -TaskName $LegacyTask -Confirm:$false
45
+ Write-Host "migrated: retired legacy shared task $LegacyTask (now per-vault)"
46
+ }
47
+ }
48
+
49
+ New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
50
+
51
+ # Audit signing key: provision create-if-absent (never rotates) into the
52
+ # Credential Manager via the engine; the drain resolves it at runtime, so
53
+ # nothing key-shaped goes into the task action below.
54
+ try {
55
+ & $BrainExe audit-key
56
+ } catch {
57
+ Write-Warning "audit-key provisioning failed: $_"
58
+ Write-Warning "Captures will not be drained (drain fails closed, unsigned)."
59
+ Write-Warning "Run '$BrainExe audit-key' manually later — no reinstall needed."
60
+ }
61
+
62
+ $logFile = "$LogDir\brief-$(Get-Date -Format 'yyyy-MM-dd').log"
63
+
64
+ # Task action: the `maintain` umbrella -- sync --publish + brief, PLUS the
65
+ # date-gated branches (Mon=health, Tue=integrity, Sun=digest,
66
+ # 1st=graphify-documented-only). This is THE single sanctioned OS task
67
+ # (`brain-nightly`, persistence-budget.md THE LOCK) -- see
68
+ # src/brain/core.py BrainCore.maintain. routines/manifest.json id "brain-nightly".
69
+ $scriptBlock = @"
70
+ `$env:BRAIN_VAULT = '$VaultPath'
71
+ `$log = '$LogDir\brief-' + (Get-Date -Format 'yyyy-MM-dd') + '.log'
72
+ '=== brain-daily-brief / brain-nightly ' + (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') + ' ===' | Out-File -Append -FilePath `$log
73
+ & '$BrainExe' maintain --json 2>&1 | Out-File -Append -FilePath `$log
74
+ # Rotate logs older than 30 days
75
+ Get-ChildItem '$LogDir\brief-*.log' | Where-Object { `$_.LastWriteTime -lt (Get-Date).AddDays(-30) } | Remove-Item -Force
76
+ "@
77
+
78
+ $action = New-ScheduledTaskAction -Execute "powershell.exe" `
79
+ -Argument "-NonInteractive -WindowStyle Hidden -Command `"$scriptBlock`""
80
+ # HOURLY (owner decision 2026-07-11, parity with the macOS installer):
81
+ # ingestion is frequent — every firing runs the incremental/idempotent work;
82
+ # weekly/monthly branches stay date-gated inside `brain maintain`.
83
+ $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).Date `
84
+ -RepetitionInterval (New-TimeSpan -Hours 1)
85
+ $settings = New-ScheduledTaskSettingsSet `
86
+ -RunOnlyIfNetworkAvailable:$false `
87
+ -StartWhenAvailable `
88
+ -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
89
+
90
+ Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
91
+ -Settings $settings -RunLevel Limited -Force | Out-Null
92
+
93
+ Write-Host "Installed: $TaskName (hourly umbrella)"
94
+ Write-Host " Logs: $LogDir\brief-YYYY-MM-DD.log"
95
+ Write-Host " Status: Get-ScheduledTask -TaskName '$TaskName'"
96
+ Write-Host " Dry-run: Start-ScheduledTask -TaskName '$TaskName'"
97
+ Write-Host " Uninstall: Unregister-ScheduledTask -TaskName '$TaskName' -Confirm:`$false"