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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ WikiBricks is shared memory for multiple agent harnesses.
when Lakebase, Databricks credentials, and the network are absent.
- `import omnigent-server` is an explicit, opt-in network command like
`sync lakebase`.
- `wikibricks-curator` is an optional, explicit network command that calls one
model. The core library still never calls a model.

The library does not call a language model. The active agent makes semantic
decisions about page content. Deterministic local maintenance repairs indexes
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

- Added optional `wikibricks-curator propose` for local backlog curation. It
stores one model-generated run, auto-applies low-risk groups under the safe
policy, leaves other groups for review, and lists pending review runs in
`wiki_index`.

## 0.12.3 - 2026-09-29

- Local curation now reports the newest projects whose sessions are newer than
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,29 @@ only after every immutable event version has a committed archive receipt:
wikibricks curate --prune-archived-sessions-after-days 90
```

## Nightly curation (optional)

`wikibricks-curator propose` asks one Databricks model for updates to the top
local backlog projects. It writes those proposals as a curation run, applies
low-risk groups with the safe policy, and leaves high-risk or conflicting
groups for review. Run it only when you intend to call the model:

```bash
wikibricks-curator propose \
--database-path ~/.wikibricks/wikibricks.db \
--base-url https://YOUR-DATABRICKS-WORKSPACE/serving-endpoints \
--profile PROFILE
```

Use `--dry-run` to print proposals without writing. Use `--no-apply` to store
every run for inspection. `wiki_index` lists pending runs as
`_meta/curation-review`. After reading them, run:

```bash
wikibricks sync plan RUN_ID
wikibricks sync apply RUN_ID --policy all
```

## Optional Lakebase curation

Local memory does not need Lakebase. Configure it only when you want a remote
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ postgres-migration = [

[project.scripts]
wikibricks = "wikibricks.cli:main"
wikibricks-curator = "wikibricks_curator.cli:main"
wiki-init = "wikibricks.cli:init_main"
wikibricks-mcp = "wikibricks.mcp_server:main"
wikibricks-remote-maintenance = "wikibricks_remote.main:main"
Expand All @@ -33,7 +34,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/wikibricks", "src/wikibricks_remote"]
packages = ["src/wikibricks", "src/wikibricks_remote", "src/wikibricks_curator"]

[tool.ruff]
target-version = "py314"
Expand Down
6 changes: 6 additions & 0 deletions src/wikibricks/curation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@
from wikibricks.curation.planning import plan_run
from wikibricks.curation.protocol import (
build_manifest,
canonical_json,
create_patch,
validate_manifest,
)
from wikibricks.curation.repository import (
get_or_create_replica_id,
list_conflicts,
pending_review_runs,
publish_manifest,
pull_manifests,
store_manifest,
)

__all__ = [
"apply_run",
"build_manifest",
"canonical_json",
"create_patch",
"get_or_create_replica_id",
"list_conflicts",
"pending_review_runs",
"plan_run",
"publish_manifest",
"pull_manifests",
"resolve_conflict",
"store_manifest",
"validate_manifest",
]
35 changes: 28 additions & 7 deletions src/wikibricks/curation/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ def publish_manifest(
return _insert_manifest(conn, checked, received=False)


def store_manifest(
store: PostgresStore | SQLiteStore,
manifest: dict[str, Any],
) -> bool:
checked = validate_manifest(manifest)
if isinstance(store, SQLiteStore):
with store.connection(write=True) as conn:
return _insert_manifest(conn, checked, received=True)
with store.connection() as conn, conn.transaction():
return _insert_manifest(conn, checked, received=True)


def pull_manifests(
local: PostgresStore | SQLiteStore,
remote: PostgresStore,
Expand All @@ -153,13 +165,8 @@ def pull_manifests(
received_runs = 0
received_patches = 0
for row in rows:
manifest = validate_manifest(dict(row[0]))
if isinstance(local, SQLiteStore):
with local.connection(write=True) as conn:
inserted = _insert_manifest(conn, manifest, received=True)
else:
with local.connection() as conn, conn.transaction():
inserted = _insert_manifest(conn, manifest, received=True)
manifest = dict(row[0])
inserted = store_manifest(local, manifest)
if inserted:
received_runs += 1
received_patches += len(manifest["patches"])
Expand Down Expand Up @@ -417,3 +424,17 @@ def list_conflicts(store: PostgresStore | SQLiteStore) -> list[dict[str, Any]]:
}
for row in rows
]


def pending_review_runs(store: PostgresStore | SQLiteStore) -> list[dict[str, Any]]:
store.migrate()
with store.connection() as conn:
rows = conn.execute(
"SELECT p.run_id, count(*) FROM curation_patches p "
"LEFT JOIN curation_receipts r ON r.patch_id = p.patch_id "
"WHERE r.patch_id IS NULL GROUP BY p.run_id ORDER BY p.run_id"
).fetchall()
return [
{"run_id": str(row[0]), "pending_patches": int(row[1])}
for row in rows
]
18 changes: 18 additions & 0 deletions src/wikibricks/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def _build_tools() -> dict[str, Any]:
write_tools = make_agent_tools(database_path=str(client.database_path))
def wiki_index(prefix=None):
from wikibricks.curation.backlog import load_curation_backlog
from wikibricks.curation.repository import pending_review_runs
from wikibricks.maintenance import capture_status

pages = [
Expand Down Expand Up @@ -56,6 +57,23 @@ def wiki_index(prefix=None):
"items": backlog,
}
)
pending_runs = pending_review_runs(client.store)
if pending_runs:
pending = sum(run["pending_patches"] for run in pending_runs)
pages.append(
{
"path": "_meta/curation-review",
"page_type": "review",
"title": f"Curation review: {pending} proposed changes wait for review",
"summary": (
f"Tell the user about these proposed changes, or for example run "
f"`wikibricks sync plan {pending_runs[0]['run_id']}` and "
f"`wikibricks sync apply {pending_runs[0]['run_id']} --policy all` "
"after checking."
),
"items": pending_runs,
}
)
return pages

return {
Expand Down
3 changes: 3 additions & 0 deletions src/wikibricks_curator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Local curator request and gateway clients."""

__all__: list[str] = []
54 changes: 54 additions & 0 deletions src/wikibricks_curator/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Command-line entry point for the optional local curator."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

from wikibricks_curator.gateway import chat_json, resolve_token


def build_parser() -> argparse.ArgumentParser:
from wikibricks.config import load_config

config = load_config()
parser = argparse.ArgumentParser(prog="wikibricks-curator")
commands = parser.add_subparsers(dest="command", required=True)
propose = commands.add_parser("propose", help="Propose local curation updates")
propose.add_argument("--database-path", type=Path, default=config.database_path)
propose.add_argument("--base-url", required=True)
propose.add_argument("--profile", default=config.sync_profile)
propose.add_argument("--model", default="system.ai.glm-5-3-flash")
propose.add_argument("--projects", type=int, default=3)
propose.add_argument("--no-apply", action="store_true")
propose.add_argument("--dry-run", action="store_true")
return parser


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
from wikibricks_curator.curator import run_curator

token = resolve_token(args.profile)

def chat(system_prompt: str, request: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
return chat_json(
system_prompt,
request,
schema,
base_url=args.base_url,
token=token,
model=args.model,
)

result = run_curator(
args.database_path,
chat=chat,
projects=args.projects,
apply=not args.no_apply,
dry_run=args.dry_run,
)
print(json.dumps(result, default=str, indent=2))
return 1 if result["errors"] else 0
Loading
Loading