diff --git a/.github/workflows/board_tests.yml b/.github/workflows/board_tests.yml
index 6cd20f0..dab39ba 100644
--- a/.github/workflows/board_tests.yml
+++ b/.github/workflows/board_tests.yml
@@ -14,6 +14,22 @@ jobs:
with:
repository: PyAutoLabs/PyAutoBrain
path: PyAutoBrain
+ # Coordinated presentation changes test the companion Brain branch.
+ # Main remains the fallback when this is a Scientist-only change.
+ - name: Prefer matching Brain branch on pull requests
+ if: github.event_name == 'pull_request'
+ working-directory: PyAutoBrain
+ env:
+ SCIENTIST_TASK_BRANCH: ${{ github.head_ref }}
+ run: |
+ if git ls-remote --exit-code --heads origin "refs/heads/$SCIENTIST_TASK_BRANCH" > /dev/null; then
+ git fetch --depth 1 origin "refs/heads/$SCIENTIST_TASK_BRANCH"
+ git checkout -q FETCH_HEAD
+ else
+ status=$?
+ if [ "$status" -ne 2 ]; then exit "$status"; fi
+ echo "No companion Brain branch; testing against main"
+ fi
# Canonical repository identity input; relationships remain in Scientist.
- uses: actions/checkout@v4
with:
@@ -25,5 +41,6 @@ jobs:
python-version: '3.12'
- run: python -m pip install pytest pyyaml
- run: python scripts/organism_board.py --repos PyAutoMind/repos.yaml --check-projects
+ - run: python scripts/organism_board.py --instance examples/starter/instance.json --repos examples/starter/repos.yaml --check-instance
- run: python -m pytest tests -q
- run: node --test tests/overview.cjs
diff --git a/AGENTS.md b/AGENTS.md
index 81a23b3..5c81725 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,6 +37,14 @@ feeds without replacing open disclosures or prompt controls.
https://pyautoscientist.readthedocs.io are built from **`PyAutoBrain/docs/`**;
edit the docs there.
+## Independent instance preview
+
+`STARTER.md` documents the opt-in `--instance` profile. Keep instance names,
+repository identities, project paths and connected-organ locations in configuration.
+An empty profile contacts no feeds and imports no live project records. Repo
+identities continue to come from Mind. Bootstrap, activation and deployment are
+separate phases; do not describe this preview as a full installation.
+
## Future intent (not now — Phase 3, demand-gated)
Migrating the RTD docs source *into* this repo — so PyAutoScientist becomes the
diff --git a/README.md b/README.md
index bcadd8e..ef6c15d 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,12 @@ The Cortex keeps scientific experiments on a separate track from software develo
The software developed by the organism lives across the [PyAutoLabs](https://github.com/PyAutoLabs) organisation. See the [PyAutoLabs front door](https://pyautolabs.github.io) for the full repository map.
+## Independent starter preview
+
+[STARTER.md](STARTER.md) provides an empty, independently named local dashboard
+with explicit optional organ connections. It is the first starter slice; full
+bootstrap, organ activation and independent deployment remain in development.
+
## Project History
In March 2026, following more than a decade of exclusively human-led software development, PyAutoLabs transitioned to a fully natural-language, agentic-AI development ecosystem called `PyAutoScientist`. The software it now develops grew out of that decade of human-led work: [PyAutoLens](https://github.com/PyAutoLabs/PyAutoLens) (strong gravitational lensing), [PyAutoGalaxy](https://github.com/PyAutoLabs/PyAutoGalaxy) (galaxy structure and morphology), [PyAutoFit](https://github.com/PyAutoLabs/PyAutoFit) (Bayesian model fitting), [PyAutoArray](https://github.com/PyAutoLabs/PyAutoArray) (scientific data structures) and [PyAutoCTI](https://github.com/PyAutoLabs/PyAutoCTI) (CCD charge-transfer calibration), together with their workspaces and tutorials.
diff --git a/STARTER.md b/STARTER.md
new file mode 100644
index 0000000..8fc837b
--- /dev/null
+++ b/STARTER.md
@@ -0,0 +1,95 @@
+# Start a Scientist dashboard with your own identity
+
+This is the first slice of the independent starter: an empty dashboard with a
+name, its own repository identity and explicit organ connections. It needs
+Python, PyYAML and a pinned PyAutoBrain checkout for shared presentation. It
+requires no science libraries, assistant repository or organ service.
+
+This local preview does not initialize a working Mind, install an agent harness,
+provision repositories, activate organs or publish a site. Those remain later
+starter steps. Existing projects keep their own source and records.
+
+## Try the empty preview
+
+From this repository, with Brain beside it (or `PYAUTO_BRAIN` set to its checkout):
+
+```bash
+python -m pip install pyyaml
+python scripts/organism_board.py --instance examples/starter/instance.json \
+ --repos examples/starter/repos.yaml --check-instance
+python scripts/organism_board.py --instance examples/starter/instance.json \
+ --repos examples/starter/repos.yaml --html > starter.html
+```
+
+Open `starter.html` locally. The fictitious **Mira Scientist** has no projects or
+connected organs. Rendering and opening this empty page fetch no organ feeds.
+The example GitHub identity and `example.invalid` address are placeholders,
+not repositories or a deployed service. This example body map is a fixture,
+not a complete Mind installation.
+
+## Configure your instance
+
+Copy the example configuration to a directory you own, outside the framework
+checkout. Set `name`, `repo` and `pages_url`. `repo` is a stable ID in your
+canonical Mind `repos.yaml`; its `github` field supplies the repository home.
+`pages_url` is the explicit HTTPS directory where you intend to publish this
+Scientist, including any custom path. Naming is independent of framework organ
+keys and repository names.
+
+The required `projects` field names a file relative to the profile directory.
+Start with the empty project file. Supplying `--instance` never falls back to
+this repository's live project configuration. Use `--projects` only when you
+intentionally want to override the profile's file. Keep this configuration and
+your records when updating the framework; this change neither migrates nor
+rewrites them. A supported upgrade procedure remains future work.
+
+Omitting `--instance` retains the existing PyAutoLabs-compatible dashboard path.
+The current Actions publishing workflow still uses that path; this local preview
+does not configure independent-instance deployment.
+
+## Connect an existing project
+
+Give your coding assistant this request in the local checkout:
+
+> Read STARTER.md, PROJECTS.md and my instance profile. Help connect one existing
+> project. First inspect its current source and any existing Scientist records.
+> Ask me for its purpose, canonical repository identity and intended visibility
+> if these are not available. Reuse my Mind body map and explicit project/resource
+> relationships; do not copy source material or assume an assistant exists.
+> Explain the proposed configuration changes before applying them. Validate the
+> profile and relationships offline, then show me a local dashboard preview.
+> Do not create repositories, publish, change access or run science as part of
+> connecting the project. Suggest optional organs only when they serve a concrete
+> need; setup and activation remain separate steps.
+
+Add canonical repository identities in your Mind body map, then configure
+resources and projects as described in [PROJECTS.md](PROJECTS.md). Projects and
+resources are private by default; only explicit `public: true` entries appear in
+the publication snapshot. Do not enter private URLs into public connections.
+A project may have no assistant and may share dependencies with other projects.
+
+## Connect an already available organ
+
+`organs` maps Brain's stable organ keys to explicit canonical identities and
+published board directories. For example, after your own Mind board exists:
+
+```json
+"organs": {
+ "mind": {
+ "repo": "research-plans",
+ "pages_url": "https://research.example/boards/plans/"
+ }
+}
+```
+
+`research-plans` must be an entry in your body map with its actual GitHub home.
+The board's `state.json` must use `organ: "mind"` and the repository name from
+that GitHub identity. Connection order follows Brain's shared registry; only
+selected organs are collected, displayed and linked in the footer. A connected
+organ with a missing or invalid feed remains visible as unavailable. An omitted
+organ is unconfigured, not failed. Empty `organs: {}` is supported.
+
+Connections are an explicit choice of public feed locations. Adding a connection
+reads that feed during rendering and browser refresh; it never installs or runs
+an organ. Grey available-to-add capabilities, resumable AI-led activation and
+independent deployment are subsequent epic phases.
diff --git a/examples/starter/instance.json b/examples/starter/instance.json
new file mode 100644
index 0000000..f2bb494
--- /dev/null
+++ b/examples/starter/instance.json
@@ -0,0 +1,8 @@
+{
+ "schema_version": 1,
+ "name": "Mira Scientist",
+ "repo": "mira",
+ "pages_url": "https://example.invalid/research/mira/",
+ "projects": "projects.json",
+ "organs": {}
+}
diff --git a/examples/starter/projects.json b/examples/starter/projects.json
new file mode 100644
index 0000000..ea3849d
--- /dev/null
+++ b/examples/starter/projects.json
@@ -0,0 +1,5 @@
+{
+ "schema_version": 1,
+ "resources": {},
+ "projects": {}
+}
diff --git a/examples/starter/repos.yaml b/examples/starter/repos.yaml
new file mode 100644
index 0000000..ef6c624
--- /dev/null
+++ b/examples/starter/repos.yaml
@@ -0,0 +1,5 @@
+# Fictitious canonical identity input for the offline preview only.
+# Use your own Mind body map when connecting real projects.
+repos:
+ mira:
+ github: ExampleResearch/MiraScientist
diff --git a/scripts/instance_profile.py b/scripts/instance_profile.py
new file mode 100644
index 0000000..a0a291d
--- /dev/null
+++ b/scripts/instance_profile.py
@@ -0,0 +1,81 @@
+"""Opt-in Scientist instance configuration, resolved against the Mind body map.
+
+Loading is read-only and offline. Only explicitly selected organs enter the
+public snapshot; local configuration paths never enter it.
+"""
+
+import json
+import re
+from pathlib import Path
+from urllib.parse import urlsplit
+
+import yaml
+
+
+def _text(value, label):
+ if not isinstance(value, str) or not value.strip() or any(ord(c) < 32 for c in value):
+ raise ValueError(f"{label} must be nonempty single-line text")
+ return value
+
+
+def _pages(value):
+ value = _text(value, "pages_url")
+ url = urlsplit(value)
+ if (url.scheme != "https" or not url.hostname or url.username or url.password
+ or url.query or url.fragment or "\\" in value or any(c.isspace() for c in value)):
+ raise ValueError("pages_url must be an HTTPS directory URL without credentials, query or fragment")
+ return value.rstrip("/") + "/"
+
+
+def _identity(key, repos):
+ _text(key, "repo identity")
+ identity = repos.get(key)
+ github = identity.get("github") if isinstance(identity, dict) else None
+ if not isinstance(github, str) or not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", github):
+ raise ValueError(f"repo {key}: missing or invalid canonical GitHub identity")
+ return github
+
+
+def load(path, repos_path, theme):
+ """Return (public resolved profile, local project configuration path)."""
+ path = Path(path)
+ config = json.loads(path.read_text(encoding="utf-8"))
+ required = {"schema_version", "name", "repo", "pages_url", "projects", "organs"}
+ if not isinstance(config, dict) or set(config) != required:
+ raise ValueError("instance profile must declare schema_version, name, repo, pages_url, projects and organs")
+ if type(config["schema_version"]) is not int or config["schema_version"] != 1:
+ raise ValueError("unsupported instance schema_version")
+ body = yaml.safe_load(Path(repos_path).read_text(encoding="utf-8"))
+ repos = body.get("repos") if isinstance(body, dict) else None
+ if not isinstance(repos, dict):
+ raise ValueError("canonical identity map must contain repos")
+ name = _text(config["name"], "instance name")
+ github = _identity(config["repo"], repos)
+ pages_url = _pages(config["pages_url"])
+ relative = Path(_text(config["projects"], "projects"))
+ if relative.is_absolute() or ".." in relative.parts:
+ raise ValueError("projects must be a relative path within the instance configuration directory")
+ projects = path.parent / relative
+ if not projects.resolve().is_relative_to(path.parent.resolve()):
+ raise ValueError("projects must stay within the instance configuration directory")
+ selected = config["organs"]
+ family = theme.board_links(pages_url, current="organism")
+ if not family:
+ raise ValueError("Brain board registry unavailable")
+ if not isinstance(selected, dict) or set(selected) - set(family):
+ raise ValueError("organs must map canonical Brain organ keys to explicit connections")
+ boards = []
+ for key in family:
+ if key not in selected:
+ continue
+ connection = selected[key]
+ if not isinstance(connection, dict) or set(connection) != {"repo", "pages_url"}:
+ raise ValueError(f"organ {key}: declare only repo and pages_url")
+ organ_github = _identity(connection["repo"], repos)
+ boards.append({"key": key, "name": theme.organ(key)["organ"],
+ "role": theme.organ(key)["function"],
+ "repo": organ_github.split("/")[1],
+ "url": _pages(connection["pages_url"])})
+ return {"schema_version": 1, "name": name, "repo": github.split("/")[1],
+ "source_url": "https://github.com/" + github, "pages_url": pages_url,
+ "boards": boards}, projects
diff --git a/scripts/organism_board.py b/scripts/organism_board.py
index 197078f..d3faf4a 100644
--- a/scripts/organism_board.py
+++ b/scripts/organism_board.py
@@ -38,16 +38,13 @@
# the whole family at once — organism_board.yml checks PyAutoBrain out beside
# this repo, and a local run finds the sibling checkout the same way the other
# PyAuto tools resolve each other.
-CHECKIN_PROMPT = (
- "Use this chat as an ongoing entry point to PyAutoLabs. Read PyAutoScientist/AGENTS.md "
- "and PyAutoScientist/REPORTING.md. Use the board skill to inspect relevant operational evidence. Load other organs and "
- "project context as the request requires.\n\n"
+COMMON_CHECKIN_PROMPT = (
"When I give no particular direction, provide a concise overview of the organism’s "
- "work across all organs over the last 24 hours, plus its current position: significant progress, blockers, decisions needing my attention and "
+ "work across connected organs over the last 24 hours, plus its current position: significant progress, blockers, decisions needing my attention and "
"useful next steps. Check evidence freshness and coverage, linking to the owning boards "
"rather than reproducing every queue.\n\n"
"For a summary over a given time period, use that period instead of the last 24 hours. "
- "State the start, end and timezone. Check dated records in every organ, distinguish "
+ "State the start, end and timezone. Check dated records in every connected organ, distinguish "
"completed outcomes from work still in progress and decisions awaiting me, and link "
"the evidence. Deduplicate work spanning organs. A dashboard refresh is not a completed "
"event; current snapshots alone cannot establish a historical summary. Report coverage "
@@ -73,6 +70,26 @@
"background follow-up."
)
+CHECKIN_PROMPT = (
+ "Use this chat as an ongoing entry point to PyAutoLabs. Read PyAutoScientist/AGENTS.md "
+ "and PyAutoScientist/REPORTING.md. Use the board skill to inspect relevant operational evidence. Load other organs and "
+ "project context as the request requires.\n\n"
+ + COMMON_CHECKIN_PROMPT
+)
+
+
+def instance_prompt(instance):
+ return (f"Use this chat as an ongoing entry point to {instance['name']}. "
+ "Read this Scientist checkout’s AGENTS.md, STARTER.md and REPORTING.md, "
+ "then its explicit instance profile and project relationships. "
+ "Consult only connected organs and relevant project-owned sources. "
+ "Do not assume optional organs, an agent harness or remote access are installed. "
+ "Help connect existing projects using their canonical repository identities; "
+ "ask about purpose, visibility and needed capabilities before changing setup. "
+ "Keep missing evidence distinct from unconfigured organs.\n\n"
+ + COMMON_CHECKIN_PROMPT)
+
+
BOARD_KEY = "organism" # this board's entry in the Brain's palette table
@@ -138,12 +155,13 @@ def _get(url: str) -> str:
return resp.read().decode("utf-8", errors="replace")
-def collect(owner: str | None = None) -> dict:
- owner = owner if owner is not None else _owner()
- if not owner:
+def collect(owner: str | None = None, instance: dict | None = None) -> dict:
+ owner = owner if owner is not None else (_owner() if instance is None else None)
+ if instance is None and not owner:
raise ValueError("Cannot resolve the repository owner")
- rows = boards(owner)
+ rows = boards(owner) if instance is None else instance["boards"]
# theme() resolves the matching Brain checkout and its validator.
+ theme()
from _state import validate_state
def read(row):
@@ -160,8 +178,11 @@ def read(row):
with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
collected = list(executor.map(read, rows))
- return {"generated": datetime.datetime.now(datetime.timezone.utc).isoformat(),
- "owner": owner, "boards": collected}
+ snapshot = {"generated": datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ "owner": owner, "boards": collected}
+ if instance is not None:
+ snapshot["instance"] = {k: v for k, v in instance.items() if k != "boards"}
+ return snapshot
# --- pure helpers ---------------------------------------------------------------
@@ -183,6 +204,10 @@ def heart_word(snapshot: dict) -> str:
def route_hint(snapshot: dict) -> str:
"""Where to work next, keyed off the Heart's verdict."""
+ if "instance" in snapshot:
+ if not snapshot.get("boards"):
+ return "No organs connected. Connect a project, then add only the organs it needs."
+ return "Inspect connected organ evidence before choosing the next action; missing evidence is not a passing result."
word = heart_word(snapshot)
if word in ("RED", "YELLOW"):
return ("The Heart is " + word +
@@ -196,7 +221,9 @@ def route_hint(snapshot: dict) -> str:
def _render_md(snapshot: dict) -> str:
- lines = ["# PyAutoScientist Dashboard", "",
+ from project_resources import markdown_text
+ name = snapshot.get("instance", {}).get("name", "PyAutoScientist")
+ lines = [f"# {markdown_text(name)} Dashboard", "",
f"_{route_hint(snapshot)}_", "",
"| Dashboard | Says | |", "|---|---|---|"]
for b in snapshot.get("boards") or []:
@@ -254,37 +281,46 @@ def _row_id(name: str) -> str:
def _render_html(snapshot: dict) -> str:
t = theme()
owner = snapshot.get("owner")
+ instance = snapshot.get("instance")
+ name = instance["name"] if instance is not None else "PyAutoScientist"
+ source = instance["source_url"] if instance is not None else (f"https://github.com/{owner}/PyAutoScientist" if owner else None)
+ prompt = CHECKIN_PROMPT if instance is None else instance_prompt(instance)
panel = t.orchestration_panel(
- "scientist", "", "", CHECKIN_PROMPT, organ="scientist",
- work_links=([{"label": "Scientist repository", "href": f"https://github.com/{owner}/PyAutoScientist"}] if owner else []),
+ "scientist", "", "", prompt, organ="scientist",
+ work_links=([{"label": (name if instance is not None else "Scientist") + " repository", "href": source}] if source else []),
refreshed_at=(snapshot.get("generated") if snapshot.get("boards") and
all(row.get("headline") is not None for row in snapshot["boards"]) else None),
- refresh_url=(f"https://github.com/{owner}/PyAutoScientist/actions/workflows/organism_board.yml" if owner else None))
+ refresh_url=(source + "/actions/workflows/organism_board.yml" if source and instance is None else None))
from project_resources import render_html as render_projects
projects_html = render_projects(snapshot["project_resources"]) if "project_resources" in snapshot else ""
- footer = t.boards_footer(t.board_links(f"https://{owner.lower()}.github.io", BOARD_KEY), BOARD_KEY) if owner else ""
+ links = ({row["key"]: row["url"] for row in snapshot["boards"]} if instance is not None
+ else t.board_links(f"https://{owner.lower()}.github.io", BOARD_KEY) if owner else {})
+ footer = t.boards_footer(links, BOARD_KEY)
rows = []
for row in snapshot.get("boards") or []:
key = row.get("key", row["name"].lower())
- name = _html.escape(row["name"])
+ organ_name = _html.escape(row["name"])
ident = _row_id(row["name"])
# Server fallback is explicitly a publication snapshot, not a live check.
head = _html.escape(row.get("headline") or "Evidence unavailable")
url = _html.escape(row["url"], quote=True)
rows.append(f"""{name}Snapshot
+{organ_name}Snapshot
-
No organs connected. Add a connection when your project needs it.
' if instance is not None and not rows else ''}