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
67 changes: 65 additions & 2 deletions STARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,9 @@ To resume with an AI assistant:
> report connection validation separately from readiness. Do not create or publish
> anything without its own approved setup task.

Organ provisioning, resumable owner-specific setup, pause controls and deployment
are later activation phases. This helper covers the validated connection boundary.
This helper covers the validated connection boundary. The lifecycle controls below
track incomplete setup and pause Scientist observation. Provisioning and deployment
remain owner-specific steps.

## Discover what a project can add

Expand All @@ -186,3 +187,65 @@ Names are suggestions in instance configuration, not detected repositories.
Choose opportunities explicitly when connecting your project; see
[PROJECTS.md](PROJECTS.md#available-to-add). The empty starter stays empty.
These prompts begin a setup conversation; they do not run setup or activate organs.

## Track setup and pause observation

Independent profiles now distinguish configuration from evidence. The **Available
organs** catalogue offers setup prompts; selecting text there does not change files.
An optional `organ_setup` map records a chosen organ's canonical `repo` and
`pages_url` while its setup is incomplete. Its card makes no feed request and is
not a connected board link. Mind retains the actual setup task and history.

Preview beginning Broca setup (repeat with `--apply` after reviewing):

```sh
python scripts/organ_lifecycle.py setup --instance /path/to/instance.json \
--repos /path/to/Mind/repos.yaml --organ broca --repo my-guides \
--pages-url https://research.example/guides/
```

When the owner setup and publication are ready, use `connect_organ.py` as above.
A matching setup entry moves to `organs` only after its real feed validates.
Failures preserve the profile; mismatched identities require manual review.
Neither grey evidence nor a recorded failure prevents a valid connection: owner
readiness is separate from connection state.

To pause Scientist's observation of a connected organ:

```sh
python scripts/organ_lifecycle.py pause --instance /path/to/instance.json \
--repos /path/to/Mind/repos.yaml --organ broca
```

Repeat with `--apply` to save optional `paused: true` on the connection. Render and
publish your updated Scientist dashboard for viewers to see the change; an already
open or previously published page keeps its old configuration until reloaded from
the updated publication. New renders and refreshed pages skip server/browser feed
requests for paused organs, and cached evidence cannot revive them as current.
The repository identity and board link remain available.

**Pause stops only Scientist's observation.** It does not stop services, scheduled
Actions, jobs or compute. Those remain controlled by the organ's own procedures.
Use the same command with `resume` to preview resumption. Resume validates the saved
feed identity before removing the pause flag; an unavailable or invalid feed leaves
it paused. Apply and republish the Scientist page to resume its observation.

Connected cards show **Connected** separately from current evidence such as
**Needs attention** or **Unavailable**. Missing, old or grey evidence never turns a
connection into an available-to-add capability. Setup entries and paused connections
do not claim healthy evidence. Existing profiles without these optional fields
retain their connections and continue fetching normally.

For a fresh conversation:

> Read my instance profile, canonical map and existing Mind task. Identify organs
> still being set up, connected organs and paused observations. Preview the relevant
> lifecycle operation before applying it. Preserve records and identities; validate
> the published feed before connecting or resuming. Pausing observation must not
> stop owner jobs. Explain whether the dashboard needs republishing.

The fictitious `examples/lifecycle/` profile contains a pending Broca, paused Mind
and connected Heart with explicit independent names. Its `example.invalid` URLs
are placeholders; it makes no claim of live deployment. Use the normal renderer
with `--instance` and `--repos` for a local preview. Optional-organ provisioning,
real independent deployment and framework upgrades remain separate tasks.
14 changes: 14 additions & 0 deletions examples/lifecycle/instance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"schema_version": 1,
"name": "Mira Scientist",
"repo": "scientist",
"pages_url": "https://example.invalid/mira/",
"projects": "projects.json",
"organs": {
"mind": {"repo": "plans", "pages_url": "https://example.invalid/plans/", "paused": true},
"heart": {"repo": "checks", "pages_url": "https://example.invalid/checks/"}
},
"organ_setup": {
"broca": {"repo": "guides", "pages_url": "https://example.invalid/guides/"}
}
}
5 changes: 5 additions & 0 deletions examples/lifecycle/projects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"schema_version": 1,
"resources": {},
"projects": {}
}
9 changes: 9 additions & 0 deletions examples/lifecycle/repos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
repos:
scientist:
github: Research/Mira
plans:
github: Research/ResearchPlans
checks:
github: Research/Checks
guides:
github: Research/Communication
14 changes: 10 additions & 4 deletions scripts/connect_organ.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import project_resources


def plan(instance, repos, organ, repo, pages_url):
def plan(instance, repos, organ, repo, pages_url, *, resume=False):
"""Return validated replacement bytes and input snapshots, without writing.

A connection is a location/contract check, never installation or a health
Expand All @@ -37,8 +37,12 @@ def plan(instance, repos, organ, repo, pages_url):
github = instance_profile._identity(repo, yaml.safe_load(body_bytes)["repos"])
connection = {"repo": repo, "pages_url": instance_profile._pages(pages_url)}
existing = config["organs"].get(organ)
if existing is not None:
normalized = dict(existing, pages_url=instance_profile._pages(existing["pages_url"]))
pending = config.get("organ_setup", {}).get(organ)
if existing is not None and existing.get("paused", False) and not resume:
raise ValueError("organ observation is paused; use explicit lifecycle resume")
if existing is not None or pending is not None:
prior = existing if existing is not None else pending
normalized = {"repo": prior["repo"], "pages_url": instance_profile._pages(prior["pages_url"])}
if normalized != connection:
raise ValueError("organ already has a different connection; review it manually")

Expand All @@ -54,12 +58,14 @@ def plan(instance, repos, organ, repo, pages_url):
raise ValueError("organ feed pages_url does not match the selected location")

config["organs"][organ] = connection
if pending is not None:
del config["organ_setup"][organ]
return {
"instance": instance,
"inputs": {instance: original, repos: body_bytes, projects: project_bytes},
"mode": stat.S_IMODE(instance.stat().st_mode),
"replacement": (json.dumps(config, indent=2, ensure_ascii=False) + "\n").encode(),
"changed": existing is None,
"changed": existing is None or bool(existing.get("paused", False)),
"organ": organ,
"connection": connection,
"status": feed["status"],
Expand Down
26 changes: 21 additions & 5 deletions scripts/instance_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load(path, repos_path, theme):
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:
if not isinstance(config, dict) or not required <= set(config) or set(config) - required - {"organ_setup"}:
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")
Expand All @@ -64,18 +64,34 @@ def load(path, repos_path, theme):
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")
pending = config.get("organ_setup", {})
if not isinstance(pending, dict) or set(pending) - set(family) or set(pending) & set(selected):
raise ValueError("organ_setup must contain known, unconnected organ keys")
setup = []
for key, connection in pending.items():
if not isinstance(connection, dict) or set(connection) != {"repo", "pages_url"}:
raise ValueError(f"organ setup {key}: declare only repo and pages_url")
github_setup = _identity(connection["repo"], repos)
setup.append({"key": key, "name": theme.organ(key)["organ"],
"role": theme.organ(key)["function"], "repo": github_setup.split("/")[-1],
"url": _pages(connection["pages_url"])})
available = [{"key": key, "name": theme.organ(key)["organ"],
"role": theme.organ(key)["function"]} for key in family
if key not in selected and key not in pending]
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")
if (not isinstance(connection, dict) or not {"repo", "pages_url"} <= set(connection)
or set(connection) - {"repo", "pages_url", "paused"}
or type(connection.get("paused", False)) is not bool):
raise ValueError(f"organ {key}: declare repo, pages_url and optional boolean paused")
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"])})
"url": _pages(connection["pages_url"]), "paused": connection.get("paused", False)})
return {"schema_version": 1, "name": name, "repo": github.split("/")[1],
"source_url": "https://github.com/" + github, "pages_url": pages_url,
"boards": boards}, projects
"boards": boards, "setup": setup, "available": available}, projects
87 changes: 87 additions & 0 deletions scripts/organ_lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Preview/apply setup or pause Scientist observation; never stop owner jobs."""
import argparse
import json
from pathlib import Path
import stat

import yaml

import connect_organ
import instance_profile
import organism_board
import project_resources


def plan(instance, repos, organ, action, repo=None, pages_url=None):
instance, repos = Path(instance), Path(repos)
if instance.is_symlink():
raise ValueError("instance profile must not be a symlink")
original, body = instance.read_bytes(), repos.read_bytes()
config = json.loads(original)
theme = organism_board.theme()
_, projects = instance_profile.load(instance, repos, theme)
project_bytes = projects.read_bytes()
project_resources.load(projects, repos)
if organ not in theme.board_links(config["pages_url"], current="organism"):
raise ValueError("unknown organ key")
existing = config["organs"].get(organ)
if action == "resume":
if existing is None:
raise ValueError("resume requires a connected organ")
prepared = connect_organ.plan(instance, repos, organ, existing["repo"], existing["pages_url"], resume=True)
# Preserve the initial snapshot too; a concurrent identity edit must not
# silently change which connection this resume was meant to validate.
if prepared["inputs"] != {instance: original, repos: body, projects: project_bytes}:
raise ValueError("input changed during resume; rerun preview")
return prepared
if action == "setup":
if existing is not None:
raise ValueError("organ is already connected")
instance_profile._identity(repo, yaml.safe_load(body)["repos"])
connection = {"repo": repo, "pages_url": instance_profile._pages(pages_url)}
pending = config.setdefault("organ_setup", {})
if organ in pending and pending[organ] != connection:
raise ValueError("conflicting setup; review existing identity manually")
changed = organ not in pending
pending[organ] = connection
elif action == "pause":
if existing is None:
raise ValueError("pause requires a connected organ")
changed = not existing.get("paused", False)
existing["paused"] = True
else:
raise ValueError("action must be setup, pause or resume")
return {"instance": instance, "inputs": {instance: original, repos: body, projects: project_bytes},
"mode": stat.S_IMODE(instance.stat().st_mode), "changed": changed,
"replacement": (json.dumps(config, indent=2, ensure_ascii=False) + "\n").encode()}


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=["setup", "pause", "resume"])
parser.add_argument("--instance", type=Path, required=True)
parser.add_argument("--repos", type=Path, required=True)
parser.add_argument("--organ", required=True)
parser.add_argument("--repo")
parser.add_argument("--pages-url")
parser.add_argument("--apply", action="store_true")
args = parser.parse_args(argv)
if args.action != "setup" and (args.repo is not None or args.pages_url is not None):
parser.error("pause/resume use the existing connection identity")
try:
prepared = plan(args.instance, args.repos, args.organ, args.action, args.repo, args.pages_url)
connect_organ._unchanged(prepared)
if args.apply:
print("Updated" if connect_organ.apply(prepared) else "Unchanged")
else:
print("Preview only; pass --apply to save" if prepared["changed"] else "Unchanged")
print(prepared["replacement"].decode())
print("Pause controls Scientist observation only; owner jobs and services are unaffected.")
except (OSError, ValueError, RuntimeError, yaml.YAMLError) as exc:
parser.exit(2, f"organ-lifecycle: {exc}\n")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading