Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,9 @@ provenance, freshness and next-action guidance.
The optional `board_base_url` in `assistants.json` enables the shared organ
footer for this Scientist instance. It is the hosting root, without an organ
repository suffix; membership and order come from Brain’s board registry.

## Start an independent instance

Use the [local setup guide](docs/setup.md) to preview and create empty user-owned
data, resume interrupted setup and validate a local board/feed. Publication and
Scientist connection follow as explicit steps.
13 changes: 13 additions & 0 deletions broca/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ def main(argv=None):
parser.add_argument("--root", type=Path, default=Path.cwd(), help="Broca data checkout")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("check")
setup = sub.add_parser("setup", help="preview or resume empty local instance setup")
setup.add_argument("--repos", type=Path, required=True)
setup.add_argument("--repo", required=True, help="canonical repository key")
setup.add_argument("--pages-url", required=True)
setup.add_argument("--brain", type=Path, required=True)
setup.add_argument("--apply", action="store_true")
add = sub.add_parser("ingest")
add.add_argument("record", type=Path)
collect = sub.add_parser("refresh")
Expand All @@ -28,6 +34,13 @@ def main(argv=None):
board.add_argument("--pages-url", help="override the configured public board directory")
args = parser.parse_args(argv)
try:
if args.command == "setup":
from .setup import prepare, apply
prepared = prepare(args.root, args.repos, args.repo, args.pages_url, args.brain)
missing = apply(prepared) if args.apply else prepared[2]
print(("Created: " if args.apply else "Would create: ") + (", ".join(missing) or "nothing"))
print("Local setup only; publication and Scientist connection remain separate.")
return 0
names = config(args.root)["assistants"]
if args.command == "check":
records = load_records(args.root, names)
Expand Down
152 changes: 152 additions & 0 deletions broca/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Offline, create-only initialization of a user-owned Broca data directory."""
import json
import os
from pathlib import Path
import re
import shlex
import tempfile

import yaml

from .state import pages_directory

MANIFEST = "setup.json"


def encoded(value):
return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode()


def plain_path(path):
"""Reject symlink destinations, including symlink parents, before resolving."""
path = Path(os.path.abspath(path))
if any(p.is_symlink() for p in (path, *path.parents)):
raise ValueError(f"setup path must not use symlinks: {path}")
return path


def prepare(root, repos, repo, pages_url, brain):
root = plain_path(root)
if not root.parent.is_dir():
raise ValueError("destination parent must already exist")
if root.exists() and not root.is_dir():
raise ValueError("destination must be a directory")
repos, brain = Path(repos).resolve(), Path(brain).resolve()
try:
catalogue = yaml.safe_load(repos.read_text())
except yaml.YAMLError as exc:
raise ValueError("invalid canonical repository map") from exc
entries = catalogue.get("repos") if isinstance(catalogue, dict) else None
row = entries.get(repo) if isinstance(entries, dict) else None
identity = row.get("github") if isinstance(row, dict) else None
if not isinstance(identity, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", identity):
raise ValueError("repo must identify a canonical owner/repository in the supplied map")
pages = pages_directory(pages_url)
if not all((brain / "board" / name).is_file() for name in ("_state.py", "_theme.py")):
raise ValueError("Brain checkout needs the shared board and state contract")
manifest = {"schema_version": 1, "repo": repo, "repository": identity,
"pages_url": pages, "repos": str(repos), "brain": str(brain),
"root": str(root), "framework": str(Path(__file__).resolve().parents[1])}
cfg = {"schema_version": 1, "assistants": [], "evidence_max_age_days": 30,
"collection_max_age_days": 30, "pages_url": pages}
checkin = ("Review this instance's saved communication inventory and assistant evidence.\n"
"Read the canonical Mind map and existing task before proposing changes.\n"
"Missing collection or evaluation is unknown, never a pass. Preserve records.\n"
"Do not collect, evaluate, schedule or publish without explicit authorization.\n")
q = shlex.quote
prefix = f"python -m broca --root {q(str(root))}"
resume = (f"{prefix} setup --repos {q(str(repos))} --repo {q(repo)} "
f"--pages-url {q(pages)} --brain {q(str(brain))}")
render = (f"{prefix} render --brain {q(str(brain))} --repository {q(identity)} "
f"--output {q(str(root / 'site/index.html'))} "
f"--state-output {q(str(root / 'site/state.json'))}")
handoff = f"""# Local Broca setup handoff

Keep this file and setup.json private: they contain local paths.
From the framework checkout, with Python and its dependencies available:

```sh
cd {q(manifest['framework'])}
{resume}
# After reviewing the preview, repeat with --apply to finish missing files.
{prefix} check
{render}
```

For a fresh conversation: read setup.json, this file, the canonical map and the
existing Mind task. Preview setup to identify missing or conflicting files. Resume
only this setup; never reinitialize or overwrite user edits. If configuration has
changed intentionally since setup, use check/render rather than restoring defaults.
The manifest records setup inputs, not task status or evidence of activation.

This prepares local data only. No collection or evaluation has occurred. An empty
feed is grey with an unknown collection timestamp. Projects, tasks and research
records remain in their existing homes. Add communication resources using Broca's
existing inventory procedure; do not copy a maintainer's records or receipts.

Publishing needs a separate approved task using the configured identity and public
board location. Publish only site/index.html and site/state.json after reviewing
visibility. Do not publish this directory wholesale. After the real HTTPS feed is
available, use Scientist's connect_organ.py preview/apply with organ broca, repo
{repo!r}, and pages URL {pages!r}. Until that validation succeeds, setup is not a
connected or active Scientist capability. Missing or stale evidence after connection
remains evidence trouble; it does not mean the organ is uninstalled.
"""
files = {MANIFEST: encoded(manifest), "assistants.json": encoded(cfg),
"CHECKIN.md": checkin.encode(), "SETUP.md": handoff.encode()}
# Only a manifest from this exact setup grants permission to fill missing files
# in an existing directory. Never adopt a pre-existing live data checkout.
if root.exists() and any(root.iterdir()) and not (root / MANIFEST).exists():
raise ValueError("nonempty destination has no setup manifest; refusing adoption")
missing = inspect(root, files)
return root, files, missing


def inspect(root, files):
plain_path(root)
missing = []
for name, content in files.items():
path = root / name
if path.is_symlink() or (path.exists() and (not path.is_file() or path.read_bytes() != content)):
raise ValueError(f"setup conflict: {name}; preserve it and review manually")
if not path.exists():
missing.append(name)
return missing


def create_file(path, content):
"""Publish a complete file without replacing a concurrently created target."""
fd, temporary = tempfile.mkstemp(prefix=".broca-setup-", dir=path.parent)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.link(temporary, path) # atomic create-only; an existing target wins
finally:
os.unlink(temporary)


def apply(prepared):
root, files, _ = prepared
root.mkdir(exist_ok=True)
if any(root.iterdir()) and not (root / MANIFEST).exists():
raise ValueError("destination changed before apply; no setup manifest")
missing = inspect(root, files) # preflight every conflict before writing
for name in missing: # manifest is published first; interruption is resumable
inspect(root, files)
create_file(root / name, files[name])
inspect(root, files)
from .collect import check_receipts, config, load_snapshot
from .records import load_records
from .state import build, shared_contract
cfg = config(root)
check_receipts(root)
load_snapshot(root)
load_records(root, cfg["assistants"])
manifest = json.loads(files[MANIFEST])
state = build(root, manifest["brain"], manifest["repository"])
errors = shared_contract(manifest["brain"]).validate_state(state)
if errors:
raise ValueError(f"owner feed validation failed: {errors}")
return missing
66 changes: 66 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Set up your own local Broca data

Use an existing Broca framework checkout with its Python dependencies and a
compatible Brain checkout. Keep user data in a separate directory. Register your
chosen repository identity in your canonical Mind `repos.yaml` first; its key and
GitHub name need not contain “Broca”. No GitHub access is needed for local setup.

From the Broca framework checkout:

```sh
python -m broca --root /path/to/my-guides setup \
--repos /path/to/Mind/repos.yaml --repo my-guides \
--pages-url https://research.example/guides/ --brain /path/to/PyAutoBrain
```

Review the file list, then repeat with `--apply`. The parent directory must exist.
A new destination or an empty directory is accepted; an existing nonempty directory
requires the matching setup manifest. Symlink destinations are refused.

Setup creates four files: `setup.json`, `assistants.json`, `CHECKIN.md` and
`SETUP.md`. The manifest stores the identity and local paths needed to resume.
Assistant selection starts empty. There are no copied measurements, collection
receipts, task records or invented resource inventories. Setup does not install
Python dependencies, clone repositories or configure a harness.

Each file is created atomically without replacing an existing file. After an
interruption, rerun the same command: matching files remain untouched and missing
files are filled in. A conflicting setup file stops the operation before writes;
inspect it manually. A crash before the first manifest was published may leave a
`.broca-setup-*` temporary file; inspect that new destination before removing only
that temporary file and retrying. Do not run setup concurrently with manual edits.

Once you customize configuration, validate it with `check` and `render`; setup
will refuse to restore defaults over your changes. It is an initializer, not an
upgrade or repair command. Keep the same framework version and paths when resuming
an unfinished setup. Record-preserving upgrades remain a separate pilot phase.

## Validate and resume

The generated `SETUP.md` has commands with your actual paths for:

- Previewing and resuming setup from a fresh conversation.
- Checking saved owner records with `python -m broca --root … check`.
- Rendering a local `site/index.html` and `site/state.json` with canonical identity.

Applying setup validates the local owner state and shared feed contract. Rendering
remains an explicit command. With no collection evidence, the feed is grey and its
required timestamp is the unknown-time sentinel, not the setup time. Resource
inventory remains unavailable until generated through the existing communication
inventory procedure. Setup success is not evidence of communication quality.

Keep `SETUP.md` and `setup.json` private: they contain local paths. They record
setup inputs and resumption instructions; the existing Mind task owns task status.
Project source and research records stay in their existing homes.

## Connect it to Scientist

Publish only the reviewed HTML and feed from `site/` through a separately approved
publication task. Local setup neither checks remote ownership nor creates a
repository, publishes content, schedules upkeep or evaluates assistants.

When the real HTTPS feed is available, use Scientist's existing
`connect_organ.py` preview/apply with organ key `broca`, your canonical repository
key and the configured Pages directory. Until that check succeeds, this is local
setup, not an active Scientist connection. Subsequent stale or unavailable evidence
must remain visible as evidence trouble for the connected organ.
Loading
Loading