brainiac-cli 0.18.0__tar.gz → 0.18.2__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.
- {brainiac_cli-0.18.0/src/brainiac_cli.egg-info → brainiac_cli-0.18.2}/PKG-INFO +1 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/pyproject.toml +1 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/brain-brief.sh +8 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_version.py +1 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/core.py +22 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/cos.py +68 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/embed.py +36 -6
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/spine.py +14 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2/src/brainiac_cli.egg-info}/PKG-INFO +1 -1
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/LICENSE +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/MANIFEST.in +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/README.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/setup.cfg +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/__init__.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/__main__.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/AGENTS.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/brand/brand-guide.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/cos/priorities.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/keywords/glossary.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/people/roster.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/voice/voice-profile.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/routines/manifest.json +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/brain-brief-mac.plist +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/brain-synthesis-mac.plist +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/brain-synthesis.sh +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/install-brief-mac.sh +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/install-brief-windows.ps1 +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/register_tasks.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/company.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/concept.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/daily.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/decision.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/meeting.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/person.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/project.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/templates/state-moc.md +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/anchor.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/audit.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/backup.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/brief.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/capture.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/chunk.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/classification.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/cli.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/config.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/connect.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/context.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/doctor.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/egress.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/encryption.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/frontmatter.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/golden_probe.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/graph.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/graphify.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/inbox.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/index.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/__init__.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/__init__.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/base.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/docx.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/email.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/html.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/image.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/pdf.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/pptx.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/tables.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/text.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/xlsx.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/handlers/zip.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/pipeline.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/ingest/transcript.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/init.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/maintenance.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/mcp_adapter.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/multihop.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/notes.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/overlay.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/projection.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/rerank.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/retro.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/snapshot.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/update.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/vectors.py +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brainiac_cli.egg-info/SOURCES.txt +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brainiac_cli.egg-info/dependency_links.txt +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brainiac_cli.egg-info/entry_points.txt +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brainiac_cli.egg-info/requires.txt +0 -0
- {brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brainiac_cli.egg-info/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
# check. The IMPORT package stays `brain` and the console script stays
|
|
9
9
|
# `brain` — only the distribution/project name changes.
|
|
10
10
|
name = "brainiac-cli"
|
|
11
|
-
version = "0.18.
|
|
11
|
+
version = "0.18.2"
|
|
12
12
|
description = "Brainiac — local any-LLM second-brain core engine + brain CLI (Markdown truth, derived SQLite index)."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
license = "Apache-2.0"
|
|
@@ -63,6 +63,14 @@ fi
|
|
|
63
63
|
|
|
64
64
|
{
|
|
65
65
|
echo "=== brain-daily-brief / brain-nightly $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
|
66
|
+
# cos-broker FIRST: the host broker fold claims VM proposal drops, releases due
|
|
67
|
+
# holds, runs auto-capture, enqueues the owner batch, and renders
|
|
68
|
+
# shared/spine-summary.md. Nothing else schedules it -- without this line the
|
|
69
|
+
# VM proposes into a drop dir no one drains (measured 2026-07-16: spine DB empty,
|
|
70
|
+
# candidates unclaimed ~29h) . It runs BEFORE maintain so notes it signs are
|
|
71
|
+
# indexed + published by the same run's sync. Host-only, idempotent, and
|
|
72
|
+
# NON-FATAL: a broker failure must never cost the vault its capture-drain floor.
|
|
73
|
+
"$BRAIN_BIN" cos-broker --json || echo "brain-brief: cos-broker fold failed (non-fatal), continuing to maintain"
|
|
66
74
|
# maintain: sync --publish (drain pending captures + reconcile index +
|
|
67
75
|
# republish snapshot) + brief, THEN whichever of health/integrity/digest is
|
|
68
76
|
# due today (date-gated). graphify is documented-only (no build invoked --
|
|
@@ -482,6 +482,28 @@ class BrainCore:
|
|
|
482
482
|
except ValueError as exc:
|
|
483
483
|
skipped.append({"draft": draft.name, "reason": f"unsafe-id (fail-closed): {exc}"})
|
|
484
484
|
continue
|
|
485
|
+
# COS owner-gate integrity: never sign a draft whose id is still
|
|
486
|
+
# awaiting the owner's accept/reject in the proposal pipeline.
|
|
487
|
+
# The gated copy is authoritative; only the owner's answer may
|
|
488
|
+
# promote it (consume_answers moves it here itself on accept).
|
|
489
|
+
# Quarantined rather than skipped-in-place — see
|
|
490
|
+
# cos.quarantine_gate_bypass for why leaving it would let a
|
|
491
|
+
# later REJECT still leak the content on the next drain.
|
|
492
|
+
try:
|
|
493
|
+
from . import cos as _cos_mod
|
|
494
|
+
if nid in _cos_mod.undecided_proposal_ids(self.vault):
|
|
495
|
+
dest = _cos_mod.quarantine_gate_bypass(self.vault, draft)
|
|
496
|
+
skipped.append({
|
|
497
|
+
"draft": draft.name,
|
|
498
|
+
"reason": f"gate-bypass: {nid!r} is awaiting the owner's "
|
|
499
|
+
f"accept/reject — quarantined to {dest.parent.name}/{dest.name}",
|
|
500
|
+
})
|
|
501
|
+
continue
|
|
502
|
+
except Exception as exc: # noqa: BLE001 — never break the drain floor
|
|
503
|
+
skipped.append({"draft": draft.name,
|
|
504
|
+
"reason": f"gate-check-failed (fail-closed, left in place): "
|
|
505
|
+
f"{type(exc).__name__}: {exc}"})
|
|
506
|
+
continue
|
|
485
507
|
# raw source -> raw/<id>.md ; otherwise a brain note -> resources/.
|
|
486
508
|
if note.type == "source" or note.zone == "raw":
|
|
487
509
|
rel, subtree = f"raw/{nid}.md", "raw"
|
|
@@ -413,6 +413,49 @@ def claim_drops(vault, now: _dt.datetime | None = None) -> dict[str, Any]:
|
|
|
413
413
|
|
|
414
414
|
|
|
415
415
|
# -- proposal state helpers ----------------------------------------------------
|
|
416
|
+
def undecided_proposal_ids(vault) -> set[str]:
|
|
417
|
+
"""Candidate ids the owner has NOT yet ruled on — staged in the VM's
|
|
418
|
+
proposal drop, or claimed into host ``pending/``. Both states mean "the
|
|
419
|
+
owner's answer is still outstanding".
|
|
420
|
+
|
|
421
|
+
A capture draft carrying one of these ids is a GATE BYPASS: the same
|
|
422
|
+
content is simultaneously travelling the gated route (cos-propose ->
|
|
423
|
+
broker -> owner batch -> selective commit) and the UNGATED one
|
|
424
|
+
(draft-capture -> capture-inbox -> signed on the next drain). The ungated
|
|
425
|
+
one always wins the race, so the owner gets asked to approve a note that is
|
|
426
|
+
already authoritative in the vault, and a "reject" has nothing to reject.
|
|
427
|
+
|
|
428
|
+
Measured 2026-07-16 (run 14/15): the COS skill forbids SUBSTITUTING
|
|
429
|
+
draft-capture for cos-propose in Phase 1.6, but Phase 5 separately requires
|
|
430
|
+
draft-capture for anything the owner must see. A finding that is also an
|
|
431
|
+
ingestion candidate satisfies both rules and bypasses the gate — no rule
|
|
432
|
+
violated. Policy cannot fix a collision between two obeyed rules; the
|
|
433
|
+
engine must.
|
|
434
|
+
"""
|
|
435
|
+
ids: set[str] = set()
|
|
436
|
+
for d in (proposal_drop_dir(vault), proposals_dir(vault) / "pending"):
|
|
437
|
+
if d.is_dir():
|
|
438
|
+
ids.update(p.stem for p in d.glob("*.md"))
|
|
439
|
+
return ids
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def quarantine_gate_bypass(vault, draft: Path, *, now: _dt.datetime | None = None) -> Path:
|
|
443
|
+
"""Move a bypassing capture draft out of the drain's path, reversibly.
|
|
444
|
+
|
|
445
|
+
NOT deleted (recoverable, auditable) and NOT left in place: leaving it
|
|
446
|
+
would re-offer it to every subsequent drain, and — the real hazard — a
|
|
447
|
+
later owner REJECT clears the gated copy out of ``pending/``, after which
|
|
448
|
+
the leftover draft no longer matches an undecided id and the next drain
|
|
449
|
+
would sign the very content the owner just rejected.
|
|
450
|
+
"""
|
|
451
|
+
dest_dir = host_dir(vault) / "gate-bypass"
|
|
452
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
453
|
+
stamp = (now or _utcnow()).strftime("%Y%m%dT%H%M%S")
|
|
454
|
+
dest = dest_dir / f"{stamp}-{draft.name}"
|
|
455
|
+
shutil.move(str(draft), dest)
|
|
456
|
+
return dest
|
|
457
|
+
|
|
458
|
+
|
|
416
459
|
def _pending_metas(vault) -> list[dict[str, Any]]:
|
|
417
460
|
pending = proposals_dir(vault) / "pending"
|
|
418
461
|
out = []
|
|
@@ -1587,7 +1630,16 @@ def ingest_sweep(vault, *, downloads_dir: Path | str | None = None,
|
|
|
1587
1630
|
continue
|
|
1588
1631
|
fname = next((n for n in safe if (ddir / n).exists()), None)
|
|
1589
1632
|
if fname is None:
|
|
1590
|
-
|
|
1633
|
+
# WHY matters: "not in the downloads dir at all" is a different
|
|
1634
|
+
# problem from "present but rejected by a guard", and the old
|
|
1635
|
+
# bare-name list could not tell them apart — the 2026-07-16
|
|
1636
|
+
# field read was "sweeper stalled ~32h" when the sweeper was in
|
|
1637
|
+
# fact correctly refusing stale namesakes. Reasons are additive:
|
|
1638
|
+
# the plain names stay in `unmatched` for compatibility.
|
|
1639
|
+
report["unmatched"].extend(safe)
|
|
1640
|
+
report.setdefault("unmatched_reasons", []).extend(
|
|
1641
|
+
{"filename": n, "reason": "not present in the downloads dir"}
|
|
1642
|
+
for n in safe)
|
|
1591
1643
|
continue
|
|
1592
1644
|
cand = ddir / fname
|
|
1593
1645
|
if cand.is_symlink() or not cand.is_file():
|
|
@@ -1605,11 +1657,26 @@ def ingest_sweep(vault, *, downloads_dir: Path | str | None = None,
|
|
|
1605
1657
|
if isinstance(want, int) and want > 0:
|
|
1606
1658
|
if abs(size - want) > max(want * INGEST_SWEEP_SIZE_TOLERANCE, 4096):
|
|
1607
1659
|
report["unmatched"].append(fname) # wrong file — leave it
|
|
1660
|
+
report.setdefault("unmatched_reasons", []).append({
|
|
1661
|
+
"filename": fname,
|
|
1662
|
+
"reason": f"size mismatch: on disk {size}B, manifest expects "
|
|
1663
|
+
f"{want}B (tolerance "
|
|
1664
|
+
f"{int(max(want * INGEST_SWEEP_SIZE_TOLERANCE, 4096))}B) "
|
|
1665
|
+
f"— a DIFFERENT file of the same name",
|
|
1666
|
+
})
|
|
1608
1667
|
continue
|
|
1609
1668
|
ets = _parse_ts(str(entry.get("ts", "")))
|
|
1610
1669
|
if ets is not None and cand.stat().st_mtime < (
|
|
1611
1670
|
ets.timestamp() - INGEST_SWEEP_SKEW_SECONDS):
|
|
1612
1671
|
report["unmatched"].append(fname) # older than the download
|
|
1672
|
+
age_h = (ets.timestamp() - cand.stat().st_mtime) / 3600.0
|
|
1673
|
+
report.setdefault("unmatched_reasons", []).append({
|
|
1674
|
+
"filename": fname,
|
|
1675
|
+
"reason": f"stale namesake: file mtime is {age_h:.1f}h OLDER than the "
|
|
1676
|
+
f"manifest's download ts {ets.isoformat()} (skew allowance "
|
|
1677
|
+
f"{INGEST_SWEEP_SKEW_SECONDS}s) — the VM's download did not "
|
|
1678
|
+
f"land; this is a pre-existing file with the same name",
|
|
1679
|
+
})
|
|
1613
1680
|
continue
|
|
1614
1681
|
dest = _unique_dest(inbox, fname)
|
|
1615
1682
|
if not dry_run:
|
|
@@ -210,8 +210,8 @@ class ArcticEmbedder:
|
|
|
210
210
|
from fastembed import TextEmbedding
|
|
211
211
|
except Exception as exc: # pragma: no cover - exercised when absent
|
|
212
212
|
raise EmbedderUnavailable(
|
|
213
|
-
"fastembed/onnxruntime not importable; install the 'embed' "
|
|
214
|
-
"extra or bundle the model"
|
|
213
|
+
f"fastembed/onnxruntime not importable; install the 'embed' "
|
|
214
|
+
f"extra or bundle the model: {type(exc).__name__}: {exc}"
|
|
215
215
|
) from exc
|
|
216
216
|
self._model = TextEmbedding(
|
|
217
217
|
model_name=self.model_id, cache_dir=self._cache_dir,
|
|
@@ -315,7 +315,8 @@ class CatalogEmbedder:
|
|
|
315
315
|
from fastembed import TextEmbedding
|
|
316
316
|
except Exception as exc: # pragma: no cover
|
|
317
317
|
raise EmbedderUnavailable(
|
|
318
|
-
"fastembed/onnxruntime not importable for CatalogEmbedder"
|
|
318
|
+
f"fastembed/onnxruntime not importable for CatalogEmbedder: "
|
|
319
|
+
f"{type(exc).__name__}: {exc}"
|
|
319
320
|
) from exc
|
|
320
321
|
self._register_custom()
|
|
321
322
|
self._model = TextEmbedding(
|
|
@@ -470,7 +471,8 @@ class OnnxEmbedder:
|
|
|
470
471
|
from tokenizers import Tokenizer
|
|
471
472
|
except Exception as exc: # pragma: no cover
|
|
472
473
|
raise EmbedderUnavailable(
|
|
473
|
-
"onnxruntime/tokenizers not importable for OnnxEmbedder"
|
|
474
|
+
f"onnxruntime/tokenizers not importable for OnnxEmbedder: "
|
|
475
|
+
f"{type(exc).__name__}: {exc}"
|
|
474
476
|
) from exc
|
|
475
477
|
try:
|
|
476
478
|
onnx_path, base = self._resolve_model_files()
|
|
@@ -721,14 +723,42 @@ _HASH_FALLBACK_MSG = (
|
|
|
721
723
|
)
|
|
722
724
|
|
|
723
725
|
|
|
726
|
+
def embedder_unavailable_reason() -> str:
|
|
727
|
+
"""Probe WHY no real embedder resolved and report the ACTUAL error.
|
|
728
|
+
|
|
729
|
+
The old message guessed ("onnxruntime/tokenizers missing OR the model is
|
|
730
|
+
absent") because both ``available()`` and the raise sites discard the
|
|
731
|
+
exception. Field cost (2026-07-16): the Cowork VM reported the identical
|
|
732
|
+
opaque line for SEVEN consecutive runs — every one grounding on lexical
|
|
733
|
+
search only, every dedup `inconclusive` — and no operator could tell a
|
|
734
|
+
missing package from an ABI mismatch from a missing model without shell
|
|
735
|
+
access to the VM. An error that cannot distinguish its own causes cannot
|
|
736
|
+
be fixed remotely; that is the defect this repairs.
|
|
737
|
+
|
|
738
|
+
Carries the interpreter version deliberately: the vendored VM wheels are
|
|
739
|
+
built for one CPython minor (`vendor_semantic_deps.py` pins cp311), so an
|
|
740
|
+
image whose python moved yields `undefined symbol` / `No module named` —
|
|
741
|
+
indistinguishable from "never installed" unless the version is printed.
|
|
742
|
+
"""
|
|
743
|
+
parts: list[str] = [f"python {sys.version.split()[0]}"]
|
|
744
|
+
for mod in ("onnxruntime", "tokenizers"):
|
|
745
|
+
try:
|
|
746
|
+
__import__(mod)
|
|
747
|
+
except Exception as exc: # noqa: BLE001 — the reason IS the payload
|
|
748
|
+
parts.append(f"{mod}: {type(exc).__name__}: {exc}")
|
|
749
|
+
else:
|
|
750
|
+
parts.append(f"{mod}: ok")
|
|
751
|
+
return "; ".join(parts)
|
|
752
|
+
|
|
753
|
+
|
|
724
754
|
def _implicit_hash_fallback() -> Embedder:
|
|
725
755
|
"""Return HashEmbedder for the IMPLICIT auto-path, but never silently:
|
|
726
756
|
fail closed when the operator demanded a real embedder, else warn loudly."""
|
|
727
757
|
if os.environ.get("BRAIN_REQUIRE_REAL_EMBEDDER"):
|
|
728
758
|
raise EmbedderUnavailable(
|
|
729
759
|
"BRAIN_REQUIRE_REAL_EMBEDDER is set but no real semantic embedder is "
|
|
730
|
-
"available
|
|
731
|
-
"
|
|
760
|
+
"available. Refusing to degrade to the non-semantic HashEmbedder. "
|
|
761
|
+
f"Probe: {embedder_unavailable_reason()}"
|
|
732
762
|
)
|
|
733
763
|
print(_HASH_FALLBACK_MSG, file=sys.stderr, flush=True)
|
|
734
764
|
return HashEmbedder()
|
|
@@ -55,6 +55,19 @@ def _utcnow() -> _dt.datetime:
|
|
|
55
55
|
return _dt.datetime.now(_dt.timezone.utc)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _json_safe(v: Any) -> Any:
|
|
59
|
+
"""The event log is JSON text, but callers hand us values straight out of
|
|
60
|
+
YAML frontmatter, where an unquoted `due: 2026-07-17` parses as a
|
|
61
|
+
`datetime.date` — not JSON-serializable. Normalize dates to ISO at this one
|
|
62
|
+
boundary: every source (ingestion candidates, `brain cos-spine record`,
|
|
63
|
+
calendar follow-ups) routes through record_event, and the reducer/radar
|
|
64
|
+
read `due` back as ISO text either way.
|
|
65
|
+
"""
|
|
66
|
+
if isinstance(v, (_dt.date, _dt.datetime)):
|
|
67
|
+
return v.isoformat()
|
|
68
|
+
return v
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
def _ts(dt: _dt.datetime | None = None) -> str:
|
|
59
72
|
return (dt or _utcnow()).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
60
73
|
|
|
@@ -227,7 +240,7 @@ def record_event(vault, *, event: str, direction: str | None = None,
|
|
|
227
240
|
("text", text), ("topic", _topic_of(text or "", topic) if text or topic else None),
|
|
228
241
|
("due", due), ("source_ref", source_ref), ("note", note)):
|
|
229
242
|
if v is not None:
|
|
230
|
-
evidence[k] = v
|
|
243
|
+
evidence[k] = _json_safe(v)
|
|
231
244
|
|
|
232
245
|
conn = _conn(vault)
|
|
233
246
|
try:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/brand/brand-guide.md
RENAMED
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/cos/priorities.md
RENAMED
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/keywords/glossary.md
RENAMED
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/overlay/template/people/roster.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/brain-synthesis-mac.plist
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{brainiac_cli-0.18.0 → brainiac_cli-0.18.2}/src/brain/_assets/scripts/install-brief-windows.ps1
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|