ebony-enriching 0.1.0__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.
- ebony_enriching-0.1.0/.github/workflows/release.yml +97 -0
- ebony_enriching-0.1.0/.gitignore +36 -0
- ebony_enriching-0.1.0/Dockerfile +26 -0
- ebony_enriching-0.1.0/LICENSE +21 -0
- ebony_enriching-0.1.0/PKG-INFO +353 -0
- ebony_enriching-0.1.0/README.md +336 -0
- ebony_enriching-0.1.0/docker-compose.yml +30 -0
- ebony_enriching-0.1.0/pyproject.toml +70 -0
- ebony_enriching-0.1.0/src/ebony_enriching/__init__.py +6 -0
- ebony_enriching-0.1.0/src/ebony_enriching/__main__.py +16 -0
- ebony_enriching-0.1.0/src/ebony_enriching/app.py +33 -0
- ebony_enriching-0.1.0/src/ebony_enriching/config.py +51 -0
- ebony_enriching-0.1.0/src/ebony_enriching/mutex.py +66 -0
- ebony_enriching-0.1.0/src/ebony_enriching/permissions.py +69 -0
- ebony_enriching-0.1.0/src/ebony_enriching/schema.py +261 -0
- ebony_enriching-0.1.0/src/ebony_enriching/server.py +202 -0
- ebony_enriching-0.1.0/src/ebony_enriching/storage/__init__.py +0 -0
- ebony_enriching-0.1.0/src/ebony_enriching/storage/gaps.py +163 -0
- ebony_enriching-0.1.0/src/ebony_enriching/storage/markdown.py +83 -0
- ebony_enriching-0.1.0/src/ebony_enriching/storage/paths.py +73 -0
- ebony_enriching-0.1.0/src/ebony_enriching/tools.py +1145 -0
- ebony_enriching-0.1.0/tests/__init__.py +0 -0
- ebony_enriching-0.1.0/tests/_mcp_helpers.py +91 -0
- ebony_enriching-0.1.0/tests/conftest.py +46 -0
- ebony_enriching-0.1.0/tests/test_cross_server.py +311 -0
- ebony_enriching-0.1.0/tests/test_experiments.py +260 -0
- ebony_enriching-0.1.0/tests/test_gaps.py +230 -0
- ebony_enriching-0.1.0/tests/test_proposals.py +432 -0
- ebony_enriching-0.1.0/tests/test_schema.py +189 -0
- ebony_enriching-0.1.0/tests/test_server.py +221 -0
- ebony_enriching-0.1.0/uv.lock +937 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
packages: write # GHCR push
|
|
10
|
+
id-token: write # PyPI trusted publishing
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
# Pre-flight checks. Both publish jobs depend on this, so a failed gate
|
|
14
|
+
# prevents *any* artifact from shipping (no half-shipped state).
|
|
15
|
+
gate:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0 # full history needed for the ancestor check below
|
|
21
|
+
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v3
|
|
24
|
+
|
|
25
|
+
- name: Verify tag matches pyproject version
|
|
26
|
+
run: |
|
|
27
|
+
TAG="${GITHUB_REF#refs/tags/v}"
|
|
28
|
+
PROJECT_VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
29
|
+
if [ "$TAG" != "$PROJECT_VERSION" ]; then
|
|
30
|
+
echo "tag $TAG != pyproject $PROJECT_VERSION"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Verify tag is reachable from origin/main
|
|
35
|
+
run: |
|
|
36
|
+
git fetch --no-tags origin main
|
|
37
|
+
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
|
38
|
+
echo "tagged commit $GITHUB_SHA is not on origin/main"
|
|
39
|
+
echo "release tags must come from main — back-merge through, then re-tag from main"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
docker:
|
|
44
|
+
needs: gate
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
|
|
49
|
+
- name: Set up QEMU (arm64 emulation)
|
|
50
|
+
uses: docker/setup-qemu-action@v3
|
|
51
|
+
|
|
52
|
+
- name: Set up Docker Buildx
|
|
53
|
+
uses: docker/setup-buildx-action@v3
|
|
54
|
+
|
|
55
|
+
- name: Log in to GHCR
|
|
56
|
+
uses: docker/login-action@v3
|
|
57
|
+
with:
|
|
58
|
+
registry: ghcr.io
|
|
59
|
+
username: ${{ github.actor }}
|
|
60
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
61
|
+
|
|
62
|
+
- name: Extract metadata (tags + labels)
|
|
63
|
+
id: meta
|
|
64
|
+
uses: docker/metadata-action@v5
|
|
65
|
+
with:
|
|
66
|
+
images: ghcr.io/${{ github.repository }}
|
|
67
|
+
tags: |
|
|
68
|
+
type=semver,pattern={{version}}
|
|
69
|
+
type=semver,pattern={{major}}.{{minor}}
|
|
70
|
+
type=raw,value=latest
|
|
71
|
+
|
|
72
|
+
- name: Build and push (linux/amd64 + linux/arm64)
|
|
73
|
+
uses: docker/build-push-action@v5
|
|
74
|
+
with:
|
|
75
|
+
context: .
|
|
76
|
+
platforms: linux/amd64,linux/arm64
|
|
77
|
+
push: true
|
|
78
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
79
|
+
labels: ${{ steps.meta.outputs.labels }}
|
|
80
|
+
cache-from: type=gha
|
|
81
|
+
cache-to: type=gha,mode=max
|
|
82
|
+
|
|
83
|
+
pypi:
|
|
84
|
+
needs: gate
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
environment: pypi # matches the trusted-publisher config on PyPI
|
|
87
|
+
steps:
|
|
88
|
+
- uses: actions/checkout@v4
|
|
89
|
+
|
|
90
|
+
- name: Install uv
|
|
91
|
+
uses: astral-sh/setup-uv@v3
|
|
92
|
+
|
|
93
|
+
- name: Build wheel + sdist
|
|
94
|
+
run: uv build
|
|
95
|
+
|
|
96
|
+
- name: Publish to PyPI
|
|
97
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.ty_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
|
|
14
|
+
# Build
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
|
|
18
|
+
# venv / uv
|
|
19
|
+
.venv/
|
|
20
|
+
.python-version
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
.idea/
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# Local test artifacts
|
|
31
|
+
/tmp-ebony/
|
|
32
|
+
/output/
|
|
33
|
+
|
|
34
|
+
# Secrets
|
|
35
|
+
.env
|
|
36
|
+
.env.*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Resolve deps first so they cache across source changes.
|
|
6
|
+
COPY pyproject.toml uv.lock ./
|
|
7
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
8
|
+
uv sync --no-dev --no-install-project
|
|
9
|
+
|
|
10
|
+
# README.md is part of the package metadata (pyproject.toml -> readme).
|
|
11
|
+
# uv sync --no-install-project skipped reading it; the second sync
|
|
12
|
+
# (which installs the project itself) does, so it must be present.
|
|
13
|
+
COPY README.md ./
|
|
14
|
+
COPY src/ src/
|
|
15
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
16
|
+
uv sync --no-dev
|
|
17
|
+
|
|
18
|
+
ENV PYTHONUNBUFFERED=1 \
|
|
19
|
+
EBONY_ENRICHING_DIR=/data \
|
|
20
|
+
PORT=35834 \
|
|
21
|
+
HOST=0.0.0.0
|
|
22
|
+
|
|
23
|
+
EXPOSE 35834
|
|
24
|
+
VOLUME ["/data"]
|
|
25
|
+
|
|
26
|
+
CMD ["uv", "run", "python", "-m", "ebony_enriching"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parkview Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ebony-enriching
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server: the lab notebook substrate (proposals + experiments + gap signals) for ParkviewLab's CoGrind project.
|
|
5
|
+
Author-email: Gary <garycoding@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: fastapi
|
|
10
|
+
Requires-Dist: mcp[cli]>=1.27.0
|
|
11
|
+
Requires-Dist: pydantic
|
|
12
|
+
Requires-Dist: python-frontmatter
|
|
13
|
+
Requires-Dist: pyyaml
|
|
14
|
+
Requires-Dist: starlette
|
|
15
|
+
Requires-Dist: uvicorn[standard]
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ebony-enriching
|
|
19
|
+
|
|
20
|
+
MCP server: **the lab notebook substrate** (proposals + experiments + gap signals) for ParkviewLab's [CoGrind](https://github.com/ParkviewLab/cobalt-grinding) project.
|
|
21
|
+
|
|
22
|
+
Sister to [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp): smalt-mcp is the **library** (canonical knowledge); ebony-enriching is the **lab notebook** (research-in-flight). Both substrates have zero outbound dependencies — cobalt-grinding's cognitive agents orchestrate any cross-substrate flow.
|
|
23
|
+
|
|
24
|
+
## Status
|
|
25
|
+
|
|
26
|
+
**v0.1 surface complete.** 13 tools across 2 permission tiers, covering the full proposal / experiment / gap lifecycle. End-to-end orchestration with smalt-mcp is exercised by the integration tests. Awaiting the v0.1.0 release tag.
|
|
27
|
+
|
|
28
|
+
- **READ_ONLY (6):** `status`, `read_proposal`, `list_proposals`, `read_experiment`, `list_experiments`, `list_gaps`
|
|
29
|
+
- **READ_WRITE (7):** `bootstrap`, `write_proposal`, `update_proposal_status`, `supersede_proposal`, `write_experiment`, `add_gap`, `remove_gap`
|
|
30
|
+
|
|
31
|
+
No `REMOVE_DESTRUCTIVE` tier in v0 — lab-notebook semantics are append-only with status transitions (don't delete proposals, transition to `rejected`; don't delete experiments, they're the historical record). Gaps are the one exception: `remove_gap` exists because a gap is a transient signal that gets resolved when the answering work lands.
|
|
32
|
+
|
|
33
|
+
## Lab notebook
|
|
34
|
+
|
|
35
|
+
A scientist keeps two artifacts: a **library** of established knowledge (textbooks, published papers, vetted references) and a **lab notebook** where research-in-flight lives (observations, hypotheses, tested predictions, unanswered questions). The two have different rules — the library is canonical and citable, the notebook is messy and dated. cobalt-grinding mirrors this split across two MCP servers:
|
|
36
|
+
|
|
37
|
+
| Concept | Library | Lab notebook |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| **Server** | `smalt-mcp` | `ebony-enriching` |
|
|
40
|
+
| **Substrate dir** | `~/Documents/Smalt/` | `~/Documents/EbonyEnriching/` |
|
|
41
|
+
| **Storage** | LanceDB + markdown (hybrid FTS + vector + alias search) | Filesystem + markdown only (filesystem walks) |
|
|
42
|
+
| **Lifecycle** | pages stabilize toward canonical state; old versions superseded by new | proposals flow through `proposed → under_test → validated → applied \| rejected`; experiments accrue; gaps queue and drain |
|
|
43
|
+
| **Tier of truth** | one-and-only canonical store | working memory that publishes to the library when validated |
|
|
44
|
+
|
|
45
|
+
**ebony-enriching records; it doesn't decide.** Lifecycle policy (when to mark a proposal `rejected`, when to auto-test vs. defer to user review, what counts as falsifiability) lives in cobalt-grinding's cognitive agents reading the substrate's `POLICY.md`. The MCP tools enforce **storage** correctness (path safety, atomicity, schema validation) and nothing else.
|
|
46
|
+
|
|
47
|
+
## Run
|
|
48
|
+
|
|
49
|
+
Same five-mode pattern as [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp) and [`deco-assaying`](https://github.com/ParkviewLab/deco-assaying). Pick whichever fits.
|
|
50
|
+
|
|
51
|
+
| Mode | When to use |
|
|
52
|
+
|---|---|
|
|
53
|
+
| 1. `uvx` (one-off) | Try it once, no install. |
|
|
54
|
+
| 2. `uv tool install` (pinned daemon) | Run it occasionally, want it on `$PATH`. |
|
|
55
|
+
| 3. macOS LaunchAgent | Persistent daemon on a Mac. |
|
|
56
|
+
| 4. Linux systemd user unit | Persistent daemon on Linux. |
|
|
57
|
+
| 5. Docker / docker compose | Container deployment. |
|
|
58
|
+
|
|
59
|
+
### Prereqs
|
|
60
|
+
|
|
61
|
+
- **uv-based modes (1–4)** need [`uv`](https://docs.astral.sh/uv/) and `git`. uv ships a portable Python 3.13, so no system Python install required.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Docker mode (5)** needs `docker` (or compatible). The image bundles Python 3.13; nothing else on the host.
|
|
68
|
+
|
|
69
|
+
In every mode the server listens on `PORT` (default `35834` — one above smalt-mcp's `35833`). Sanity-check it's up:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
curl http://127.0.0.1:35834/health
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### 1. One-off — `uvx`
|
|
78
|
+
|
|
79
|
+
`uvx` resolves the package into a temporary venv and runs it once. Nothing persists between runs.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
uvx ebony-enriching # latest release
|
|
83
|
+
uvx ebony-enriching@0.1.0 # pin a specific version
|
|
84
|
+
|
|
85
|
+
# With env vars (custom data dir, restricted scope):
|
|
86
|
+
EBONY_ENRICHING_DIR=$HOME/EbonyEnriching \
|
|
87
|
+
EBONY_SCOPE=read_only \
|
|
88
|
+
uvx ebony-enriching
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Good for kicking the tires or running on a CI box where you don't want to leave anything on disk.
|
|
92
|
+
|
|
93
|
+
### 2. Pinned daemon — `uv tool install`
|
|
94
|
+
|
|
95
|
+
Installs the `ebony-enriching` command on your `$PATH`, isolated in its own venv that uv manages. Faster startup than `uvx` (no resolve on each run).
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv tool install ebony-enriching
|
|
99
|
+
ebony-enriching # foreground server
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
To upgrade: `uv tool upgrade ebony-enriching`. To remove: `uv tool uninstall ebony-enriching`.
|
|
103
|
+
|
|
104
|
+
For a real "always running" setup, see the launchd / systemd recipes below.
|
|
105
|
+
|
|
106
|
+
### 3. macOS persistent daemon (launchd)
|
|
107
|
+
|
|
108
|
+
After `uv tool install ebony-enriching`, register a LaunchAgent so the daemon starts at login and restarts if it crashes.
|
|
109
|
+
|
|
110
|
+
Save this as `~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist` (replace `CHANGE-ME` with your username):
|
|
111
|
+
|
|
112
|
+
```xml
|
|
113
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
114
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
115
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
116
|
+
<plist version="1.0">
|
|
117
|
+
<dict>
|
|
118
|
+
<key>Label</key>
|
|
119
|
+
<string>com.garycoding.ebony-enriching</string>
|
|
120
|
+
|
|
121
|
+
<key>ProgramArguments</key>
|
|
122
|
+
<array>
|
|
123
|
+
<string>/Users/CHANGE-ME/.local/bin/ebony-enriching</string>
|
|
124
|
+
</array>
|
|
125
|
+
|
|
126
|
+
<key>EnvironmentVariables</key>
|
|
127
|
+
<dict>
|
|
128
|
+
<key>EBONY_ENRICHING_DIR</key>
|
|
129
|
+
<string>/Users/CHANGE-ME/Documents/EbonyEnriching</string>
|
|
130
|
+
<key>EBONY_SCOPE</key>
|
|
131
|
+
<string>read_write</string>
|
|
132
|
+
</dict>
|
|
133
|
+
|
|
134
|
+
<key>RunAtLoad</key><true/>
|
|
135
|
+
<key>KeepAlive</key><true/>
|
|
136
|
+
|
|
137
|
+
<key>StandardOutPath</key>
|
|
138
|
+
<string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.out.log</string>
|
|
139
|
+
<key>StandardErrorPath</key>
|
|
140
|
+
<string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.err.log</string>
|
|
141
|
+
</dict>
|
|
142
|
+
</plist>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Load and start it:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
|
|
149
|
+
launchctl kickstart -k gui/$(id -u)/com.garycoding.ebony-enriching
|
|
150
|
+
|
|
151
|
+
# Check status:
|
|
152
|
+
launchctl print gui/$(id -u)/com.garycoding.ebony-enriching | head -30
|
|
153
|
+
|
|
154
|
+
# Tail logs:
|
|
155
|
+
tail -f ~/Library/Logs/ebony-enriching.{out,err}.log
|
|
156
|
+
|
|
157
|
+
# Stop / unload:
|
|
158
|
+
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 4. Linux persistent daemon (systemd)
|
|
162
|
+
|
|
163
|
+
After `uv tool install ebony-enriching`, register a user-scope systemd unit so no root is required.
|
|
164
|
+
|
|
165
|
+
Save this as `~/.config/systemd/user/ebony-enriching.service`:
|
|
166
|
+
|
|
167
|
+
```ini
|
|
168
|
+
[Unit]
|
|
169
|
+
Description=ebony-enriching MCP server (lab notebook substrate)
|
|
170
|
+
After=network-online.target
|
|
171
|
+
|
|
172
|
+
[Service]
|
|
173
|
+
Type=simple
|
|
174
|
+
ExecStart=%h/.local/bin/ebony-enriching
|
|
175
|
+
Restart=on-failure
|
|
176
|
+
RestartSec=5
|
|
177
|
+
Environment=EBONY_ENRICHING_DIR=%h/Documents/EbonyEnriching
|
|
178
|
+
Environment=EBONY_SCOPE=read_write
|
|
179
|
+
|
|
180
|
+
[Install]
|
|
181
|
+
WantedBy=default.target
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Enable and start:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
systemctl --user daemon-reload
|
|
188
|
+
systemctl --user enable --now ebony-enriching
|
|
189
|
+
|
|
190
|
+
# Check status:
|
|
191
|
+
systemctl --user status ebony-enriching
|
|
192
|
+
|
|
193
|
+
# Tail logs:
|
|
194
|
+
journalctl --user -u ebony-enriching -f
|
|
195
|
+
|
|
196
|
+
# Stop:
|
|
197
|
+
systemctl --user disable --now ebony-enriching
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
To keep the daemon running when the user is logged out, enable lingering:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
loginctl enable-linger "$USER"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 5. Docker / GHCR
|
|
207
|
+
|
|
208
|
+
Pull the published multi-arch image (linux/amd64 + linux/arm64) and run it directly:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
docker pull ghcr.io/parkviewlab/ebony-enriching:latest
|
|
212
|
+
|
|
213
|
+
docker run --rm \
|
|
214
|
+
-p 35834:35834 \
|
|
215
|
+
-e EBONY_SCOPE=read_write \
|
|
216
|
+
-v ebony-data:/data \
|
|
217
|
+
ghcr.io/parkviewlab/ebony-enriching:latest
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Pin a specific version with a tag — `:0.1.0`, `:0.1`, or `:latest`. See the [container registry](https://github.com/ParkviewLab/ebony-enriching/pkgs/container/ebony-enriching) for available tags.
|
|
221
|
+
|
|
222
|
+
For a real deployment, copy [`docker-compose.yml`](docker-compose.yml), edit env vars if needed, then:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
docker compose up -d # start in background
|
|
226
|
+
docker compose logs -f # tail logs
|
|
227
|
+
docker compose pull && docker compose up -d # upgrade
|
|
228
|
+
docker compose down # stop, keep volume
|
|
229
|
+
docker compose down -v # stop and drop the volume
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### From source (for development)
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
git clone https://github.com/ParkviewLab/ebony-enriching.git
|
|
236
|
+
cd ebony-enriching
|
|
237
|
+
uv sync
|
|
238
|
+
EBONY_ENRICHING_DIR=~/Documents/EbonyEnriching uv run python -m ebony_enriching
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Endpoints
|
|
242
|
+
|
|
243
|
+
- `POST /sse` — MCP Streamable HTTP transport. Tools.
|
|
244
|
+
- `GET /health` — liveness probe (`{ok, version, uptime_seconds}`).
|
|
245
|
+
- `GET /admin/version` — server identity + scope + configured EbonyEnriching path.
|
|
246
|
+
- `GET /docs` — OpenAPI / Swagger UI for the HTTP routes.
|
|
247
|
+
|
|
248
|
+
HTTP responses are gzipped when the client sends `Accept-Encoding: gzip`.
|
|
249
|
+
|
|
250
|
+
## MCP tools
|
|
251
|
+
|
|
252
|
+
Two permission tiers controlled by `EBONY_SCOPE`. A caller at tier N sees and may call any tool whose required scope is ≤ N.
|
|
253
|
+
|
|
254
|
+
**`read_only` (6 tools):**
|
|
255
|
+
|
|
256
|
+
- `status` — EbonyEnriching path, existence, single-writer mutex state. Always safe to call.
|
|
257
|
+
- `read_proposal` — read a single proposal by id. Returns full frontmatter + body.
|
|
258
|
+
- `list_proposals` — list proposals, optionally filtered by `system` (subdir), `status` (lifecycle state), or `kind` (`proposal_kind`). Malformed proposals appear with `valid: false` rather than being silently dropped.
|
|
259
|
+
- `read_experiment` — read one experiment record by `(proposal_id, run_timestamp)`. Returns full input + result.
|
|
260
|
+
- `list_experiments` — list experiments. With `proposal_id`, only that proposal's runs; without, all experiments. Returns summary metadata.
|
|
261
|
+
- `list_gaps` — parse `gaps.md` and return all gap entries (id, query, created_at, optional why / source).
|
|
262
|
+
|
|
263
|
+
**`read_write` (+7 tools):**
|
|
264
|
+
|
|
265
|
+
- `bootstrap` — initialize the canonical directory layout at `EBONY_ENRICHING_DIR`; drop in `gaps.md` / `schema/SCHEMA.md` / `schema/POLICY.md` / `config.toml` placeholders. Idempotent — reports only what was newly created.
|
|
266
|
+
- `write_proposal` — write a proposal to `proposals/<subdir>/<id>.md`. Schema-related kinds (`schema_addition` / `schema_drift` / `schema_removal`) route to `proposals/schema/`; others to `proposals/<proposed_by>/`. Atomic write.
|
|
267
|
+
- `update_proposal_status` — update a proposal's lifecycle fields (`status`, optional `test_status`, `test_cost`) in-place. RMW under the single-writer mutex. Validates values against their StrEnum but does NOT enforce transition rules — that policy lives in cobalt-grinding's agents.
|
|
268
|
+
- `supersede_proposal` — link two proposals: sets `superseded_by: new_id` on `old_id` and `supersedes: old_id` on `new_id`. Both must already exist; does not transition statuses.
|
|
269
|
+
- `write_experiment` — record one run of a proposal's prediction test at `experiments/<proposal_id>/<run_timestamp>.md`. `run_timestamp` defaults to now (UTC). Doesn't check that the referenced proposal exists.
|
|
270
|
+
- `add_gap` — record an unanswered query in `gaps.md`. `gap_id` is derived from the query (SHA-256 hex, truncated to 8 chars; lowercase + collapsed whitespace), so adding the same query twice is idempotent (returns `already_present: true`).
|
|
271
|
+
- `remove_gap` — drop a gap bullet by id. Idempotent — unknown id returns `removed: 0`.
|
|
272
|
+
|
|
273
|
+
For the canonical-page storage surface (writing `EntityPage` / `ConceptPage` / `SourcePage` / `SynthesisPage`, hybrid search, link / claim management), use [`smalt-mcp`](https://github.com/ParkviewLab/smalt-mcp) — the library substrate. cobalt-grinding's cognitive agents orchestrate any cross-substrate flow.
|
|
274
|
+
|
|
275
|
+
## On-disk layout
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
$EBONY_ENRICHING_DIR/
|
|
279
|
+
├── proposals/
|
|
280
|
+
│ ├── schema/ # schema_addition / schema_drift / schema_removal kinds
|
|
281
|
+
│ ├── cogitate/ # written by the Cogitate cognitive system
|
|
282
|
+
│ ├── curate/ # written by Curate
|
|
283
|
+
│ ├── research/ # written by Research
|
|
284
|
+
│ ├── toolsmith/ # written by Toolsmith
|
|
285
|
+
│ └── converse/ # written by Converse (novelty detector)
|
|
286
|
+
├── experiments/
|
|
287
|
+
│ └── <proposal-id>/
|
|
288
|
+
│ └── <run-timestamp>.md
|
|
289
|
+
├── gaps.md # one bullet per open gap (managed by add_gap / remove_gap)
|
|
290
|
+
├── schema/
|
|
291
|
+
│ ├── SCHEMA.md # human-readable narrative of proposal / experiment / gap shape
|
|
292
|
+
│ └── POLICY.md # human-readable falsifiability + cost-tier policy
|
|
293
|
+
└── config.toml # reserved (empty in v0)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`bootstrap` materializes this layout. Proposal subdirs route by `proposal_kind` (schema-related kinds land in `proposals/schema/`; everything else lands in `proposals/<proposed_by>/`).
|
|
297
|
+
|
|
298
|
+
## Configuration
|
|
299
|
+
|
|
300
|
+
| Env var | Default | Purpose |
|
|
301
|
+
|---|---|---|
|
|
302
|
+
| `PORT` | `35834` | HTTP listen port. |
|
|
303
|
+
| `HOST` | `0.0.0.0` | HTTP bind address. |
|
|
304
|
+
| `EBONY_ENRICHING_DIR` | `~/Documents/EbonyEnriching` | Path to the lab notebook this server wraps. Call `bootstrap` once to materialize the canonical layout. `EBONY_DIR` is accepted as a shorter alias. |
|
|
305
|
+
| `EBONY_SCOPE` | `read_write` | `read_only`, `read_write`, or `remove_destructive`. Tiered: caller at tier N sees every tool whose required scope is ≤ N. (`remove_destructive` is reserved — no v0 tool requires it.) |
|
|
306
|
+
| `EBONY_INTERNAL_TOKEN` | *(unset)* | Reserved for future per-client scope routing; not yet enforced. |
|
|
307
|
+
|
|
308
|
+
## Why a separate MCP server (not part of smalt-mcp)
|
|
309
|
+
|
|
310
|
+
The two storage substrates have different shapes:
|
|
311
|
+
|
|
312
|
+
- **Smalt** is LanceDB-backed (hybrid FTS + vector + alias search over thousands of pages); ships an embedder; the `smalt-mcp` package carries hundreds of MB of deps.
|
|
313
|
+
- **Lab notebook** is filesystem-text-only (filesystem walks over hundreds of proposals/experiments/gaps); no embedder, no LanceDB; the `ebony-enriching` package is small.
|
|
314
|
+
|
|
315
|
+
Bundling them produced a server that paid the search-stack cost for a workload that didn't need it, and made the two surfaces' release cadences coupled when they shouldn't be. smalt-mcp's storage tools stabilize toward 1.0; ebony-enriching's schema will iterate as cobalt-grinding's cognitive systems land. Splitting them into two MCP children — both supervised by `cogrindd` — gives each substrate its own lifecycle.
|
|
316
|
+
|
|
317
|
+
See [`cobalt-grinding/docs/plan.md`](https://github.com/ParkviewLab/cobalt-grinding/blob/main/docs/plan.md) → *Decisions made* for the full rationale.
|
|
318
|
+
|
|
319
|
+
## Tests
|
|
320
|
+
|
|
321
|
+
Default (fast — ~0.3s, the full v0.1 tool surface in-process):
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
uv run pytest
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Integration tests** exercise both ebony-enriching AND a real smalt-mcp subprocess to verify the cobalt-grinding orchestration pattern (write proposal → validate → cross-substrate publish → mark applied). Default `pytest` skips them; run explicitly:
|
|
328
|
+
|
|
329
|
+
```sh
|
|
330
|
+
uv run pytest -m integration
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The integration fixture resolves smalt-mcp's project directory in this order:
|
|
334
|
+
1. `SMALT_MCP_PROJECT` env var (explicit override)
|
|
335
|
+
2. `../../smalt-mcp/worktrees/main` relative to this repo (the [ParkviewLab worktree convention](https://github.com/ParkviewLab/dev-tools))
|
|
336
|
+
|
|
337
|
+
Skipped with a clear message if neither resolves.
|
|
338
|
+
|
|
339
|
+
## Releasing
|
|
340
|
+
|
|
341
|
+
Tag-driven via the release workflow on push of a `v*` tag. Use the [`ParkviewLab/dev-tools`](https://github.com/ParkviewLab/dev-tools) helpers — they enforce the SSOT-tag-CI loop (`pyproject.toml` is the only place the version lives; CI verifies the pushed tag matches before publishing).
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
git bump patch # 0.1.0 → 0.1.1, committed
|
|
345
|
+
git release # annotated tag v0.1.1 from pyproject.toml
|
|
346
|
+
git push --follow-tags # CI fires
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Don't have the helpers? Install once: `git clone https://github.com/ParkviewLab/dev-tools.git ~/dev-tools && cd ~/dev-tools && ./install.sh`.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
MIT. See `LICENSE`.
|