Add assistant-neutral GroundRecall protocol

This commit is contained in:
welsberr 2026-05-01 13:57:11 +00:00
parent 88a547463d
commit 066dd1d42f
8 changed files with 812 additions and 21 deletions

193
README.md
View File

@ -1,22 +1,158 @@
# GroundRecall # GroundRecall
GroundRecall is a human-reviewable/AI usable knowledge layer with capabilities to meet or exceed 'llmwiki' with 'v2' specifications, plus an import path for existing llmwiki instances, and integration with Didactopus for review workflows and knowledge merging. GroundRecall is a local-first, provenance-aware knowledge substrate for
human-reviewable and assistant-usable memory. It imports source material into a
canonical store, supports review and promotion, exports assistant-neutral
snapshots, and can generate assistant-specific bundles for tools such as Codex
and Claude Code.
`GroundRecall` can also import normalized `doclift` bundles directly when the GroundRecall is intended for work where durable context matters:
source material began as legacy office documents and you want a provenance-aware
knowledge import without going through a learner pack first. See
`docs/quickstart.md` for the minimal `doclift -> GroundRecall` flow.
`GroundRecall` can now also export a pack-ready - site, app, and service administration across sessions
`groundrecall_query_bundle.json` for a reviewed concept so `Didactopus` can - local/remote deployment memory with host-role distinctions
carry that concept context into a learner-facing pack: - research notes and grounded claim tracking
- legacy document normalization through `doclift`
- learner-facing workflows through `Didactopus`
- assistant handoff between Codex, Claude Code, and other file-aware tools
## Current Features
- Import from llmwiki-style trees, plain notes, normalized `doclift` bundles,
Didactopus packs, transcripts, PolyPaper projects, and specialized corpora.
- Normalize imports into artifacts, fragments, observations, claims, concepts,
and relations.
- Lint and review import output before promotion.
- Promote reviewed records into a canonical GroundRecall store.
- Query by concept and export query bundles.
- Export assistant-neutral canonical snapshots.
- Export assistant-specific bundles:
- Codex: `SKILL.md` plus `codex_bundle.json`
- Claude Code: `CLAUDE.md` plus `claude_code_bundle.json`
- Export pack-ready query bundles for Didactopus.
- Initialize an assistant-neutral host/project memory protocol with
`groundrecall protocol-init`.
## Installation
From a checkout:
```bash ```bash
python -m groundrecall.export /path/to/groundrecall-store /tmp/groundrecall-export \ python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .
groundrecall --help
```
For development:
```bash
.venv/bin/python -m pytest
```
The package also supports module invocation when working directly from source:
```bash
PYTHONPATH=src python -m groundrecall --help
```
## Basic Workflow
Import a source:
```bash
groundrecall import /path/to/source --out-root .groundrecall/imports --mode quick
```
Lint the import:
```bash
groundrecall lint .groundrecall/imports/<import-id>
```
Promote the import into a canonical store:
```bash
groundrecall promote .groundrecall/imports/<import-id> .groundrecall/store --reviewer your-name
```
Inspect or query the store:
```bash
groundrecall inspect .groundrecall/store
groundrecall query .groundrecall/store channel-capacity
```
Export assistant-neutral data:
```bash
groundrecall export .groundrecall/store .groundrecall/exports/canonical
```
Export assistant-specific data:
```bash
groundrecall assistant-export .groundrecall/store codex .groundrecall/exports/codex
groundrecall assistant-export .groundrecall/store claude_code .groundrecall/exports/claude_code
```
## Assistant-Neutral Host Protocol
GroundRecall can initialize a reusable memory pattern for a project or host:
```bash
groundrecall protocol-init /opt/www \
--host-id local-dev \
--host-role development \
--assistant codex \
--assistant claude_code
```
This creates:
- `.groundrecall/README.md`
- `.groundrecall/source-notes/host-profile-<host-id>.md`
- `.groundrecall/local-inbox/`
- `.groundrecall/remote-inbox/`
- `ASSISTANT_PROJECT.md`
- assistant bootstrap files such as `CODEX_PROJECT.md` and `CLAUDE.md`
Use `--force` only when you intend to overwrite existing bootstrap files.
For a two-host local/remote setup, each host should maintain its own
GroundRecall store and exchange source notes or exports. Do not make both hosts
write directly into the same mutable store.
See [docs/assistant-protocol.md](docs/assistant-protocol.md).
## Suggested Workspace Layout
```text
.groundrecall/
source-notes/
imports/
store/
exports/
canonical/
codex/
claude_code/
local-inbox/
remote-inbox/
```
`source-notes/` is where humans and assistants should leave durable Markdown
notes. Those notes can later be imported and promoted.
## Didactopus Bridge
GroundRecall can export a pack-ready `groundrecall_query_bundle.json` for a
reviewed concept:
```bash
groundrecall export /path/to/groundrecall-store /tmp/groundrecall-export \
--pack-ready-concept channel-capacity --pack-ready-concept channel-capacity
``` ```
The matching `Didactopus` bridge flow is: The matching Didactopus bridge flow is:
```bash ```bash
didactopus doclift-bundle-groundrecall \ didactopus doclift-bundle-groundrecall \
@ -27,7 +163,38 @@ didactopus doclift-bundle-groundrecall \
--course-title "Example Course" --course-title "Example Course"
``` ```
See: See [docs/didactopus-bridge.md](docs/didactopus-bridge.md).
- `docs/quickstart.md` ## Use Cases
- `docs/didactopus-bridge.md`
GroundRecall is useful when the same project may be touched by different
assistants, at different times, or on different hosts:
- A local development host and a remote production host both need operational
memory.
- Codex performs a code change locally, then Claude Code investigates a service
failure remotely.
- A WordPress or Forgejo service needs routing, backup, deployment, and recovery
notes that survive across sessions.
- A research corpus needs grounded claims, citations, source provenance, and
review state.
- Legacy office documents need `doclift` normalization before becoming
searchable assistant context.
## Safety Rules
- Store where secrets live, not secret values.
- Keep host-specific facts labeled by host and role.
- Treat production and mixed hosts as higher risk than development hosts.
- Prefer source-note/export replication between hosts over shared mutable stores.
- Commit code/config changes separately from generated GroundRecall exports
unless the export is intentionally part of the deliverable.
## Documentation
- [docs/quickstart.md](docs/quickstart.md)
- [docs/assistant-protocol.md](docs/assistant-protocol.md)
- [docs/architecture.md](docs/architecture.md)
- [docs/didactopus-bridge.md](docs/didactopus-bridge.md)
- [docs/llmwiki-import.md](docs/llmwiki-import.md)
- [docs/sync-roadmap.md](docs/sync-roadmap.md)

View File

@ -5,6 +5,7 @@ The top-level documentation in this repository is intended to describe `GroundRe
Primary docs: Primary docs:
- [quickstart.md](quickstart.md) - [quickstart.md](quickstart.md)
- [assistant-protocol.md](assistant-protocol.md)
- [architecture.md](architecture.md) - [architecture.md](architecture.md)
- [llmwiki-import.md](llmwiki-import.md) - [llmwiki-import.md](llmwiki-import.md)
- [sync-roadmap.md](sync-roadmap.md) - [sync-roadmap.md](sync-roadmap.md)

203
docs/assistant-protocol.md Normal file
View File

@ -0,0 +1,203 @@
# Assistant-Neutral GroundRecall Protocol
This document codifies a site-neutral pattern for using GroundRecall as durable
memory across assistants and hosts. It applies to Codex, Claude Code, and other
assistants that can read and write local files.
## Problem
Long-running site, app, and service work often crosses multiple sessions,
assistants, hosts, and operational modes. Plain chat history is not enough for
that. GroundRecall provides a local, reviewable memory store with provenance,
exports, and assistant-specific bundles.
## Principles
- Each host has its own GroundRecall workspace and canonical store.
- Local and remote hosts exchange source notes or exports, not shared mutable
store internals.
- Operational facts should be scoped by host, project, service, and role when
those details matter.
- Assistants should read memory before planning and update memory as durable
work progresses.
- Secrets are never stored in GroundRecall.
## Initialize A Host Or Project
Use `protocol-init`:
```bash
groundrecall protocol-init /opt/www \
--host-id local-dev \
--host-role development \
--hostname devbox \
--assistant codex \
--assistant claude_code
```
Supported host roles are `development`, `staging`, `production`, and `mixed`.
Use `--force` to overwrite existing bootstrap files.
Generated files include:
- `.groundrecall/README.md`
- `.groundrecall/source-notes/host-profile-<host-id>.md`
- `.groundrecall/local-inbox/`
- `.groundrecall/remote-inbox/`
- `ASSISTANT_PROJECT.md`
- `CODEX_PROJECT.md` when `--assistant codex` is used
- `CLAUDE.md` when `--assistant claude_code` is used
## Host Profile
The generated host profile records:
```yaml
host_id: local-dev
hostname: devbox
fqdn:
host_role: development
primary_root: /opt/www
groundrecall_root: /opt/www/.groundrecall
public_entrypoint:
last_verified: yyyy-mm-dd
```
Assistants should read this profile before changing services, deployment state,
data stores, routing, or recovery configuration.
## Operational Scopes
Use these scopes in source notes:
- `local-only`: applies only to this host.
- `remote-only`: applies only to a paired remote host.
- `shared`: applies to both hosts or project architecture independent of host.
- `deployment`: describes controlled transfer from one host to another.
- `recovery`: describes backup, restore, rollback, or disaster recovery.
Example note header:
```yaml
scope: deployment
host_id: remote-prod
project: example.net
service: wordpress
topic: REST routing under /wp
```
## Assistant Startup
At session start, assistants should:
1. Read `ASSISTANT_PROJECT.md`, `CODEX_PROJECT.md`, or `CLAUDE.md` if present.
2. Read `.groundrecall/README.md`.
3. Read `.groundrecall/source-notes/host-profile-*.md`.
4. Inspect canonical or assistant-specific exports.
5. Query relevant project/service memory before planning changes.
6. Check version-control status before edits.
7. Record durable findings as source notes.
Codex should prefer `.groundrecall/exports/codex/` when present. Claude Code
should prefer `.groundrecall/exports/claude_code/` when present. Other
assistants should use `.groundrecall/exports/canonical/`.
## Source Notes
Write durable findings to:
```text
.groundrecall/source-notes/<project-or-topic>-YYYYMMDD.md
```
A good source note includes host and role, project/service, changed files,
commands/tests run, deployment or restart actions, backup/recovery status if
data was touched, remaining risks, and next safe action.
Then import, promote, and export:
```bash
groundrecall import .groundrecall/source-notes/example-20260501.md \
--out-root .groundrecall/imports \
--mode quick
groundrecall promote .groundrecall/imports/<import-id> .groundrecall/store \
--reviewer codex
groundrecall export .groundrecall/store .groundrecall/exports/canonical
groundrecall assistant-export .groundrecall/store codex .groundrecall/exports/codex
groundrecall assistant-export .groundrecall/store claude_code .groundrecall/exports/claude_code
```
## Local/Remote Sharing
Do not make local and remote hosts write directly into the same store.
Recommended pattern:
1. Local host writes, promotes, and exports local notes.
2. Remote host imports selected local notes or canonical export with provenance.
3. Remote host writes, promotes, and exports remote notes.
4. Local host imports selected remote notes or canonical export with provenance.
Suggested inboxes:
```text
.groundrecall/local-inbox/
.groundrecall/remote-inbox/
```
Transport can be git, rsync, scp, Forgejo, or another controlled file sync. Do
not sync secrets.
## Deployment Records
Each deployed project should eventually have a source note or promoted record
containing:
```yaml
project: example.net
repo: git@git.example:owner/example.net.git
local_path: /opt/www/dev/example.net
remote_path: /opt/www/dev/example.net
host_scope: shared
compose_files:
- docker-compose.yml
- docker-compose-public.yml
containers:
- example_web
- example_db
deploy_method: git pull + docker compose up -d
pre_deploy:
- git status --short
- backup command if data-bearing
health_checks:
- https://example.net/
rollback:
- git checkout previous-known-good
- docker compose up -d
data_owner: remote-prod
```
## No-Secrets Rule
Allowed:
```text
The database password is in /opt/www/dev/example.net/.env.
The Forgejo config is mounted from /mnt/data/www/.../app.ini.
```
Not allowed:
```text
password=...
token=...
private key material
cookies
session IDs
database dumps containing credentials
```
If an assistant sees a secret, it should not copy it into GroundRecall, docs,
chat, or commits.

View File

@ -133,9 +133,27 @@ A simple local layout is:
The current alpha does not require this exact layout, but it is a sensible starting point. The current alpha does not require this exact layout, but it is a sensible starting point.
## Initialize Assistant Memory
For site, app, service, or deployment work, initialize the assistant-neutral
GroundRecall protocol:
```bash
groundrecall protocol-init /opt/www \
--host-id local-dev \
--host-role development \
--assistant codex \
--assistant claude_code
```
This writes a host profile, GroundRecall workspace README, assistant bootstrap
files, and local/remote inbox directories. See
[assistant-protocol.md](assistant-protocol.md).
## Next Reading ## Next Reading
- [architecture.md](architecture.md) - [architecture.md](architecture.md)
- [assistant-protocol.md](assistant-protocol.md)
- [didactopus-bridge.md](didactopus-bridge.md) - [didactopus-bridge.md](didactopus-bridge.md)
- [llmwiki-import.md](llmwiki-import.md) - [llmwiki-import.md](llmwiki-import.md)
- [sync-roadmap.md](sync-roadmap.md) - [sync-roadmap.md](sync-roadmap.md)

View File

@ -3,13 +3,14 @@ from __future__ import annotations
import argparse import argparse
import sys import sys
from . import assistant_export, export, ingest, inspect, lint, promotion, query, review_server from . import assistant_export, export, ingest, inspect, lint, promotion, protocol, query, review_server
COMMANDS = { COMMANDS = {
"import": ingest.main, "import": ingest.main,
"lint": lint.main, "lint": lint.main,
"promote": promotion.main, "promote": promotion.main,
"protocol-init": protocol.main,
"query": query.main, "query": query.main,
"export": export.main, "export": export.main,
"assistant-export": assistant_export.main, "assistant-export": assistant_export.main,
@ -26,15 +27,20 @@ def build_parser() -> argparse.ArgumentParser:
def main() -> None: def main() -> None:
argv = sys.argv[1:] argv = sys.argv[1:]
parser = build_parser() if argv and argv[0] in COMMANDS:
args, remainder = parser.parse_known_args(argv) command = argv[0]
if not args.command: remainder = argv[1:]
parser.print_help() else:
return parser = build_parser()
handler = COMMANDS[args.command] args, remainder = parser.parse_known_args(argv)
if not args.command:
parser.print_help()
return
command = args.command
handler = COMMANDS[command]
original_argv = sys.argv original_argv = sys.argv
try: try:
sys.argv = [f"groundrecall.cli {args.command}", *remainder] sys.argv = [f"groundrecall.cli {command}", *remainder]
handler() handler()
finally: finally:
sys.argv = original_argv sys.argv = original_argv

View File

@ -0,0 +1,304 @@
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass
from datetime import date
from pathlib import Path
from typing import Iterable
VALID_HOST_ROLES = {"development", "production", "staging", "mixed"}
DEFAULT_ASSISTANTS = ("codex", "claude_code")
@dataclass
class ProtocolInitResult:
root: Path
written: list[Path]
skipped: list[Path]
def as_dict(self) -> dict[str, object]:
return {
"root": str(self.root),
"written": [str(path) for path in self.written],
"skipped": [str(path) for path in self.skipped],
}
def slugify(text: str) -> str:
slug = re.sub(r"[^a-zA-Z0-9_.-]+", "-", text.strip()).strip("-")
return slug or "host"
def write_if_allowed(path: Path, text: str, *, force: bool, written: list[Path], skipped: list[Path]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and not force:
skipped.append(path)
return
path.write_text(text, encoding="utf-8")
written.append(path)
def host_profile_note(
*,
host_id: str,
host_role: str,
primary_root: str,
groundrecall_root: str,
hostname: str = "",
fqdn: str = "",
public_entrypoint: str = "",
) -> str:
return f"""# GroundRecall Host Profile - {host_id}
```yaml
host_id: {host_id}
hostname: {hostname}
fqdn: {fqdn}
host_role: {host_role}
primary_root: {primary_root}
groundrecall_root: {groundrecall_root}
public_entrypoint: {public_entrypoint}
last_verified: {date.today().isoformat()}
```
Purpose:
- Identify the operational role of this host for assistants using GroundRecall.
- Prevent assistants from assuming that a development, staging, production, or
mixed host has the same safety rules as another host.
Assistant startup rule:
- Read this host profile before changing services, deployment state, data
stores, routing, or recovery configuration.
- Treat host-specific facts as scoped to `{host_id}` unless a note explicitly
says `scope: shared`.
No-secrets rule:
- This profile may name where secrets are stored, but it must not contain secret
values, tokens, private keys, cookies, database passwords, or API keys.
"""
def workspace_readme(*, primary_root: str, groundrecall_root: str) -> str:
return f"""# GroundRecall Workspace
This directory is the host or project GroundRecall workspace.
Primary root: `{primary_root}`
GroundRecall root: `{groundrecall_root}`
## Layout
- `source-notes/`: durable Markdown notes written by humans or assistants.
- `imports/`: normalized import artifacts.
- `store/`: promoted canonical GroundRecall store.
- `exports/canonical/`: assistant-neutral exports.
- `exports/codex/`: Codex-targeted exports when generated.
- `exports/claude_code/`: Claude Code-targeted exports when generated.
- `local-inbox/`: notes or exports imported from a local/development peer.
- `remote-inbox/`: notes or exports imported from a remote/staging/production
peer.
## Assistant Startup
Assistants should:
1. Read `ASSISTANT_PROJECT.md`, `CODEX_PROJECT.md`, or `CLAUDE.md` if present.
2. Read this file and `source-notes/host-profile-*.md`.
3. Identify this host role before changing services or deployment state.
4. Query or inspect GroundRecall for the project/service in scope.
5. Write source notes for durable findings and promote/export them after
significant work.
## No-Secrets Rule
GroundRecall may store where secrets live, but not secret values. Do not store
passwords, tokens, private keys, cookies, database dumps with secrets, or
session material.
"""
def assistant_project_stub(*, primary_root: str, groundrecall_root: str) -> str:
return f"""# Assistant Project Bootstrap
Primary durable memory is GroundRecall.
- primary root: `{primary_root}`
- GroundRecall workspace: `{groundrecall_root}`
- canonical export: `{groundrecall_root}/exports/canonical`
- source notes: `{groundrecall_root}/source-notes`
On startup:
1. Identify this host and host role from `{groundrecall_root}/source-notes/host-profile-*.md`.
2. Inspect relevant GroundRecall context before planning site, app, service,
deployment, or recovery work.
3. Check version-control status before edits.
4. Update GroundRecall source notes as durable work progresses.
5. Do not store secrets in GroundRecall, chat, docs, or commits.
Use assistant-specific exports when available; otherwise use the canonical
GroundRecall export.
"""
def codex_stub(*, primary_root: str, groundrecall_root: str) -> str:
return f"""# CODEX_PROJECT
Primary durable memory is GroundRecall.
- primary root: `{primary_root}`
- GroundRecall workspace: `{groundrecall_root}`
- canonical export: `{groundrecall_root}/exports/canonical`
- Codex export: `{groundrecall_root}/exports/codex`
On startup, identify this host role, read the relevant GroundRecall export, and
query or inspect project/service memory before planning changes. Update
GroundRecall source notes as durable work progresses. Do not store secrets in
GroundRecall, chat, docs, or commits.
"""
def claude_stub(*, primary_root: str, groundrecall_root: str) -> str:
return f"""# CLAUDE
Primary durable memory is GroundRecall.
- primary root: `{primary_root}`
- GroundRecall workspace: `{groundrecall_root}`
- canonical export: `{groundrecall_root}/exports/canonical`
- Claude Code export: `{groundrecall_root}/exports/claude_code`
On startup, identify this host role, read relevant GroundRecall context, and
check project/service memory before planning changes. Update source notes for
durable findings. Do not store secrets in memory, chat, docs, or commits.
"""
def normalize_assistants(assistants: Iterable[str] | None) -> list[str]:
values = list(assistants or DEFAULT_ASSISTANTS)
normalized: list[str] = []
for value in values:
name = value.strip().lower().replace("-", "_")
if name and name not in normalized:
normalized.append(name)
return normalized
def initialize_protocol(
root: str | Path,
*,
host_id: str,
host_role: str,
primary_root: str | None = None,
groundrecall_root: str | None = None,
hostname: str = "",
fqdn: str = "",
public_entrypoint: str = "",
assistants: Iterable[str] | None = None,
force: bool = False,
) -> ProtocolInitResult:
if host_role not in VALID_HOST_ROLES:
raise ValueError(f"host_role must be one of: {', '.join(sorted(VALID_HOST_ROLES))}")
root_path = Path(root)
primary_root_value = primary_root or str(root_path)
groundrecall_value = groundrecall_root or str(root_path / ".groundrecall")
groundrecall_path = Path(groundrecall_value)
if not groundrecall_path.is_absolute():
groundrecall_path = root_path / groundrecall_path
groundrecall_value = str(groundrecall_path)
written: list[Path] = []
skipped: list[Path] = []
for rel in ("source-notes", "imports", "store", "exports/canonical", "local-inbox", "remote-inbox"):
(groundrecall_path / rel).mkdir(parents=True, exist_ok=True)
write_if_allowed(
groundrecall_path / "README.md",
workspace_readme(primary_root=primary_root_value, groundrecall_root=groundrecall_value),
force=force,
written=written,
skipped=skipped,
)
write_if_allowed(
groundrecall_path / "source-notes" / f"host-profile-{slugify(host_id)}.md",
host_profile_note(
host_id=host_id,
host_role=host_role,
primary_root=primary_root_value,
groundrecall_root=groundrecall_value,
hostname=hostname,
fqdn=fqdn,
public_entrypoint=public_entrypoint,
),
force=force,
written=written,
skipped=skipped,
)
write_if_allowed(
root_path / "ASSISTANT_PROJECT.md",
assistant_project_stub(primary_root=primary_root_value, groundrecall_root=groundrecall_value),
force=force,
written=written,
skipped=skipped,
)
assistant_names = normalize_assistants(assistants)
if "codex" in assistant_names:
write_if_allowed(
root_path / "CODEX_PROJECT.md",
codex_stub(primary_root=primary_root_value, groundrecall_root=groundrecall_value),
force=force,
written=written,
skipped=skipped,
)
if "claude_code" in assistant_names:
write_if_allowed(
root_path / "CLAUDE.md",
claude_stub(primary_root=primary_root_value, groundrecall_root=groundrecall_value),
force=force,
written=written,
skipped=skipped,
)
return ProtocolInitResult(root=root_path, written=written, skipped=skipped)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Initialize assistant-neutral GroundRecall protocol files.")
parser.add_argument("root", help="Project or host root where bootstrap files should be written.")
parser.add_argument("--host-id", required=True, help="Stable host identifier, for example local-dev or remote-prod.")
parser.add_argument("--host-role", required=True, choices=sorted(VALID_HOST_ROLES))
parser.add_argument("--primary-root", help="Operational root recorded in generated notes. Defaults to root.")
parser.add_argument("--groundrecall-root", help="GroundRecall workspace path. Defaults to ROOT/.groundrecall.")
parser.add_argument("--hostname", default="")
parser.add_argument("--fqdn", default="")
parser.add_argument("--public-entrypoint", default="")
parser.add_argument("--assistant", action="append", default=[], help="Assistant bootstrap to write: codex, claude_code. Repeatable.")
parser.add_argument("--force", action="store_true", help="Overwrite existing bootstrap/profile files.")
return parser
def main() -> None:
args = build_parser().parse_args()
result = initialize_protocol(
args.root,
host_id=args.host_id,
host_role=args.host_role,
primary_root=args.primary_root,
groundrecall_root=args.groundrecall_root,
hostname=args.hostname,
fqdn=args.fqdn,
public_entrypoint=args.public_entrypoint,
assistants=args.assistant or DEFAULT_ASSISTANTS,
force=args.force,
)
print(json.dumps(result.as_dict(), indent=2))
if __name__ == "__main__":
main()

View File

@ -15,3 +15,20 @@ def test_groundrecall_console_script_help() -> None:
assert result.returncode == 0 assert result.returncode == 0
assert "GroundRecall command-line tools" in result.stdout assert "GroundRecall command-line tools" in result.stdout
assert "protocol-init" in result.stdout
def test_groundrecall_subcommand_help_reaches_subcommand_parser() -> None:
executable = shutil.which("groundrecall")
assert executable is not None
result = subprocess.run(
[executable, "protocol-init", "--help"],
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0
assert "--host-id" in result.stdout
assert "--host-role" in result.stdout

View File

@ -0,0 +1,75 @@
from __future__ import annotations
from pathlib import Path
import pytest
from groundrecall.cli import COMMANDS
from groundrecall.protocol import initialize_protocol
def test_protocol_init_writes_host_profile_and_bootstraps(tmp_path: Path) -> None:
result = initialize_protocol(
tmp_path,
host_id="local-dev",
host_role="development",
hostname="localbox",
assistants=["codex", "claude_code"],
)
written = {path.name for path in result.written}
assert "README.md" in written
assert "ASSISTANT_PROJECT.md" in written
assert "CODEX_PROJECT.md" in written
assert "CLAUDE.md" in written
assert (tmp_path / ".groundrecall" / "source-notes" / "host-profile-local-dev.md").exists()
assert (tmp_path / ".groundrecall" / "local-inbox").is_dir()
assert (tmp_path / ".groundrecall" / "remote-inbox").is_dir()
host_profile = (tmp_path / ".groundrecall" / "source-notes" / "host-profile-local-dev.md").read_text()
assert "host_id: local-dev" in host_profile
assert "host_role: development" in host_profile
assert "hostname: localbox" in host_profile
assert "No-secrets rule" in host_profile
codex = (tmp_path / "CODEX_PROJECT.md").read_text()
claude = (tmp_path / "CLAUDE.md").read_text()
assert "GroundRecall workspace" in codex
assert "Claude Code export" in claude
def test_protocol_init_does_not_overwrite_without_force(tmp_path: Path) -> None:
(tmp_path / "CODEX_PROJECT.md").write_text("existing\n", encoding="utf-8")
result = initialize_protocol(
tmp_path,
host_id="remote-prod",
host_role="production",
assistants=["codex"],
)
assert (tmp_path / "CODEX_PROJECT.md").read_text() == "existing\n"
assert tmp_path / "CODEX_PROJECT.md" in result.skipped
def test_protocol_init_force_overwrites_existing_bootstrap(tmp_path: Path) -> None:
(tmp_path / "CODEX_PROJECT.md").write_text("existing\n", encoding="utf-8")
initialize_protocol(
tmp_path,
host_id="remote-prod",
host_role="production",
assistants=["codex"],
force=True,
)
assert "host role" in (tmp_path / "CODEX_PROJECT.md").read_text()
def test_protocol_init_rejects_unknown_host_role(tmp_path: Path) -> None:
with pytest.raises(ValueError):
initialize_protocol(tmp_path, host_id="x", host_role="laptop")
def test_cli_exposes_protocol_init_command() -> None:
assert "protocol-init" in COMMANDS