diff --git a/docs/architecture/KEY_FILES.md b/docs/architecture/KEY_FILES.md index d7cffd6cb..482cb624c 100644 --- a/docs/architecture/KEY_FILES.md +++ b/docs/architecture/KEY_FILES.md @@ -396,7 +396,7 @@ per-release `**vX.Y.Z:**` narration — CI enforces this - `src/commands/lint.ts` — Page quality linter (catches LLM artifacts, placeholder dates) - `src/commands/report.ts` — Structured report saver (audit trail for maintenance/enrichment) - `src/core/destructive-guard.ts` — three-layer protection against accidental data loss. `assessDestructiveImpact(engine, sourceId)` counts pages/chunks/embeddings/files for a source. `checkDestructiveConfirmation(impact, opts)` is the fail-closed gate (`--confirm-destructive` required when data is present; `--yes` alone is rejected). `softDeleteSource` / `restoreSource` / `listArchivedSources` / `purgeExpiredSources` drive the source-level archive lifecycle via `sources.archived BOOLEAN`, `archived_at TIMESTAMPTZ`, `archive_expires_at TIMESTAMPTZ`. Page-level analog: `BrainEngine.softDeletePage` / `restorePage` / `purgeDeletedPages` plus `pages.deleted_at TIMESTAMPTZ` and a partial purge index. The MCP `delete_page` op rewires to `softDeletePage`; ops `restore_page` (`scope: write`) and `purge_deleted_pages` (`scope: admin`, `localOnly: true`) round out the surface. Search visibility (`buildVisibilityClause` in `src/core/search/sql-ranking.ts`) hides soft-deleted pages and archived sources from `searchKeyword` / `searchKeywordChunks` / `searchVector` in both engines. The autopilot cycle's `purge` phase calls `purgeExpiredSources` + `engine.purgeDeletedPages(72)` so the 72h TTL is real. -- `src/commands/pages.ts` — `gbrain pages purge-deleted [--older-than HOURS|Nd] [--dry-run] [--json]` operator escape hatch. Mirror of `gbrain sources purge` for the page-level lifecycle. Hard-deletes pages whose `deleted_at` is older than the cutoff; cascades to content_chunks/page_links/chunk_relations. +- `src/commands/pages.ts` — `gbrain purge-deleted [--older-than HOURS|Nd] [--dry-run] [--json]` operator escape hatch. Mirror of `gbrain sources purge` for the page-level lifecycle. Hard-deletes pages whose `deleted_at` is older than the cutoff; cascades to content_chunks/page_links/chunk_relations. - `src/core/op-checkpoint.ts` — DB-backed checkpoint primitive for long-running ops. Migration v67 introduces `op_checkpoints (op TEXT, fingerprint TEXT, completed_keys JSONB, updated_at TIMESTAMPTZ, PK(op, fingerprint))`. Per-op fingerprint helpers (`embedFingerprint`, `extractFingerprint`, `reindexFingerprint`, `integrityFingerprint`, `purgeFingerprint`) compute `sha8(canonical-JSON(relevant-params))` so re-running with the same params resumes from `completed_keys` and re-running with different params (e.g. `--limit 100` vs `--limit 200`) starts fresh. Cross-worker safe on Postgres (DB row, no file-lock race); PGLite degrades gracefully. Replaces per-op file-backed JSON checkpoints scattered across `import.ts`, `embed.ts`, `reindex.ts`. The 7-day TTL GC runs in the cycle's `purge` phase. All writes (`recordCompleted`, `clearOpCheckpoint`) route through `engine.executeRawDirect` + `withRetry(BULK_RETRY_OPTS)` so they survive Supavisor pool exhaustion, and `recordCompleted` returns `boolean` (banked vs failed-after-retries) — the 9 non-sync consumers keep its REPLACE-into-`completed_keys` semantics. Resumable sync uses the additive `appendCompleted(key, deltaKeys)` / `appendCompletedOnce` (the latter no-retry for the SIGTERM path) which INSERT a delta into the `op_checkpoint_paths` child table (migration v115: `(op, fingerprint, path)` PK, FK to `op_checkpoints` ON DELETE CASCADE) via a single writable-CTE `unnest($3::text[])` write — O(delta), killing the old O(N²) full-set rewrite. `loadOpCheckpoint` returns the `UNION ALL` of legacy `completed_keys` + child-table paths (deduped in JS), so an in-flight upgrade loses nothing. The legacy arm is gated on `jsonb_typeof(completed_keys) = 'array'` so a non-array (scalar) parent row can't make `jsonb_array_elements_text` throw "cannot extract elements from a scalar" and take down the whole union (which would discard the valid child rows and lose all banked progress for the key); a third union arm flags the corruption so the loader logs it once and keeps the child rows. Migration v119 adds the `op_checkpoints_completed_keys_array` CHECK (`jsonb_typeof(completed_keys) = 'array'`) — a DB-enforced, always-on guard that makes the scalar-corruption class structurally impossible going forward; the migration repairs any pre-existing scalar to `'[]'` under `LOCK TABLE ... IN SHARE ROW EXCLUSIVE MODE` and `src/core/schema-embedded.ts` + `src/core/pglite-schema.ts` ship the same CHECK on fresh installs (a loader hit now implies schema drift, a disabled constraint, or an out-of-band writer). `recordCompleted` binds its array through `$3::text::jsonb` (NOT a bare `$3::jsonb`) so postgres.js `.unsafe()` doesn't double-encode `JSON.stringify(sorted)` into the scalar string that CHECK rejects — the #2339 bug that aborted every multi-source sync at the first pin write (PGLite parsed it silently, so it shipped). A DATABASE_URL-gated `test/e2e/op-checkpoint-jsonb-parity.test.ts` (its own CI job) asserts the array shape on real Postgres. `syncFingerprint({sourceId, lastCommit})` keys the sync rows. Pinned by `test/op-checkpoint.test.ts` (incl. delta-append, union read, cascade clear, durable-write boolean, and the scalar-parent guard). `import-checkpoint.ts` was NOT migrated to this primitive — both checkpoint systems coexist without conflict; migrating requires async-propagating 4 sync call sites in `src/commands/import.ts` and rewriting 18 tests, deferred. - `src/core/brain-score-recommendations.ts` — pure data layer consumed by both `gbrain doctor --remediation-plan` / `--remediate` and `gbrain features`. `computeRecommendations(checks, opts)` returns `Remediation[]` with stable `id`, content-hash `idempotency_key`, `severity`, `est_seconds`, `est_usd_cost`, `depends_on` (references stable ids, not check names — so plan order is reproducible). `classifyChecks(report)` triages every doctor check three-state into `remediable | human_only | blocked` (`human_only` covers RLS warnings and other human-judgment gates; `blocked` covers dependency chains where a parent check failed). `maxReachableScore(checks)` computes the ceiling for empty/under-configured brains (no entity pages → graph_coverage caps at 70; no embedding key → embedding_coverage caps at 60). Cost estimates pull from `anthropic-pricing.ts` (synthesize/patterns/consolidate) and `embedding-pricing.ts` (embed jobs). Pinned by `test/brain-score-recommendations.test.ts` (~27 cases incl. determinism, content-hash idempotency, DB-backed checkpoint provenance, three-state triage). - `src/commands/doctor.ts` extension — `--remediation-plan [--json] [--target-score N]` prints what would run (stable `id`, `idempotency_key`, `severity`, `est_seconds`, `est_usd_cost`, `depends_on`); `--remediate [--yes] [--target-score N] [--max-usd N]` submits each plan step as a Minion job in dependency order, re-checking score between steps. `--target-score N` defaults to 90; refuses to start when target exceeds `maxReachableScore()` and lists what's missing. `--max-usd N` is the cron-safety guard — submission refuses when the plan's `est_total_usd_cost` exceeds the cap. JSON envelope adds a `Check.remediation` field (additive, schema_version unchanged). Pinned by tests in `test/doctor.test.ts`. diff --git a/docs/architecture/pack-upgrade-mechanism.md b/docs/architecture/pack-upgrade-mechanism.md index 0364e67b4..fce352a21 100644 --- a/docs/architecture/pack-upgrade-mechanism.md +++ b/docs/architecture/pack-upgrade-mechanism.md @@ -230,7 +230,7 @@ add `GBRAIN_AUDIT_FULL=1` (v0.43+ TODO; not yet wired). - Per-source pack-upgrade (the handler accepts `sourceId` but `findPackSuccessors` doesn't yet pass it through) - Cross-brain federated mounts that disagree on canonical packs -- Automatic rollback (today: manual SQL or `gbrain pages restore`) +- Automatic rollback (today: manual SQL or `gbrain restore`) - LLM-assisted mapping_rules codegen from production data (`gbrain schema detect-mappings`; deferred to v0.43+) diff --git a/docs/architecture/schema-packs.md b/docs/architecture/schema-packs.md index bb11888e2..e8bf8c7ec 100644 --- a/docs/architecture/schema-packs.md +++ b/docs/architecture/schema-packs.md @@ -214,7 +214,7 @@ gbrain schema downgrade 1. `git revert ` — restores the code. 2. `gbrain schema downgrade --to gbrain-base` — restores config. -3. (Optional) `gbrain pages purge-deleted --older-than 0h` — drops +3. (Optional) `gbrain purge-deleted --older-than 0h` — drops v0.39-typed pages that no longer have a matching type in the active pack. diff --git a/docs/architecture/system-of-record.md b/docs/architecture/system-of-record.md index a4f67bf4b..283c7eb4f 100644 --- a/docs/architecture/system-of-record.md +++ b/docs/architecture/system-of-record.md @@ -19,11 +19,13 @@ entire DB from scratch. This means: -- **Disaster recovery is one command.** If your DB volume corrupts, if - Postgres eats itself, if PGLite's WASM lock wedges — you don't need - a backup. You wipe the DB, re-import from your brain repo, and the - derived state regenerates. v0.32.3 ships `gbrain rebuild - --confirm-destructive` as the documented one-liner. +- **Disaster recovery is a short, boring sequence.** If your DB volume + corrupts, if Postgres eats itself, if PGLite's WASM lock wedges — you + don't need a backup. You wipe the derived tables (on PGLite, + `gbrain reinit-pglite` wipes the whole embedded DB), re-import from + your brain repo with `gbrain sync`, and `gbrain extract all` + regenerates the derived state. See "Disaster recovery" below for the + exact commands. - **Multi-machine sync is git.** Your brain is a repo. Push from one machine, pull from another, and the second machine's DB rebuilds on its next sync. No "back up the database" step. @@ -146,11 +148,9 @@ The promise the rule makes: # Snapshot what's there gbrain stats > /tmp/before.txt -# Wipe and rebuild -gbrain rebuild --confirm-destructive # v0.32.3 — deletes derived tables - # (pages + content_chunks survive - # the CASCADE-safe design) - # OR manually for v0.32.2: +# Wipe and rebuild — delete the derived tables (pages + content_chunks +# survive the CASCADE-safe design), then re-derive from the repo. +# On PGLite, `gbrain reinit-pglite` wipes the whole embedded DB instead. psql -c 'DELETE FROM facts; DELETE FROM takes; DELETE FROM links; DELETE FROM timeline_entries;' gbrain sync gbrain extract all diff --git a/docs/architecture/type-taxonomy.md b/docs/architecture/type-taxonomy.md index 84e27cdf3..138d7b52f 100644 --- a/docs/architecture/type-taxonomy.md +++ b/docs/architecture/type-taxonomy.md @@ -109,8 +109,8 @@ Every primitive ships with a documented rollback: | Operation | Rollback | |-----------|----------| | Retype | `frontmatter.legacy_type = ` preserved on every page (D8). One SQL UPDATE restores types: `UPDATE pages SET type = frontmatter->>'legacy_type' WHERE frontmatter ? 'legacy_type'`. | -| Page-to-link | Source page soft-deleted with 72h TTL. `gbrain pages restore ` within 72h. Link row stays harmless if source restored. | -| Page-to-alias | Source page soft-deleted with 72h TTL. `gbrain pages restore ` within 72h. Alias row stays harmless (or `DELETE FROM slug_aliases WHERE alias_slug = ` to clean up). | +| Page-to-link | Source page soft-deleted with 72h TTL. `gbrain restore ` within 72h. Link row stays harmless if source restored. | +| Page-to-alias | Source page soft-deleted with 72h TTL. `gbrain restore ` within 72h. Alias row stays harmless (or `DELETE FROM slug_aliases WHERE alias_slug = ` to clean up). | | Active-pack flip | `gbrain schema use gbrain-base` reverses the flip. | ## What if my brain doesn't fit? diff --git a/docs/ethos/MARKDOWN_SKILLS_AS_RECIPES.md b/docs/ethos/MARKDOWN_SKILLS_AS_RECIPES.md index 9178714e3..cc80e66fd 100644 --- a/docs/ethos/MARKDOWN_SKILLS_AS_RECIPES.md +++ b/docs/ethos/MARKDOWN_SKILLS_AS_RECIPES.md @@ -183,6 +183,6 @@ This also means the best AI agent setups will be open source by default. Closed, Software distribution reimagined: the package is a markdown file, the runtime is a sufficiently smart model, the package manager is your AI agent, and the app store is a git repo. -`gbrain install voice-agent` +`gbrain skillpack scaffold voice-agent` That's it. diff --git a/docs/guides/compiled-truth.md b/docs/guides/compiled-truth.md index edbf6f0f1..43a9329b4 100644 --- a/docs/guides/compiled-truth.md +++ b/docs/guides/compiled-truth.md @@ -69,7 +69,7 @@ update_brain_page(slug, new_info, source): page = gbrain get {slug} // TIMELINE: always APPEND (never edit existing entries) - gbrain add_timeline_entry {slug} { + gbrain timeline-add {slug} { date: today, summary: new_info.summary, detail: new_info.detail, diff --git a/docs/guides/content-media.md b/docs/guides/content-media.md index 853d3468a..9fa257bd1 100644 --- a/docs/guides/content-media.md +++ b/docs/guides/content-media.md @@ -46,10 +46,10 @@ on user_shares_media(url_or_file): # Step 4: Extract and cross-reference entities for person in transcript.mentioned_people: - gbrain add_link - gbrain add_link - gbrain add_timeline_entry \ - --entry "Discussed in {video_title}: {what_was_said}" \ + gbrain link + gbrain link + gbrain timeline-add {date} \ + "Discussed in {video_title}: {what_was_said}" \ --source "YouTube: {url}" # PATTERN 2: Social Media Bundles @@ -80,8 +80,8 @@ on user_shares_media(url_or_file): # Extract entities and cross-reference for entity in bundle.mentioned_entities: - gbrain add_link - gbrain add_link + gbrain link + gbrain link # PATTERN 3: PDFs and Documents elif media.type == "pdf" or media.type == "document": @@ -109,8 +109,8 @@ on user_shares_media(url_or_file): """ for entity in document.mentioned_entities: - gbrain add_link - gbrain add_link + gbrain link + gbrain link # Always sync after ingestion gbrain sync @@ -127,7 +127,7 @@ on user_shares_media(url_or_file): ## How to Verify 1. Ingest a YouTube video. Run `gbrain get media/youtube/{slug}`. Confirm the page has: the agent's analysis (not just a summary), key quotes with speaker attribution, and the full diarized transcript. -2. Run `gbrain get_links media/youtube/{slug}`. Confirm back-links exist to brain pages for every person and company mentioned in the video. +2. Run `gbrain call get_links '{"slug": "media/youtube/{slug}"}'`. Confirm back-links exist to brain pages for every person and company mentioned in the video. 3. Pick a person mentioned in the video. Run `gbrain get `. Confirm their timeline has a new entry referencing the video with specific context. 4. Ingest a tweet. Confirm the brain page includes the thread context, linked article summaries, and entity cross-references -- not just the tweet text. 5. Run `gbrain search "{topic_from_video}"`. Confirm the media page appears in search results (verifies the content is indexed and searchable). diff --git a/docs/guides/enrichment-pipeline.md b/docs/guides/enrichment-pipeline.md index 7b0e2fec3..a9caad98d 100644 --- a/docs/guides/enrichment-pipeline.md +++ b/docs/guides/enrichment-pipeline.md @@ -49,23 +49,23 @@ on enrich(entity, trigger): data["contacts"] = google_contacts(entity.email) # Contact data # Step 5: Store raw data (auditable, re-processable) - gbrain put_raw_data \ - --data '{"sources": {"crustdata": {"fetched_at": "...", "data": {...}}, ...}}' + gbrain call put_raw_data \ + '{"slug": "", "data": {"sources": {"crustdata": {"fetched_at": "...", "data": {...}}, ...}}}' # Overwrite on re-enrichment, don't append # Step 6: Write to brain page if path == "CREATE": gbrain put --content "" - gbrain add_timeline_entry --entry "Page created via enrichment" + gbrain timeline-add {date} "Page created via enrichment" elif path == "UPDATE": # Append timeline, update compiled truth ONLY if materially new - gbrain add_timeline_entry --entry "Enriched: {new_signal}" + gbrain timeline-add {date} "Enriched: {new_signal}" # Flag contradictions -- don't silently resolve them # Step 7: Cross-reference the graph - gbrain add_link # person -> company - gbrain add_link # company -> person - gbrain add_link # person -> deal + gbrain link # person -> company + gbrain link # company -> person + gbrain link # person -> deal # Every entity page links to every other entity page that references it # People page sections (not a LinkedIn profile -- a living portrait): @@ -94,8 +94,8 @@ on enrich(entity, trigger): ## How to Verify 1. Enrich a Tier 1 person. Run `gbrain get ` and confirm the page has Executive Summary, State, What They Believe, Contact, and Timeline sections populated from multiple sources. -2. Run `gbrain get_raw_data `. Confirm raw API responses are stored with `sources.{provider}.fetched_at` timestamps. -3. Run `gbrain get_links `. Confirm cross-reference links exist to the person's company page, deal pages, and related entities. +2. Run `gbrain call get_raw_data '{"slug": ""}'`. Confirm raw API responses are stored with `sources.{provider}.fetched_at` timestamps. +3. Run `gbrain call get_links '{"slug": ""}'`. Confirm cross-reference links exist to the person's company page, deal pages, and related entities. 4. Check a page that was enriched AND has a user-written Assessment. Confirm the Assessment section was preserved, not overwritten by API data. 5. Try to re-enrich the same person. Confirm the system checks the `fetched_at` timestamp and skips if less than a week old. diff --git a/docs/guides/executive-assistant.md b/docs/guides/executive-assistant.md index bc2189a45..06739f9d1 100644 --- a/docs/guides/executive-assistant.md +++ b/docs/guides/executive-assistant.md @@ -53,7 +53,7 @@ on upcoming_meeting(meeting): "last_interaction": page.timeline[0], # most recent "open_threads": page.open_threads, "relationship_temperature": page.relationship, - "relevant_deals": gbrain get_links , + "relevant_deals": gbrain call get_links '{"slug": ""}', } else: briefing[attendee] = "No brain page -- consider enriching" @@ -67,14 +67,14 @@ on inbox_cleared(): for email in processed_emails: if email.contained_new_information: # Update the sender's brain page with new signal - gbrain add_timeline_entry \ - --entry "Email re: {subject}. Key info: {extracted_signal}" \ + gbrain timeline-add {date} \ + "Email re: {subject}. Key info: {extracted_signal}" \ --source "email from {sender} re {subject}, {date}" # Update any mentioned entity pages too for entity in email.mentioned_entities: - gbrain add_timeline_entry \ - --entry "{what_was_said_about_them}" \ + gbrain timeline-add {date} \ + "{what_was_said_about_them}" \ --source "email from {sender}, {date}" # WORKFLOW 4: Scheduling Nudges diff --git a/docs/guides/meeting-ingestion.md b/docs/guides/meeting-ingestion.md index 5cd32d02f..81ad159ba 100644 --- a/docs/guides/meeting-ingestion.md +++ b/docs/guides/meeting-ingestion.md @@ -32,15 +32,15 @@ on new_meeting_transcript(meeting): # Step 3: Propagate to ALL entity pages (MANDATORY -- most agents skip this) for person in meeting.attendees + meeting.mentioned_people: - gbrain add_timeline_entry \ - --entry "Met in '{meeting.title}' on {date}. Key points: ..." \ + gbrain timeline-add {date} \ + "Met in '{meeting.title}' on {date}. Key points: ..." \ --source "Meeting notes '{meeting.title}', {date}" # Update their State section if new information surfaced # Update company pages for each person's company if relevant for company in meeting.mentioned_companies: - gbrain add_timeline_entry \ - --entry "Discussed in '{meeting.title}': {what_was_said}" \ + gbrain timeline-add {date} \ + "Discussed in '{meeting.title}': {what_was_said}" \ --source "Meeting notes '{meeting.title}', {date}" # Step 4: Extract action items @@ -49,8 +49,8 @@ on new_meeting_transcript(meeting): # Step 5: Back-link everything (bidirectional graph) for entity in all_entities_mentioned: - gbrain add_link # meeting -> entity - gbrain add_link # entity -> meeting + gbrain link # meeting -> entity + gbrain link # entity -> meeting # Step 6: Sync so new pages are immediately searchable gbrain sync @@ -73,7 +73,7 @@ on new_meeting_transcript(meeting): 1. After ingesting a meeting, run `gbrain get meetings/{date}-{slug}`. Confirm the page has the agent's analysis above the bar and the full diarized transcript below it. 2. For each attendee, run `gbrain get `. Check that their timeline has a new entry referencing the meeting with specific insights (not just "attended meeting"). 3. Pick a company mentioned in the meeting. Run `gbrain get `. Confirm a timeline entry exists referencing what was discussed about the company. -4. Run `gbrain get_links meetings/{date}-{slug}`. Verify back-links exist to all attendee and entity pages. +4. Run `gbrain call get_links '{"slug": "meetings/{date}-{slug}"}'`. Verify back-links exist to all attendee and entity pages. 5. Run `gbrain search "{meeting_topic}"`. Confirm the meeting page appears in search results (verifies sync ran). --- diff --git a/docs/guides/multi-source-brains.md b/docs/guides/multi-source-brains.md index da73fea7d..03702f758 100644 --- a/docs/guides/multi-source-brains.md +++ b/docs/guides/multi-source-brains.md @@ -91,7 +91,7 @@ first): 6. The seeded `default` source. So inside `~/.gstack/plans/` on a brain that pinned `gstack` to -`~/.gstack` via `.gbrain-source`, `gbrain put-page` implicitly writes to +`~/.gstack` via `.gbrain-source`, `gbrain put` implicitly writes to the `gstack` source. Outside any registered directory with no env/dotfile set, it writes to the default. @@ -188,10 +188,10 @@ citations keep working. ```bash # Pass --source explicitly -gbrain put-page topics/ai ... --source wiki +gbrain put topics/ai ... --source wiki # Or rely on the dotfile / env / CWD match -cd ~/.gstack && gbrain put-page plans/multi-repo ... +cd ~/.gstack && gbrain put plans/multi-repo ... # → source auto-resolves to gstack ``` diff --git a/docs/guides/operational-disciplines.md b/docs/guides/operational-disciplines.md index 75012f56a..17d08db50 100644 --- a/docs/guides/operational-disciplines.md +++ b/docs/guides/operational-disciplines.md @@ -20,8 +20,8 @@ on every_inbound_message(message): for entity in entities: existing = gbrain search "{entity.name}" if existing: - gbrain add_timeline_entry \ - --entry "{what_was_said}" \ + gbrain timeline-add {date} \ + "{what_was_said}" \ --source "User, direct message, {timestamp}" # else: flag for enrichment if important enough @@ -64,13 +64,13 @@ on nightly_schedule("02:00"): # The brain COMPOUNDS overnight. # 5a: Entity sweep -- find unlinked mentions - pages = gbrain list_pages + pages = gbrain list for page in pages: mentions = extract_entity_mentions(page.content) - existing_links = gbrain get_links + existing_links = gbrain call get_links '{"slug": ""}' for mention in mentions: if mention not in existing_links: - gbrain add_link # fix broken graph + gbrain link # fix broken graph # 5b: Citation audit -- find facts without sources for page in pages: @@ -80,7 +80,7 @@ on nightly_schedule("02:00"): # 5c: Memory consolidation -- update compiled truth from timeline for page in stale_pages(older_than="7d"): - timeline = gbrain get_timeline + timeline = gbrain timeline if timeline.has_new_entries_since_last_consolidation: # Re-synthesize compiled truth from accumulated timeline updated_truth = consolidate(page.compiled_truth, timeline.new_entries) @@ -110,11 +110,11 @@ on nightly_schedule("02:00"): ## How to Verify -1. Send a message mentioning a person with a brain page. Confirm the agent detects the entity and adds a timeline entry to their page (`gbrain get_timeline `). +1. Send a message mentioning a person with a brain page. Confirm the agent detects the entity and adds a timeline entry to their page (`gbrain timeline `). 2. Ask the agent about someone in the brain. Confirm it runs `gbrain search` or `gbrain get` BEFORE reaching for external APIs (check the tool call order). 3. Write a new page with `gbrain put`, then immediately run `gbrain search` for it. Confirm it appears in results (verifies sync ran). 4. Run `gbrain doctor`. Confirm it returns a health report with database status, page count, and any flagged issues. -5. After a dream cycle runs, check a page that had unlinked entity mentions. Confirm new links were added (`gbrain get_links `). +5. After a dream cycle runs, check a page that had unlinked entity mentions. Confirm new links were added (`gbrain call get_links '{"slug": ""}'`). --- *Part of the [GBrain Skillpack](../GBRAIN_SKILLPACK.md).* diff --git a/docs/guides/originals-folder.md b/docs/guides/originals-folder.md index 3e6838042..83044df6e 100644 --- a/docs/guides/originals-folder.md +++ b/docs/guides/originals-folder.md @@ -47,8 +47,8 @@ on user_message(message): # Step 3: Cross-link to everything that shaped the thinking for entity in idea.influences: - gbrain add_link originals/{slug} - gbrain add_link originals/{slug} + gbrain link originals/{slug} + gbrain link originals/{slug} # Step 4: Sync gbrain sync @@ -79,7 +79,7 @@ on user_message(message): 1. Generate an original idea in conversation (e.g., "I call this the 'ambition debt' problem -- every year you delay going big, the compound interest works against you"). Confirm a new page appears at `brain/originals/ambition-debt` with `gbrain get originals/ambition-debt`. 2. Check that the page uses the user's exact phrasing for the title and slug -- not a sanitized version. -3. Run `gbrain get_links originals/ambition-debt`. Confirm cross-links exist to related people, meetings, or other originals. +3. Run `gbrain call get_links '{"slug": "originals/ambition-debt"}'`. Confirm cross-links exist to related people, meetings, or other originals. 4. Express a take on someone else's idea (e.g., "I think Thiel's contrarian question is wrong because..."). Confirm it goes to `originals/` (synthesis is original), not `concepts/`. 5. Run `gbrain search "ambition debt"`. Confirm the originals page appears in search results and is discoverable. diff --git a/docs/guides/plugin-authors.md b/docs/guides/plugin-authors.md index 0bebc1218..9f803bcd0 100644 --- a/docs/guides/plugin-authors.md +++ b/docs/guides/plugin-authors.md @@ -87,7 +87,7 @@ expect it. | `version` | string | yes | Your plugin's semver. Informational. | | `plugin_version` | string | yes | Contract lock. Must equal `"gbrain-plugin-v1"` for v0.15. | | `subagents` | string | no | Subdir name (default `subagents`). Escape-attempts are rejected. | -| `description` | string | no | Shown in future `gbrain plugin list`. | +| `description` | string | no | Shown in a future plugin-listing command. | ## Subagent definition files diff --git a/docs/mcp/DEPLOY.md b/docs/mcp/DEPLOY.md index e4182d593..d7fab9013 100644 --- a/docs/mcp/DEPLOY.md +++ b/docs/mcp/DEPLOY.md @@ -250,7 +250,7 @@ All 30 GBrain operations are available remotely, including `sync_brain` and directory where `gbrain serve` was launched. Symlinks, `..` traversal, and absolute paths outside cwd are rejected. Page slugs and filenames are allowlist-validated (alphanumeric + hyphens; no control chars, RTL overrides, or backslashes). Local -CLI callers (`gbrain file upload ...`) keep unrestricted filesystem access since +CLI callers (`gbrain files upload ...`) keep unrestricted filesystem access since the user owns the machine. ## Deployment Options diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 9669938f2..daf3332b0 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -13,7 +13,7 @@ Step-by-step walkthroughs that take you from zero to a working outcome. Concrete These are the next tutorials on the roadmap. Open an issue if one of them is the one you need most; that's how we'll prioritize. -- **Set up GBrain for VC dealflow** — the operator's recipe. People pages for founders, companies with typed Facts fence carrying ARR / team-size / runway across dates, meetings auto-ingested, deal pages linking everything. Shows `gbrain whoknows`, `gbrain find_trajectory`, and `gbrain founder scorecard` on real workflows. +- **Set up GBrain for VC dealflow** — the operator's recipe. People pages for founders, companies with typed Facts fence carrying ARR / team-size / runway across dates, meetings auto-ingested, deal pages linking everything. Shows `gbrain whoknows`, `gbrain find-trajectory`, and `gbrain founder scorecard` on real workflows. - **Migrate your existing vault into GBrain** — for Notion / Obsidian / Roam users with a vault that doesn't match GBrain's default layout. Walks through `gbrain schema detect` → `suggest` → `review-candidates` so the brain learns your shape instead of forcing you to learn its. diff --git a/docs/tutorials/company-brain.md b/docs/tutorials/company-brain.md index 6ebe643e1..6f4dfcd76 100644 --- a/docs/tutorials/company-brain.md +++ b/docs/tutorials/company-brain.md @@ -554,7 +554,7 @@ What to do next: - **Wire ingestion** from external systems (Granola, Linear, Slack) using the [ingestion source contract](../skillpack-anatomy.md). Most companies want their meetings auto-ingested so the brain stays current without anyone typing notes. - **Set up team-specific dashboards** through the admin UI. Each team lead can have their own view of brain health and activity. -- **Explore the rest of the brain layer.** `gbrain whoknows` (find the expert on a topic), `gbrain find_trajectory` (how a metric changed over time), `gbrain founder scorecard` (especially useful for VC and ops teams), the contradiction-detection cycle that surfaces conflicts between different people's notes. +- **Explore the rest of the brain layer.** `gbrain whoknows` (find the expert on a topic), `gbrain find-trajectory` (how a metric changed over time), `gbrain founder scorecard` (especially useful for VC and ops teams), the contradiction-detection cycle that surfaces conflicts between different people's notes. If you're building in this space (which YC has flagged as the [company-brain category in its Request for Startups](https://www.ycombinator.com/rfs#company-brain)), you might as well build on this. Everything described above is open source, MIT licensed, and what I run in production behind my own AI agents. diff --git a/docs/tutorials/personal-brain.md b/docs/tutorials/personal-brain.md index 8aaaca957..e3a7c05f8 100644 --- a/docs/tutorials/personal-brain.md +++ b/docs/tutorials/personal-brain.md @@ -115,21 +115,21 @@ You can use the same keys across multiple agents. ## Step 6: Install GBrain -Once OpenClaw is running: +Once OpenClaw is running, installation is two commands — one in the brain repo, one in the agent workspace: ```bash -gbrain install +# In the BRAIN repo (the git repo that holds your markdown pages): +gbrain init --supabase + +# In the AGENT WORKSPACE repo (where OpenClaw runs): +gbrain skillpack scaffold --all ``` -This installs: +`gbrain init --supabase` walks a short wizard that asks for your Supabase connection string and creates the schema. You'll get that connection string in Step 7 — read 7a and 7b first so you paste the right one (the transaction pooler, not the direct connection). If you'd rather try things locally before paying for a database, `gbrain init --pglite` gives you a zero-config embedded engine instead; you can migrate to Supabase later with `gbrain migrate --to supabase`. -- About 60 skills -- About 9 skill packs -- Default brain structure -- MCP server configuration -- Supabase connection (for embeddings and search) +`gbrain skillpack scaffold --all` copies the ~43 bundled skills into your agent workspace as first-class files you can edit freely. (The old managed-install model was retired in v0.36.0.0; see `docs/INSTALL.md` if you're upgrading from an older release.) -GBrain populates the brain repo with its default directory structure, skill files, and configuration. From this point, the agent has working memory and access to every skill. +From this point, the agent has working memory and access to every skill. --- diff --git a/docs/what-schemas-unlock.md b/docs/what-schemas-unlock.md index 7f5f72de3..65637da30 100644 --- a/docs/what-schemas-unlock.md +++ b/docs/what-schemas-unlock.md @@ -32,7 +32,7 @@ gbrain schema sync --apply The sync backfills `page.type = 'meeting'` on all 4000 pages in 1000-row batches. Now: - `gbrain whoknows "Q3 roadmap discussion"` routes through the meeting type, ranking by `expert_routing` signal (attendees, recency, salience) instead of raw text. -- `gbrain extract-facts` runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`. +- The `extract_facts` cycle runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`. - The downstream `think` skill can now answer "what did we decide about pricing in the last three roadmap meetings" by querying the meeting graph instead of grep'ing 4000 files. One command. 4000 pages went from invisible to queryable. The content didn't change. The structure did. @@ -62,7 +62,7 @@ gbrain schema add-link-type led-by --page-type deal --target-type inves gbrain schema sync --apply ``` -Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." `gbrain extract-facts` starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`. +Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." The `extract_facts` cycle starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`. The CRM you've been promising yourself you'll set up next quarter? You just shipped it in 4 commands. It's downstream of your notes, not parallel to them. @@ -143,7 +143,7 @@ Re-run the same `whoknows` query. Top-3 should shift, because the new type is no Three things gbrain does that generic note systems can't: -**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. `gbrain extract-facts` only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached. +**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. The `extract_facts` cycle only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached. **2. Untyped content is invisible content.** If your meetings are typed as `note`, expert routing skips them, facts extraction ignores them, link inference doesn't fire. They exist on disk and they're indexed for text search, but the structural surfaces (whoknows, find_experts, recall, think) treat them as second-class. Adding a type isn't cosmetic; it's structural promotion. diff --git a/llms-full.txt b/llms-full.txt index 41e0ddc57..27f17f2a5 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -2346,7 +2346,7 @@ gbrain schema sync --apply The sync backfills `page.type = 'meeting'` on all 4000 pages in 1000-row batches. Now: - `gbrain whoknows "Q3 roadmap discussion"` routes through the meeting type, ranking by `expert_routing` signal (attendees, recency, salience) instead of raw text. -- `gbrain extract-facts` runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`. +- The `extract_facts` cycle runs on every meeting page automatically (because `extractable: true`), pulling typed facts like `attended_by=alice-example`, `date=2026-05-23`. - The downstream `think` skill can now answer "what did we decide about pricing in the last three roadmap meetings" by querying the meeting graph instead of grep'ing 4000 files. One command. 4000 pages went from invisible to queryable. The content didn't change. The structure did. @@ -2376,7 +2376,7 @@ gbrain schema add-link-type led-by --page-type deal --target-type inves gbrain schema sync --apply ``` -Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." `gbrain extract-facts` starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`. +Now `gbrain whoknows "Series A SaaS"` routes through `investor` and `portco` types specifically, not the noisy general type set. `gbrain graph-query alice-example --type intro-from --depth 2` walks two hops of intros to surface "Alice introduced you to Bob who introduced you to Charlie." The `extract_facts` cycle starts producing typed claims from the fence in your deal pages: `(deals/acme-seed, raise=2000000, valuation=15000000, lead=widget-vc, closed_at=2026-05-23)`. The CRM you've been promising yourself you'll set up next quarter? You just shipped it in 4 commands. It's downstream of your notes, not parallel to them. @@ -2457,7 +2457,7 @@ Re-run the same `whoknows` query. Top-3 should shift, because the new type is no Three things gbrain does that generic note systems can't: -**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. `gbrain extract-facts` only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached. +**1. The brain knows the difference between a person and an idea.** Page-type matters at query time. `gbrain whoknows` only considers `expert_routing: true` types. The `extract_facts` cycle only runs on `extractable: true` types. `gbrain graph-query` walks declared link verbs. None of that works on a flat tag system because tags don't have semantics — they're labels. Types are first-class citizens with rules attached. **2. Untyped content is invisible content.** If your meetings are typed as `note`, expert routing skips them, facts extraction ignores them, link inference doesn't fire. They exist on disk and they're indexed for text search, but the structural surfaces (whoknows, find_experts, recall, think) treat them as second-class. Adding a type isn't cosmetic; it's structural promotion. @@ -3927,7 +3927,7 @@ All 30 GBrain operations are available remotely, including `sync_brain` and directory where `gbrain serve` was launched. Symlinks, `..` traversal, and absolute paths outside cwd are rejected. Page slugs and filenames are allowlist-validated (alphanumeric + hyphens; no control chars, RTL overrides, or backslashes). Local -CLI callers (`gbrain file upload ...`) keep unrestricted filesystem access since +CLI callers (`gbrain files upload ...`) keep unrestricted filesystem access since the user owns the machine. ## Deployment Options diff --git a/skills/book-mirror/SKILL.md b/skills/book-mirror/SKILL.md index 895871a5f..8a2a5a3ad 100644 --- a/skills/book-mirror/SKILL.md +++ b/skills/book-mirror/SKILL.md @@ -248,7 +248,7 @@ before submission. After the brain page is written, render to PDF using `skills/brain-pdf`: ```bash -gbrain put_page # already done by the CLI; nothing to add here +gbrain put # already done by the CLI; nothing to add here # Then invoke brain-pdf: # (see skills/brain-pdf/SKILL.md for the make-pdf invocation) ``` diff --git a/skills/conventions/cron-via-minions.md b/skills/conventions/cron-via-minions.md index 486e62f4f..7ea303e4e 100644 --- a/skills/conventions/cron-via-minions.md +++ b/skills/conventions/cron-via-minions.md @@ -73,13 +73,13 @@ stock worker auto-loads on startup) registers handlers before `start()`. Users who set `minion_mode: off` in `~/.gbrain/preferences.json` keep using `agentTurn`. Respect that. No auto-rewrite. -## Forward note (v0.12.0) +## Forward note -GBrain v0.12.0 ships `gbrain cron`: a scheduler loop inside -`gbrain jobs work` that owns cron expressions natively — no more -handing off to host schedulers. Until v0.12.0 lands, the host -scheduler keeps firing on schedule; v0.11.1 only replaces the execution -layer (what the cron trigger *does*), not the scheduling layer. +A native scheduler loop inside `gbrain jobs work` (owning cron +expressions directly, with no host-scheduler hand-off) has been on the +roadmap since v0.11.1 but has not shipped. The host scheduler keeps +firing on schedule; this convention only replaces the execution layer +(what the cron trigger *does*), not the scheduling layer. ## Related diff --git a/skills/data-research/SKILL.md b/skills/data-research/SKILL.md index 5ca479ff1..330eac0d0 100644 --- a/skills/data-research/SKILL.md +++ b/skills/data-research/SKILL.md @@ -54,8 +54,8 @@ Ask the user what they want to track. Either: - Define a custom recipe with: source queries, classification rules, extraction schema, tracker page path, tracker format -Recipes are YAML files at `~/.gbrain/recipes/{name}.yaml`. Use `gbrain research init` -to scaffold a new one. +Recipes are YAML files at `~/.gbrain/recipes/{name}.yaml`. Scaffold a new one by +copying a built-in recipe file and editing its fields. ### Phase 2: Search Sources diff --git a/skills/eiirp/SKILL.md b/skills/eiirp/SKILL.md index 370470299..6b5eced99 100644 --- a/skills/eiirp/SKILL.md +++ b/skills/eiirp/SKILL.md @@ -201,7 +201,7 @@ Use the brain page template. MUST include: ### 4b. Entity pages (people, companies) For each entity mentioned: -- Check if a brain page exists (`gbrain search ""` or `gbrain get_page people/`). +- Check if a brain page exists (`gbrain search ""` or `gbrain get people/`). - If exists: update State, append Timeline entry citing this research. - If not: create with enrichment. diff --git a/skills/perplexity-research/SKILL.md b/skills/perplexity-research/SKILL.md index 8e36056ec..d63af6c30 100644 --- a/skills/perplexity-research/SKILL.md +++ b/skills/perplexity-research/SKILL.md @@ -112,7 +112,7 @@ gbrain query "" # -d '{"model": "sonar-pro", "messages": [{"role":"user","content":"..."}]}' # 4. Write the structured research page via put_page: -gbrain put_page research/ # via the put_page operation +gbrain put research/ # via the put_page operation # 5. Cross-link entities mentioned (people, companies) per Iron Law. ``` diff --git a/skills/schema-unify/SKILL.md b/skills/schema-unify/SKILL.md index 66246a468..ac9140457 100644 --- a/skills/schema-unify/SKILL.md +++ b/skills/schema-unify/SKILL.md @@ -11,7 +11,7 @@ tools: - gbrain schema active - gbrain schema use - gbrain schema stats - - gbrain pages restore + - gbrain restore - mcp:run_onboard triggers: - "unify my types" @@ -143,7 +143,7 @@ WHERE source_id = 'default' AND frontmatter->>'legacy_type' IS NOT NULL; Page-to-alias and page-to-link source pages soft-delete with 72h TTL. Restore within that window: ```bash -gbrain pages restore +gbrain restore ``` Revert the active pack flip: @@ -197,7 +197,7 @@ Outputs: - Active pack flipped to `gbrain-base-v2` atomically at end of successful run. Side effects: -- Source pages soft-deleted with 72h restore TTL (`gbrain pages restore `). +- Source pages soft-deleted with 72h restore TTL (`gbrain restore `). - One-time cache invalidation on KNOBS_HASH_VERSION bump (5→6); self-healing in `cache.ttl_seconds`. - Query-time `--type X` alias-expands via `expandTypeFilter` (D14 back-compat). @@ -212,7 +212,7 @@ DON'T: - Submit `unify-types` directly via the MCP `submit_job` op without `--allow-protected`. PROTECTED handlers require trusted local callers; remote MCP rejection is the intentional trust boundary. - Edit `mapping_rules` in `gbrain-base-v2.yaml` to skip clusters you don't trust. Fork the pack instead (`gbrain schema fork`) so the source-of-truth migration stays consistent across brains. - Run `unify-types` from inside an autopilot tick. The check is `manual_only` per D17 — autopilot deliberately never auto-fires it because pack upgrades are one-time consenting taxonomy decisions. -- Hard-delete soft-deleted source pages before the 72h restore window. Use `gbrain pages restore ` first if rollback is needed. +- Hard-delete soft-deleted source pages before the 72h restore window. Use `gbrain restore ` first if rollback is needed. - Assume `frontmatter.legacy_type` survives every roundtrip. The marker is canonical for the immediate post-migration window; downstream re-imports may overwrite it. ## Output Format diff --git a/skills/voice-note-ingest/SKILL.md b/skills/voice-note-ingest/SKILL.md index c4b4c6558..ebdde81b7 100644 --- a/skills/voice-note-ingest/SKILL.md +++ b/skills/voice-note-ingest/SKILL.md @@ -43,8 +43,9 @@ The Analysis section can interpret; the transcript section is sacred. The user sends an audio or voice message via any channel (Telegram, voice memo upload, openclaw audio attachment). The host agent typically provides -the transcript text. If not, transcribe via `gbrain transcription` (Groq -Whisper by default; OpenAI fallback for audio > 25MB segmented via ffmpeg). +the transcript text. If not, transcribe it with your host's transcription +tool (Groq Whisper is fast and cheap; OpenAI Whisper works too — segment +audio > 25MB via ffmpeg first). ## The pipeline @@ -52,8 +53,9 @@ Whisper by default; OpenAI fallback for audio > 25MB segmented via ffmpeg). 1. STORE → Upload original audio to gbrain storage backend (S3 / Supabase Storage / local — pluggable per src/core/storage.ts). -2. TRANSCRIBE → Use the agent-provided transcript verbatim, OR call - gbrain transcription if no transcript was supplied. +2. TRANSCRIBE → Use the agent-provided transcript verbatim, OR + transcribe the audio yourself (see "When to invoke") + if no transcript was supplied. 3. ROUTE → Apply the decision tree (below) to find the right destination directory. 4. WRITE → Create / update the destination brain page; preserve the diff --git a/src/cli.ts b/src/cli.ts index 502cb0d05..a5dfd5e1e 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -55,12 +55,17 @@ export function bigintToStringReplacer(_key: string, value: unknown): unknown { } // CLI-only commands that bypass the operation layer -export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'advisor', 'watch', 'reindex-search-vector', 'backfill']); +export const CLI_ONLY = new Set(['init', 'reinit-pglite', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'extract-conversation-facts', 'enrich', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'skillpack', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'maintain', 'sources', 'mounts', 'dream', 'check-resolvable', 'routing-eval', 'skillify', 'smoke-test', 'providers', 'storage', 'repos', 'code-def', 'code-refs', 'reindex', 'reindex-code', 'reindex-frontmatter', 'code-callers', 'code-callees', 'reconcile-links', 'frontmatter', 'auth', 'friction', 'claw-test', 'book-mirror', 'takes', 'think', 'salience', 'anomalies', 'calibration', 'transcripts', 'models', 'remote', 'recall', 'forget', 'edges-backfill', 'cache', 'ze-switch', 'retrieval-upgrade', 'founder', 'brainstorm', 'lsd', 'schema', 'capture', 'onboard', 'conversation-parser', 'status', 'connect', 'skillopt', 'quarantine', 'self-upgrade', 'advisor', 'watch', 'reindex-search-vector', 'pages', 'bench', 'backfill']); // CLI-only commands whose handlers print their own --help text. These are // excluded from the generic short-circuit so detailed per-command and // per-subcommand usage stays reachable. const CLI_ONLY_SELF_HELP = new Set([ 'upgrade', 'post-upgrade', 'check-update', + // #3502 sweep: pages + bench print their own usage (pages.ts printHelp, + // bench-publish.ts printHelp). Both were documented but undispatchable — + // `pages` had a live handleCliOnly case but was missing from CLI_ONLY + // (the #2035 calibration bug class); `bench` was never wired at all. + 'pages', 'bench', 'embed', 'config', 'skillpack', 'skillpack-check', 'integrations', 'friction', @@ -1270,6 +1275,20 @@ async function handleCliOnly(command: string, args: string[]) { await runInit(args); return; } + if (command === 'bench') { + // #3502 sweep: `gbrain bench publish` was documented (docs/eval-bench.md, + // KEY_FILES.md, and eval-gate's own --help text) but never dispatched — + // the promised-but-unwired class retrieval-upgrade (#3390) fixed before. + // Pure file-in/file-out (NDJSON → baseline); no DB, no engine. + if (args[0] === 'publish') { + const { runBenchPublish } = await import('./commands/bench-publish.ts'); + await runBenchPublish(args.slice(1)); + return; + } + console.error('Usage: gbrain bench publish --from --to [flags]'); + console.error('Run `gbrain bench publish --help` for the full flag list.'); + process.exit(args[0] === '--help' || args[0] === '-h' ? 0 : 2); + } // v0.37 fix wave (deferred TODO, shipped): one-command wipe-and-reinit. // Spawns its own engine internally so no pre-bound engine needed. if (command === 'reinit-pglite') { diff --git a/test/docs-cli-commands.test.ts b/test/docs-cli-commands.test.ts new file mode 100644 index 000000000..7f6028c80 --- /dev/null +++ b/test/docs-cli-commands.test.ts @@ -0,0 +1,138 @@ +/** + * #3502: docs must not reference nonexistent gbrain commands. + * + * `docs/tutorials/personal-brain.md` shipped a `gbrain install` step for two + * months after the command it replaced was retired — every reader hit + * "Unknown command: install". This guard scans README.md, docs/, and skills/ + * for `gbrain ` invocations in code (fenced blocks + inline code spans) + * and checks each verb against the live CLI surface: CLI_ONLY, operation + * cliHints names (non-hidden), and aliases. + * + * Deliberately excluded (historical or speculative by design, per CLAUDE.md's + * "historical docs are never rewritten" rule): + * - docs/GBRAIN_V0.md — the v0 spec; documents v0's CLI + * - docs/designs/, docs/plans/ — future/speculative design docs + * - docs/migrations/, skills/migrations/ — per-release migration notes, + * written against that release's CLI + * - docs/UPGRADING_DOWNSTREAM_AGENTS.md — per-release upgrade chronicle + * + * Heuristics keep prose out: only fenced code + inline spans are scanned, + * comment lines and diagram lines are skipped, and the verb must sit in + * command position (start of command text, or after a shell operator). + */ +import { describe, expect, test } from 'bun:test'; +import { readdirSync, readFileSync, statSync } from 'fs'; +import { dirname, join, relative } from 'path'; +import { CLI_ONLY, cliAliases } from '../src/cli.ts'; +import { operations } from '../src/core/operations.ts'; + +const ROOT = dirname(import.meta.dir); + +const EXCLUDED = [ + 'docs/GBRAIN_V0.md', + 'docs/UPGRADING_DOWNSTREAM_AGENTS.md', + 'docs/designs/', + 'docs/plans/', + 'docs/migrations/', + 'skills/migrations/', +]; + +/** Known-intentional references to commands that deliberately don't exist. */ +const ALLOWLIST: Record = { + // The doc explains that gbrain does NOT ship this command, on purpose. + 'docs/guides/rls-and-you.md': ['rls-exempt'], +}; + +function validCommands(): Set { + const valid = new Set(CLI_ONLY); + for (const op of operations) { + const name = op.cliHints?.name; + if (name && !op.cliHints?.hidden) valid.add(name); + } + for (const alias of cliAliases.keys()) valid.add(alias); + return valid; +} + +function* mdFiles(dir: string): Generator { + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + if (statSync(p).isDirectory()) yield* mdFiles(p); + else if (p.endsWith('.md')) yield p; + } +} + +interface CodeLine { code: string; line: number } + +/** Fenced-block lines + inline code spans that START with `gbrain `. */ +function codeRegions(text: string): CodeLine[] { + const out: CodeLine[] = []; + const lines = text.split('\n'); + let inFence = false; + for (let i = 0; i < lines.length; i++) { + const l = lines[i]; + if (/^\s*(```|~~~)/.test(l)) { inFence = !inFence; continue; } + if (inFence) { + const t = l.trim(); + if (/^(#|\/\/|--|\*)/.test(t)) continue; // comment lines + if (/[│┌┐└┘├┤─═╔╗╚╝]/.test(l)) continue; // ASCII-art diagrams + out.push({ code: l, line: i + 1 }); + continue; + } + for (const m of l.matchAll(/`(gbrain [^`]+)`/g)) out.push({ code: m[1], line: i + 1 }); + } + return out; +} + +/** True when `gbrain` sits at command position (not mid-prose). */ +function commandPosition(prefix: string): boolean { + const p = prefix.trimEnd(); + return p === '' || /[|;&`(={[]$/.test(p) || /\$$/.test(p); +} + +function scan(): string[] { + const valid = validCommands(); + const violations: string[] = []; + const files = [ + join(ROOT, 'README.md'), + ...mdFiles(join(ROOT, 'docs')), + ...mdFiles(join(ROOT, 'skills')), + ]; + for (const file of files) { + const rel = relative(ROOT, file); + if (EXCLUDED.some((e) => rel === e || rel.startsWith(e))) continue; + const text = readFileSync(file, 'utf-8'); + for (const { code, line } of codeRegions(text)) { + for (const m of code.matchAll(/\bgbrain\s+([A-Za-z][\w-]*)/g)) { + const verb = m[1]; + if (!/^[a-z][a-z0-9_-]{2,}$/.test(verb)) continue; // flags, , v0.x + if (!commandPosition(code.slice(0, m.index))) continue; + if (valid.has(verb)) continue; + if (ALLOWLIST[rel]?.includes(verb)) continue; + violations.push(`${rel}:${line}: \`gbrain ${verb}\` is not a real command — ${code.trim().slice(0, 90)}`); + } + } + } + return violations; +} + +describe('#3502 — docs reference only real gbrain commands', () => { + test('every `gbrain ` in README/docs/skills resolves to a live command', () => { + const violations = scan(); + expect(violations).toEqual([]); + }); + + test('the sanity anchors: install is dead, init/put/skillpack are live', () => { + const valid = validCommands(); + expect(valid.has('install')).toBe(false); // retired v0.36.0.0 — the #3502 bug + expect(valid.has('init')).toBe(true); + expect(valid.has('put')).toBe(true); + expect(valid.has('skillpack')).toBe(true); + }); + + test('pages + bench are dispatchable (documented surfaces; #2035 bug class)', () => { + // `pages` had a live handleCliOnly case but was dropped from CLI_ONLY; + // `bench` (bench-publish.ts) was documented but never wired at all. + expect(CLI_ONLY.has('pages')).toBe(true); + expect(CLI_ONLY.has('bench')).toBe(true); + }); +});