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
55 changes: 43 additions & 12 deletions PROJECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ Project repositories own software, docs, notebooks, scripts, assistant definitio
and runners. Scientist owns these explicit project associations and links. The
agreed Broca expansion covers communication/documentation relationships and
upkeep evidence, with documentation, tutorials, user-facing workspaces and
assistants as first-class surfaces. Its current published implementation still
covers assistant evidence; the broader contract and evidence migration are
separate Mind tasks. These IDs/categories provide a coordination point, not a
new Broca evidence schema or copied ledger. Links to Broca route communication resources to its dashboard; they do not
imply a project-specific assessment or implemented evidence coverage.
assistants as first-class surfaces. Its dashboard now presents communication
resource inventories alongside assistant evidence; broader upkeep evidence
remains separate Mind work. These IDs/categories provide a coordination point,
not a new Broca evidence schema or copied ledger. Links to Broca route
communication resources to its dashboard; they do not imply a project-specific
assessment or implemented evidence coverage.

Ears retains listening and feedback collection; Brain interprets and routes;
Mind tracks implementation; Memory holds knowledge and citations; Cortex owns
Expand All @@ -102,13 +103,10 @@ Mind. Rename displays or change canonical locations without changing stable IDs.
Unsupported schema versions fail explicitly; an upgrade must preserve the
configuration and user records or supply a reviewable migration.

This phase does **not** yet make the complete Scientist independently named or
organs optional. Existing hero branding, organ membership, check-in prompt,
workflow checkout identities and framework discovery still reflect the current
instance. Starter packaging, configurable capability locations, optional-organ
activation and record-preserving upgrades are recorded follow-ups. No
collaborator project or access is assumed. Shared multi-contributor operation is
a later milestone.
Independent names and explicit optional organ connections are supported by the
local [starter preview](STARTER.md). Resumable activation, independently hosted
deployment and record-preserving upgrades remain separate phases. Shared
multi-contributor operation is a later milestone.

## Dashboard routing

Expand Down Expand Up @@ -142,3 +140,36 @@ no evidence timestamp: inventory refresh is not evidence collection.
Tests and communication resources link to their owning board's
`#resource-<resource ID>` disclosure while retaining source/published links.
Publish the owner-board changes before publishing the new Scientist links.

## Available to add

Each project may opt into `available` opportunities keyed by the stable capability
IDs `workspaces`, `tutorials`, `documentation`, `assistants`, `tests`, `profiling`,
`inference` and `visualization`. Add only what would be useful to that project:

```json
"available": {
"workspaces": {"name": "myproject_workspace", "public": true},
"inference": {"name": "myproject_inference", "public": true}
}
```

`name` is an instance-owned naming hint. No GitHub URL is inferred, no remote
existence is checked, and suffixes do not establish identity. The grey disclosure
contains a short purpose and selectable setup prompt. It stays usable by keyboard
and without JavaScript. Existing dashboard routes also group these opportunities;
an absent or private route falls back to the category heading.

As with connected resources, both the project and opportunity need explicit
`public: true` to appear. Opportunities are excluded from the connected resource
catalogue consumed by Heart and Broca. You cannot mark a capability available
while the same project's relationship contains a connected resource, including a
private one. To connect a validated resource, remove that opportunity and add the
normal resource/relationship using its canonical identity in one configuration
change. Projects can always omit assistants, and resources can be shared.

This is an explicit discovery contract, not a setup-state ledger. Keep incomplete
setup in your existing task records; do not claim it is connected until validated.
Missing or stale evidence on a connected organ never greys its resource into an
available opportunity. Paused/needs-attention states, resumable activation and
organ-specific validation remain separate work.
21 changes: 19 additions & 2 deletions STARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,22 @@ 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.
an organ. Grey available-to-add project resources are described below. Resumable organ
activation and independent deployment remain subsequent epic phases.

## Discover what a project can add

The separate `examples/connected-project/` fixture shows **Mira Scientist** with
one fictional software resource, no connected assistant or organs, and optional
resources such as `spectra_workspace` and `spectra_inference`:

```bash
python scripts/organism_board.py --instance examples/connected-project/instance.json \
--repos examples/connected-project/repos.yaml --html > connected-project.html
```

Expand Spectra, then a grey **Available to add** row to select its setup prompt.
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.
8 changes: 8 additions & 0 deletions examples/connected-project/instance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"schema_version": 1,
"name": "Mira Scientist",
"repo": "mira",
"pages_url": "https://example.invalid/research/mira/",
"projects": "projects.json",
"organs": {}
}
56 changes: 56 additions & 0 deletions examples/connected-project/projects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"schema_version": 1,
"resources": {
"spectra-code": {
"name": "SpectralAnalysis",
"repo": "spectra-code",
"public": true
}
},
"projects": {
"spectra": {
"name": "Spectra",
"kind": "scientific_project",
"public": true,
"resources": {
"software": [
"spectra-code"
]
},
"available": {
"workspaces": {
"name": "spectra_workspace",
"public": true
},
"tutorials": {
"name": "HowToSpectra",
"public": true
},
"documentation": {
"name": "Spectra guide",
"public": true
},
"assistants": {
"name": "spectra_assistant",
"public": true
},
"tests": {
"name": "spectra_workspace_test",
"public": true
},
"profiling": {
"name": "spectra_profiling",
"public": true
},
"inference": {
"name": "spectra_inference",
"public": true
},
"visualization": {
"name": "spectra_visualization",
"public": true
}
}
}
}
}
6 changes: 6 additions & 0 deletions examples/connected-project/repos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Fictitious identities for the offline example only.
repos:
mira:
github: ExampleResearch/MiraScientist
spectra-code:
github: ExampleResearch/SpectralAnalysis
6 changes: 6 additions & 0 deletions scripts/organism_board.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,12 @@ def _row_id(name: str) -> str:
.organ-foot a{min-height:44px;display:inline-flex;align-items:center}
.project-card ul{list-style:none;padding:0;margin:0}
.project-card li{margin:0 0 .35rem}
.available-resource{border:1px dashed var(--line);border-radius:6px}
.available-resource>summary{color:var(--muted);cursor:pointer;padding:.65rem;min-height:44px;box-sizing:border-box}
.availability-label{font-size:.8rem;margin-left:.4rem}
.available-resource-body{padding:0 .75rem .75rem 1.25rem}
.available-resource-body label{display:block;margin-bottom:.35rem}
.available-resource-body textarea{font:inherit;width:100%;box-sizing:border-box;resize:vertical;background:var(--bg);color:var(--fg);border:1px solid var(--line);padding:.65rem}
.organ-grid :focus-visible{outline:3px solid var(--accent);outline-offset:3px}
@media(max-width:64rem){.organ-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:40rem){.organ-grid{grid-template-columns:1fr}.organ-content .summary-row{grid-template-columns:1fr;gap:.15rem}}
Expand Down
65 changes: 60 additions & 5 deletions scripts/project_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@
"records": "Organ records",
}
KINDS = {"software_family": "Software family", "scientific_project": "Scientific investigation"}
CAPABILITIES = {
"workspaces": "Examples and working scripts for this project.",
"tutorials": "Step-by-step teaching material.",
"documentation": "Reference and usage guides.",
"assistants": "A project-specific research assistant.",
"tests": "Regression and smoke-test workspaces.",
"profiling": "Measure runtime and resource use.",
"inference": "Compare inference methods and results.",
"visualization": "Review figures and visualization outputs.",
}
ID = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*\Z")


Expand Down Expand Up @@ -119,13 +129,19 @@ def resolve(config: dict, body_map: dict) -> dict:

public_projects = []
for key, item in projects.items():
if set(item) - {"name", "public", "kind", "description", "resources"}:
if set(item) - {"name", "public", "kind", "description", "resources", "available"}:
raise ValueError(f"project {key}: unknown field")
if not isinstance(item.get("kind"), str) or item["kind"] not in KINDS:
raise ValueError(f"project {key}: unknown project kind")
relations = item.get("resources", {})
if not isinstance(relations, dict) or set(relations) - RELATIONS.keys():
raise ValueError(f"project {key}: unknown resource relationship")
available = _entries(item.get("available", {}), "available capability")
for capability, option in available.items():
if capability not in CAPABILITIES or set(option) - {"name", "public"}:
raise ValueError(f"project {key}: unknown available capability or field")
if relations.get(capability):
raise ValueError(f"project {key}: available capability is already connected")
groups = []
for relation, label in RELATIONS.items():
ids = relations.get(relation, [])
Expand All @@ -135,8 +151,12 @@ def resolve(config: dict, body_map: dict) -> dict:
if any(i not in resources for i in ids):
raise ValueError(f"project {key}: unknown resource reference")
visible = [resolved[i] for i in ids if resources[i].get("public") is True]
if visible:
option = available.get(relation, {})
if visible or option.get("public") is True:
group = {"relation": relation, "label": label, "resources": visible}
if option.get("public") is True:
group["available"] = [{"id": relation, "name": option["name"],
"purpose": CAPABILITIES[relation]}]
dashboard = routes.get(relation)
if dashboard and resources[dashboard].get("public") is True:
group["dashboard"] = {"id": dashboard, "name": resources[dashboard]["name"],
Expand Down Expand Up @@ -164,8 +184,10 @@ def display_groups(project: dict) -> list[dict]:
key = ("dashboard", dashboard["id"]) if dashboard else ("relation", group["relation"])
if key not in groups:
groups[key] = {"label": dashboard["name"] if dashboard else group["label"],
"url": dashboard["url"] if dashboard else None, "resources": []}
"url": dashboard["url"] if dashboard else None,
"resources": [], "available": []}
row = groups[key]
row["available"].extend(group.get("available", []))
seen = {r["id"] for r in row["resources"]}
for resource in group["resources"]:
if resource["id"] in seen:
Expand All @@ -184,10 +206,31 @@ def markdown_text(value: str) -> str:
return re.sub(r"([\\\[\]*_`#|])", r"\\\1", html.escape(value)).replace("\n", " ")


def setup_prompt(project: dict, option: dict) -> str:
"""A portable request, not an activation command or inferred repository ID."""
return (
f"Help me add {RELATIONS[option['id']].lower()} to project "
f"{json.dumps(project['name'])} (stable project ID: {project['id']}). "
f"Suggested name: {json.dumps(option['name'])}; this is only a naming hint.\n\n"
"Read this Scientist's instructions, instance profile, project relationships and "
"canonical Mind repository map. Inspect existing project-owned sources and any "
"partial setup first; reuse them and preserve user records. Ask for missing purpose, "
"location, ownership or visibility choices. Do not infer identity or access from names. "
"Propose the smallest useful setup through the existing development workflow, "
"including validation and how to resume it. Obtain approval before creating "
"repositories, changing access, publishing or running science.\n\n"
f"Keep capability {option['id']} available until setup is validated. Then replace "
"its available entry with an explicit connected resource using the canonical "
"repository identity, and validate the Scientist configuration offline. "
"Configure an owning organ separately if needed; a resource link is not organ "
"activation or evidence of quality."
)


def render_markdown(snapshot: dict) -> str:
text = markdown_text

lines = ["## Projects", "", "Connected resources; inventory is not a quality or readiness verdict."]
lines = ["## Projects"]
for project in snapshot.get("projects", []):
lines.extend(["", "### " + text(project["name"]), "", text(project["description"])])
for group in display_groups(project):
Expand All @@ -198,6 +241,9 @@ def render_markdown(snapshot: dict) -> str:
links = " · ".join(f"[{text(resource['name'] if i == 0 else link['label'])}]({quote(link['url'], safe='/:?=&%+#@~;,-_')})"
for i, link in enumerate(resource["links"]))
lines.append(f"- {label}: {links}")
for option in group["available"]:
lines.extend([f"- {label}: {text(option['name'])} — Available to add",
"", text(setup_prompt(project, option)), ""])
return "\n".join(lines)


Expand All @@ -212,6 +258,16 @@ def render_html(snapshot: dict) -> str:
links = " · ".join(f'<a href="{esc(link["url"], quote=True)}">{esc(resource["name"] if i == 0 else link["label"])}</a>'
for i, link in enumerate(resource["links"]))
entries.append(f'<li data-resource="{esc(resource["id"])}">{links}</li>')
for option in group["available"]:
ident = f"setup-{project['id']}-{option['id']}"
entries.append(
f'<li><details class="available-resource" data-capability="{option["id"]}">'
f'<summary><span>{esc(option["name"])}</span> '
'<span class="availability-label">Available to add</span></summary>'
f'<div class="available-resource-body"><p>{esc(option["purpose"])}</p>'
f'<label for="{ident}">Setup prompt</label>'
f'<textarea id="{ident}" readonly rows="8">'
f'{esc(setup_prompt(project, option))}</textarea></div></details></li>')
label = esc(group["label"])
if group["url"]:
label = f'<a href="{esc(group["url"], quote=True)}">{label}</a>'
Expand All @@ -222,7 +278,6 @@ def render_html(snapshot: dict) -> str:
<div class="organ-content"><p>{esc(project['description'])}</p>{content}</div></details>''')
empty = '<p>No public projects configured.</p>' if not cards else ''
return f'''<section id="projects" aria-labelledby="projects-heading"><h2 id="projects-heading">Projects</h2>
<p>Connected resources. Follow the source links for current records; listed resources do not establish quality or readiness.</p>
{empty}<div class="organ-grid" aria-label="Project resources">{''.join(cards)}</div></section>'''


Expand Down
Loading
Loading