Add assistant-neutral GroundRecall protocol
This commit is contained in:
parent
88a547463d
commit
066dd1d42f
193
README.md
193
README.md
|
|
@ -1,22 +1,158 @@
|
|||
# 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
|
||||
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 is intended for work where durable context matters:
|
||||
|
||||
`GroundRecall` can now also export a pack-ready
|
||||
`groundrecall_query_bundle.json` for a reviewed concept so `Didactopus` can
|
||||
carry that concept context into a learner-facing pack:
|
||||
- site, app, and service administration across sessions
|
||||
- local/remote deployment memory with host-role distinctions
|
||||
- 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
|
||||
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
|
||||
```
|
||||
|
||||
The matching `Didactopus` bridge flow is:
|
||||
The matching Didactopus bridge flow is:
|
||||
|
||||
```bash
|
||||
didactopus doclift-bundle-groundrecall \
|
||||
|
|
@ -27,7 +163,38 @@ didactopus doclift-bundle-groundrecall \
|
|||
--course-title "Example Course"
|
||||
```
|
||||
|
||||
See:
|
||||
See [docs/didactopus-bridge.md](docs/didactopus-bridge.md).
|
||||
|
||||
- `docs/quickstart.md`
|
||||
- `docs/didactopus-bridge.md`
|
||||
## Use Cases
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ The top-level documentation in this repository is intended to describe `GroundRe
|
|||
Primary docs:
|
||||
|
||||
- [quickstart.md](quickstart.md)
|
||||
- [assistant-protocol.md](assistant-protocol.md)
|
||||
- [architecture.md](architecture.md)
|
||||
- [llmwiki-import.md](llmwiki-import.md)
|
||||
- [sync-roadmap.md](sync-roadmap.md)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
- [architecture.md](architecture.md)
|
||||
- [assistant-protocol.md](assistant-protocol.md)
|
||||
- [didactopus-bridge.md](didactopus-bridge.md)
|
||||
- [llmwiki-import.md](llmwiki-import.md)
|
||||
- [sync-roadmap.md](sync-roadmap.md)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ from __future__ import annotations
|
|||
import argparse
|
||||
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 = {
|
||||
"import": ingest.main,
|
||||
"lint": lint.main,
|
||||
"promote": promotion.main,
|
||||
"protocol-init": protocol.main,
|
||||
"query": query.main,
|
||||
"export": export.main,
|
||||
"assistant-export": assistant_export.main,
|
||||
|
|
@ -26,15 +27,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
|
||||
def main() -> None:
|
||||
argv = sys.argv[1:]
|
||||
parser = build_parser()
|
||||
args, remainder = parser.parse_known_args(argv)
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
handler = COMMANDS[args.command]
|
||||
if argv and argv[0] in COMMANDS:
|
||||
command = argv[0]
|
||||
remainder = argv[1:]
|
||||
else:
|
||||
parser = build_parser()
|
||||
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
|
||||
try:
|
||||
sys.argv = [f"groundrecall.cli {args.command}", *remainder]
|
||||
sys.argv = [f"groundrecall.cli {command}", *remainder]
|
||||
handler()
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -15,3 +15,20 @@ def test_groundrecall_console_script_help() -> None:
|
|||
|
||||
assert result.returncode == 0
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue