feat: v0.17.0 — gbrain dream + runCycle primitive (one cycle, two CLIs)#321
Merged
feat: v0.17.0 — gbrain dream + runCycle primitive (one cycle, two CLIs)#321
Conversation
Precondition for v0.17 brain maintenance cycle (runCycle primitive).
The full-sync path (performFullSync) previously called runImport() even
when opts.dryRun was true, silently writing to the DB and advancing
sync.last_commit. `gbrain sync --dry-run` on a fresh brain (or with
--full) would mutate state without warning.
Fix:
- performFullSync now early-returns a `dry_run` SyncResult when
opts.dryRun is set. Walks the repo via collectMarkdownFiles +
isSyncable to count what WOULD be imported. No writes, no git
state advance.
- SyncResult gains an `embedded: number` field (required). Tracks
pages re-embedded during the sync's auto-embed step. Existing
return sites set 0; the synced + first_sync paths set real counts
(best-estimate until commit 2 sharpens runEmbedCore's return type).
- first_sync path now returns real added + chunksCreated counts
from runImport instead of hardcoded zeros.
- printSyncResult shows embedded count in human output.
Tests (test/sync.test.ts, new `performSync dry-run never writes`
block, PGLite + temp git repo, no DATABASE_URL required):
- first-sync --dry-run: no pages, no sync.last_commit
- incremental --dry-run after real sync: bookmark unchanged
- --full --dry-run: no reimport, bookmark unchanged
- SyncResult.embedded is a number
Codex outside-voice caught this. Would have shipped silent DB writes
on dry-run for anyone using `gbrain sync --dry-run --full`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Precondition for v0.17 brain maintenance cycle (runCycle primitive).
runEmbedCore previously returned Promise<void> and had no dry-run mode.
That made it impossible for runCycle to (a) report accurate embedded
counts or (b) honor --dry-run without also skipping the entire embed
phase (which would have required runCycle to know embed's internal
semantics — a layering violation).
Changes:
- EmbedOpts gains `dryRun?: boolean`. When set, embedPage and
embedAll enumerate stale chunks (or would-be-created chunks for
unchunked pages, via local chunkText without engine.upsertChunks)
but never call embedBatch and never write to the engine.
- runEmbedCore: Promise<void> -> Promise<EmbedResult>. Result shape:
{ embedded, skipped, would_embed, total_chunks, pages_processed,
dryRun }.
embedded = chunks newly embedded (0 in dryRun).
would_embed = chunks that WOULD be embedded (0 in non-dryRun).
skipped = chunks with pre-existing embeddings.
- runEmbed CLI wrapper honors --dry-run flag and returns the result
through. `gbrain embed --stale --dry-run` is now a safe preview.
- Callers ignoring the return value (sync auto-embed, autopilot
inline fallback, jobs.ts handlers, CLI) keep compiling — the new
return type is additive for `await` callers.
Tests (test/embed.test.ts, new `runEmbedCore --dry-run` block, uses
the existing mock.module embedBatch pattern, no API key required):
- dry-run --all: zero embedBatch calls, zero upsertChunks calls,
would_embed matches stale chunk total
- dry-run --stale correctly splits stale vs already-embedded counts
- dry-run --slugs on a single page tallies per-chunk counts
- non-dry-run regression guard: embedded count matches across
concurrent workers
Codex outside-voice flagged the Promise<void> return as a blocker for
accurate CycleReport.totals.pages_embedded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lobal
Precondition for v0.17 brain maintenance cycle (runCycle primitive).
findOrphans + queryOrphanPages previously reached into the postgres-js
singleton via db.getConnection(), which (a) didn't compose with
runCycle's explicit-engine contract and (b) was wrong for PGLite test
fixtures and for any caller not using the default global connection.
Codex outside-voice flagged this as a blocker.
Changes:
- BrainEngine interface gains findOrphanPages() — returns pages with
no inbound links via the same NOT EXISTS anti-join. Implemented on
both postgres-engine (sql tag) and pglite-engine (db.query).
- findOrphans signature: findOrphans(engine, { includePseudo }).
Engine is required. Uses engine.findOrphanPages() and
engine.getStats().page_count instead of raw SQL + global counts.
- queryOrphanPages signature: queryOrphanPages(engine). Delegates to
engine.findOrphanPages().
- src/commands/orphans.ts drops the `import * as db` — no more
global-state coupling.
- Callers updated: src/core/operations.ts find_orphans handler now
passes ctx.engine through; runOrphans CLI entry uses its engine arg.
- No signature change needed in cli.ts (it was already passing engine
via CLI_ONLY dispatch).
Tests (test/orphans.test.ts, new `findOrphans (engine-injected)`
describe block, PGLite in-memory, no DATABASE_URL required):
- links correctly scope orphans (alice links to bob -> bob not
an orphan; alice is)
- includePseudo:true surfaces _atlas-style pages
- queryOrphanPages delegates to passed engine
- empty brain returns {orphans: [], total_pages: 0} without crashing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The brain maintenance cycle as a single function. Six phases in
semantically-driven order (fix files → sync → extract → embed →
report orphans). Pure composition of existing library calls — no
execSync, no subprocess anti-patterns, no regex-parsed output.
┌───────────────────────────────────────────────────┐
│ runCycle(engine, opts) → CycleReport │
│ Phase 1: lint --fix (fs writes) │
│ Phase 2: backlinks --fix (fs writes) │
│ Phase 3: sync (DB picks up 1+2) │
│ Phase 4: extract (DB picks up links) │
│ Phase 5: embed --stale (DB writes) │
│ Phase 6: orphans (DB read, report) │
└───────────────────────────────────────────────────┘
Why the commit-4 primitive:
- CEO + Eng + Codex reviews all converged on "extract one cycle
function, wire both dream and autopilot through it." Two CLIs,
one definition of what the brain does overnight.
- Phase order was wrong in PR #309's original dream.ts (sync
before lint+backlinks lost the "fix files, then index them"
semantic).
- This commit is the bisectable foundation; commit 5 (dream)
and commit 6 (autopilot+jobs) just call into it.
Coordination — the codex-flagged blocker:
Session-scoped pg_try_advisory_lock does not survive PgBouncer
transaction pooling (the v0.15.4 fix made pooled connections the
default). Replaced with a DB lock table (gbrain_cycle_locks) that
works through every pooler:
- Acquire: INSERT ... ON CONFLICT DO UPDATE ... WHERE ttl < NOW()
- Refresh: UPDATE ttl_expires_at between phases via hook
- Release: DELETE in finally{}
- TTL: 30 min; crashed holders auto-release
PGLite / engine=null path uses a file lock at ~/.gbrain/cycle.lock
with PID liveness check. kill(pid, 0) with EPERM treated as alive
(so init/launchd-pid holders aren't mis-classified as stale).
Lock-skip: only phases that mutate state (lint, backlinks, sync,
extract, embed) trigger lock acquisition. orphans is read-only.
Single-phase --phase orphans runs never block on a held lock.
Engine-null mode preserved: filesystem phases run, DB phases skip
with {status:'skipped', reason:'no_database'}. Matches current
dream's capability that would have been lost if runCycle required
a connected engine.
Contract details:
- CycleReport has schema_version:"1" (stable, additive) so agents
consuming --json can rely on the shape
- status: 'ok' | 'clean' | 'partial' | 'skipped' | 'failed'.
'clean' = ran successfully with zero activity; agents trivially
detect a healthy brain.
- PhaseResult.error: { class, code, message, hint?, docs_url? }
(Stripe-API-tier structured failure info) when status='fail'
- yieldBetweenPhases hook: awaited between EVERY phase and before
return, runs even after phase failure, exceptions logged but
non-fatal. Required so the Minions autopilot-cycle handler can
renew its job lock between phases (prevents the v0.14 stall-death
regression codex flagged).
- git pull explicit: opts.pull defaults to false (cron-safe).
Autopilot daemon callers opt in if user configured it.
- extract phase doesn't have a dry-run mode in the underlying
library function, so runCycle honestly skips extract when
dryRun=true (status:'skipped', reason:'no_dry_run_support').
Schema migration v16: gbrain_cycle_locks table + idx_cycle_locks_ttl.
Also appended to src/schema.sql and src/core/pglite-schema.ts for
fresh installs. schema-embedded.ts regenerated via build:schema.
Tests (test/core/cycle.test.ts, PGLite in-memory + mocked library
functions, no DATABASE_URL required):
- dryRun × phases matrix: dryRun:true reaches lint/backlinks/sync/
embed; extract is honestly skipped
- Phase selection: default runs all 6 in order; --phase lint runs
only lint; --phase orphans runs only orphans
- Lock semantics: acquire + release on mutating phases, skip
entirely for read-only selections
- cycle_already_running: seeded live-holder lock → status:skipped,
zero phase runs; TTL-expired holder → auto-claimed
- Engine null: filesystem phases run, DB phases skip
- File lock (engine=null) blocks when PID 1 holds lock with fresh
mtime — exercises the PID liveness branch including EPERM
- Status derivation: 'ok' vs 'clean' vs 'partial' vs 'skipped'
- yieldBetweenPhases called N times, hook exceptions non-fatal
Next: commit 5 rewrites dream.ts as a thin CLI alias over runCycle,
commit 6 migrates autopilot daemon + jobs.ts handler to delegate to
runCycle too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`gbrain dream` is the README brand-promise command: "the agent runs
while I sleep, the dream cycle ... I wake up and the brain is smarter."
Cron-friendly, JSON-reportable, phase-selectable. Same maintenance
cycle as `gbrain autopilot`, just scheduled differently — both
converge on runCycle (added in commit 4) so there's one source of
truth for what happens overnight.
Contract:
gbrain dream # full 6-phase cycle
gbrain dream --dry-run # preview, no writes
gbrain dream --json # CycleReport JSON (agent-readable)
gbrain dream --phase <name> # single-phase run
gbrain dream --pull # git pull before syncing
gbrain dream --dir /path/to/brain # explicit brain location
Cron: 0 2 * * * gbrain dream --json >> /var/log/gbrain-dream.log
Behavior details:
- Brain-dir resolution: requires explicit --dir OR sync.repo_path
in engine config. No more walk-up-cwd-for-.git footgun that
PR #309's original dream.ts had (would lint unrelated git repos).
- engine=null mode preserved via cli.ts's try/catch around
connectEngine — filesystem phases (lint, backlinks) still run
without a DB, DB phases report skipped/no_database in the output.
- status=clean prints "Brain is healthy. N phase(s) checked in Ns."
status=skipped prints the reason (cycle_already_running, etc.).
Partial/failed prints the phase-by-phase detail.
- Exit code 1 when status=failed (cron spots real problems).
'partial' is not a failure — warnings shouldn't page you.
- --help text cross-references `autopilot --install` for users
who want continuous maintenance as a daemon.
CLI registration (src/cli.ts):
- 'dream' added to CLI_ONLY
- handleCliOnly has a pre-engine branch mirroring doctor's pattern:
try connectEngine() → ok path; catch → runDream(null, args) so
filesystem phases still run when DB is down
- Help text updated with one-line dream entry and autopilot cross-ref
Tests (test/dream.test.ts, real PGLite + real library calls, no mocks
to avoid `mock.module` leakage across test files):
- brainDir resolution: explicit --dir wins, engine config fallback,
missing + nonexistent errors
- phase selection: --phase lint|orphans produces single-phase report
- phase validation: --phase garbage exits 1
- output: --json parses as CycleReport with schema_version:"1"
- human output mentions "Brain is healthy" on clean status
- dry-run: cycle runs but DB stays untouched
- exit code: clean/ok/partial do not call process.exit
Also (test/core/cycle.test.ts): refactored to use beforeAll/afterAll
with one shared PGLite engine per describe + truncateCycleLocks
between tests. Cuts test time from ~11s to ~4s; avoids the 15-migration
penalty per test that was causing parallel-suite timeout flakes.
Co-Authored-By: Wintermute <wintermute@garrytan.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cle)
Autopilot daemon (`--inline` path) and Minions `autopilot-cycle`
handler both now delegate to `runCycle` (introduced in commit 4).
Three callers, one cycle definition:
1. `gbrain dream` — one-shot cron cycle
2. `gbrain autopilot` daemon inline path — scheduled cycles
3. `autopilot-cycle` Minions handler — durable queue with retry
All three share:
- Same 6 phases in same order (lint → backlinks → sync → extract →
embed → orphans)
- Same DB lock table coordination (`gbrain_cycle_locks`)
- Same yieldBetweenPhases discipline (prevents v0.14 stall-death)
- Same structured CycleReport output
Autopilot inline path gains lint + orphan sweep that the old path
skipped. Minions autopilot-cycle handler also gains lint + orphans.
Users who run `gbrain autopilot --install` see 6-phase reports in
`gbrain jobs get <id>` starting on next interval. No config change
required.
Changes:
- `src/commands/autopilot.ts`: inline fallback path (~20 lines)
replaces the ~22-line sync+extract+embed sequence with a single
runCycle call. Uses pull:true (matches pre-v0.17 autopilot
behavior). Uses setImmediate yield hook. Status/failure reporting
derives from CycleReport.status. `--help` cross-references `gbrain
dream` for one-shot use.
- `src/commands/jobs.ts:579` (`autopilot-cycle` handler): replaces
the 4-step try/catch sequence with a runCycle call. Returns
`{ partial, status, report }` so `gbrain jobs get <id>` shows the
full structured CycleReport. Preserves partial-failure semantic
(one phase failing does NOT throw; next cycle still runs).
yieldBetweenPhases yields the event loop between phases for the
worker's lock-renewal timer.
Release scaffolding:
- VERSION: 0.16.0 → 0.17.0
- CHANGELOG.md: v0.17.0 entry in GStack voice — headline, numbers
table, "what this means" paragraph, "To take advantage" block
per CLAUDE.md post-ship rules. Itemized changes below the fold.
Credit to @WinterMute for the original PR #309 thesis.
- skills/migrations/v0.17.0.md: documents what changed for
upgrading users. No mechanical action required — schema migration
v16 (cycle locks table) + handler delegation both apply
automatically. Includes opt-out paths for users who don't want
their daemon modifying files (use `dream --phase orphans` in cron
and skip autopilot-install, or other explicit configs).
- CLAUDE.md: new entries for `src/core/cycle.ts` and
`src/commands/dream.ts` with contract details.
Tests: no new test file needed for this commit — the cycle primitive
is extensively tested in test/core/cycle.test.ts (18 cases), dream
in test/dream.test.ts (11), and autopilot's delegation is mechanical
(calls runCycle with specific opts). The handler contract is covered
implicitly: if runCycle returns a CycleReport, the handler wraps it
in `{ partial, status, report }` — nothing else to assert.
Verified:
- `bun test test/autopilot-install.test.ts test/autopilot-resolve-cli.test.ts test/core/cycle.test.ts test/dream.test.ts` → 37 pass, 0 fail
Completes the v0.17.0 feature: 6 bisectable commits on one branch
(garrytan/v0.17-dream-cycle), ready to push as one PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
807ef6a to
4b3de31
Compare
…m-cycle # Conflicts: # CHANGELOG.md # VERSION
…m-cycle # Conflicts: # CHANGELOG.md # VERSION # src/commands/orphans.ts
garrytan
added a commit
that referenced
this pull request
Apr 22, 2026
Gap from the v0.17 commit series: PR #321 shipped unit-level tests for runCycle (test/core/cycle.test.ts) and dream (test/dream.test.ts) but no E2E coverage that exercises the real Postgres paths. Filling that in before merge. test/e2e/cycle.test.ts (6 cases): - schema migration v16 created gbrain_cycle_locks + index - dry-run full cycle: zero DB writes + lock table empty after - live cycle: pages + chunks materialize, sync.last_commit set - concurrent cycle blocked by lock → status:'skipped' - TTL-expired lock auto-claimed (crashed-holder recovery) - --phase orphans skips lock entirely (read-only optimization) test/e2e/dream.test.ts (3 cases): - dream --dry-run --json emits valid CycleReport + DB stays empty - dream (no --dry-run) syncs pages into real DB - dream --phase orphans doesn't touch the cycle-lock table Both files mock embedBatch via mock.module so the embed phase never calls OpenAI even when the full 6-phase cycle runs (zero API cost, zero flakiness from network calls). Verified locally: - `docker run pgvector/pgvector:pg16` on port 5434 - `DATABASE_URL=... bun test test/e2e/cycle.test.ts test/e2e/dream.test.ts` → 9 pass, 0 fail - Full E2E suite (`bun run test:e2e`): 16 files, 150 tests, 0 fail - Container torn down after: `docker stop + rm gbrain-test-pg` Per CLAUDE.md E2E test DB lifecycle. These tests skip gracefully when DATABASE_URL isn't set (via hasDatabase() helper + describe.skip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gap from the v0.17 commit series: PR #321 shipped unit-level tests for runCycle (test/core/cycle.test.ts) and dream (test/dream.test.ts) but no E2E coverage that exercises the real Postgres paths. Filling that in before merge. test/e2e/cycle.test.ts (6 cases): - schema migration v16 created gbrain_cycle_locks + index - dry-run full cycle: zero DB writes + lock table empty after - live cycle: pages + chunks materialize, sync.last_commit set - concurrent cycle blocked by lock → status:'skipped' - TTL-expired lock auto-claimed (crashed-holder recovery) - --phase orphans skips lock entirely (read-only optimization) test/e2e/dream.test.ts (3 cases): - dream --dry-run --json emits valid CycleReport + DB stays empty - dream (no --dry-run) syncs pages into real DB - dream --phase orphans doesn't touch the cycle-lock table Both files mock embedBatch via mock.module so the embed phase never calls OpenAI even when the full 6-phase cycle runs (zero API cost, zero flakiness from network calls). Verified locally: - `docker run pgvector/pgvector:pg16` on port 5434 - `DATABASE_URL=... bun test test/e2e/cycle.test.ts test/e2e/dream.test.ts` → 9 pass, 0 fail - Full E2E suite (`bun run test:e2e`): 16 files, 150 tests, 0 fail - Container torn down after: `docker stop + rm gbrain-test-pg` Per CLAUDE.md E2E test DB lifecycle. These tests skip gracefully when DATABASE_URL isn't set (via hasDatabase() helper + describe.skip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14d4a97 to
173d9f3
Compare
…m-cycle # Conflicts: # CHANGELOG.md # VERSION # src/cli.ts
garrytan
added a commit
that referenced
this pull request
Apr 22, 2026
…as v0.17)
Master shipped its own v0.17.0 ('gbrain dream' + runCycle primitive, PR #321)
while this branch was in flight. Rebasing this branch to v0.18.0 and
renumbering our schema migrations to avoid collision with master's v16
(cycle_locks_table).
Version collisions resolved:
- VERSION: 0.17.0 → 0.18.0
- package.json version: synced to 0.18.0
- Schema migrations renumbered: my v16→v20 (sources_table_additive),
v17→v21 (pages_source_id_composite_unique), v18→v22 (links_resolution_type),
v19→v23 (files_source_id_page_id_ledger). Master's v15/v16 stay put.
- Orchestrator: src/commands/migrations/v0_17_0*.ts → v0_18_0*.ts +
registry entry renamed in index.ts
- Migration skill: skills/migrations/v0.17.0.md now contains master's
gbrain-dream content; my multi-source migration skill is now at
skills/migrations/v0.18.0.md
Code conflicts resolved:
- src/core/postgres-engine.ts putPage: kept my ON CONFLICT (source_id, slug)
AND master's sql.json type cast — both are required
- src/cli.ts CLI_ONLY: merged both additions ('sources' + 'dream' +
'check-resolvable')
- skills/migrations/v0.17.0.md add/add conflict resolved by renaming mine
Documentation:
- Updated v0.17.0 references in all source comments + tests + docs
to v0.18.0 (~30 files)
- Updated test/apply-migrations.test.ts skippedFuture lists to include
0.18.0
Test results: 2063 pass / 17 fail. The 17 fails are all pre-existing
PGLite beforeEach timeouts in runDream + findOrphans tests added by
master (same count on pure master baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 23, 2026
Master shipped two releases while the skillify workstream was in review: - v0.17.0: gbrain dream + runCycle primitive (#321) - v0.18.0: multi-source brains (#337) My skillify work (originally labeled v0.17.0) moves to v0.19.0 to avoid the collision. It's the natural continuation of PR #326 (v0.16.4's skills-dir fallback) and the essay's 10-step skillify loop. ## Conflicts resolved **src/cli.ts** — union of CLI_ONLY sets. Master added `sources`, `dream`; my branch added `skillpack`, `routing-eval`, `skillify`. All six remain. **CHANGELOG.md** — three nested conflict hunks caused by the shared `## [0.17.0]` + `## To take advantage of v0.17.0` headings. Resolved by renaming my entry to v0.19.0, reordering so the file reads v0.19.0 → v0.18.0 → v0.17.0 → v0.16.4, and keeping master's gbrain dream entry intact under v0.17.0. **skills/brain-ops/SKILL.md** — both branches added frontmatter fields. Master's v0.18.0 added a "Cross-source citation format" section; my W3 added `writes_pages: true` + `writes_to:` frontmatter. Git auto-merged cleanly; verified both sets survive. ## Version bumps VERSION: 0.17.0 → 0.19.0 package.json: 0.17.0 → 0.19.0 openclaw.plugin.json: 0.17.0 → 0.19.0 ## Other mechanical updates in the merge - Privacy scan stays clean after merge (scripts/check-privacy.sh exit 0). Scrubbed one `@Wintermute` reference that master added in the v0.17.0 gbrain dream credit line → "@knee5" placeholder (keeps the credit, drops the private fork name per CLAUDE.md:550). ## Test status **Isolated runs — green across v0.19 surface:** - 207/207 pass across 12 v0.19-specific test files - Privacy guard exit 0 **Full suite — 17 fail / 3 errors, all pre-existing on master.** Verified by running origin/master (90c5d93) in a clean worktree: identical 17 fail / 3 errors profile. My merge did not introduce any new failures. The failing tests (dream.test.ts + brain-allowlist.test.ts engine.disconnect timeouts; orphans.test.ts concurrency) pass in isolation (46/46 for dream+orphans) but flake under the full concurrent suite's shared-resource contention. That's master's pre-existing condition; filing a separate issue is out of scope for this merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
garrytan
added a commit
that referenced
this pull request
Apr 23, 2026
Pulls upstream v0.16.1–v0.18.1: minions worker deploy guide (#287/#317), subagent Anthropic SDK fix + tsc CI gate (#318), check-resolvable CLI (#325), dream + runCycle primitive (#321), multi-source brains with federation + dotfile resolution (#337), RLS hardening + schema backfill (#343). Test count grows 2000 → 2354. Conflicts resolved: - VERSION — kept 0.19.0; upstream is 0.18.1 - package.json — v0.19.0 wins - CHANGELOG.md — v0.19.0 preserved above upstream's v0.18.1/v0.18.0/v0.17.0/v0.16.x - src/cli.ts — CLI_ONLY merges `agent`, `providers`, and upstream's new `sources`, `dream`, `check-resolvable` - src/core/config.ts — merged: kept embedding_model / embedding_dimensions / expansion_model / provider_base_urls (mine) + storage (upstream) Build clean: 948 modules, ~165ms compile, 0.19.0 binary runs. Typecheck green. 18 flaky failures in `bun test` are all PGLite shared-state timeouts in setup hooks — every failing file passes cleanly in isolation (dream 11/0, orphans 35/0, check-update 20/0). Pre-existing infra, not introduced by this merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
gbrain dreamas a first-class CLI verb and unifies brain maintenance around a singlerunCycleprimitive. Closes the README gap where "the dream cycle" has been promised for a year without a command to match.One primitive (
src/core/cycle.ts). Two thin CLIs (dream+autopilot). Three callers (dream, autopilot daemon inline, Minionsautopilot-cyclehandler) all converge on the same phase order: lint → backlinks → sync → extract → embed → orphans. Fix files first, then index them. The original PR #309's dream.ts had the phase order wrong (sync before lint) — this gets it right.Autopilot users get lint + orphan sweep added to their nightly cycle automatically. No config change.
gbrain jobs listnow shows 6-phase reports for everyautopilot-cyclejob.Review history
This work started as PR #309 (
gbrain dreamby @WinterMute). Deep review (CEO + Eng ×3 + Codex outside-voice + DX) found structural issues that required restarting from scratch:--dry-runsilently wrote to DB inperformSync's full-sync pathrunEmbedCorehad no dry-run mode and returnedvoidfindOrphansuseddb.getConnection()global (didn't compose with injected engines or PGLite)execSync('bun run cli.ts ...')— circular CLI invocation, defeated v0.15.2's heartbeatpg_try_advisory_lockbroken under v0.15.4's PgBouncer transaction pooling)This PR's 6 commits are the clean redesign that closed all of those.
6 bisectable commits
Each commit is independently green for the affected test files:
fix(sync): honor --dry-run in full-sync path + expose embedded countfix(embed): add dry-run mode + return EmbedResult with countsrefactor(orphans): engine-injected queries, drop db.getConnection() globalfeat(cycle): add runCycle primitive in src/core/cycle.tsfeat(dream): add gbrain dream CLI as a thin alias over runCyclefeat: v0.17.0 — autopilot + jobs delegate to runCycle (unifies the cycle)git bisectbetween any two revisions lands on a green state. Commits 1-3 are genuinely independent (upstream library-contract fixes); commit 4 depends on all three; commits 5 and 6 depend on commit 4.What shipped
New primitive:
src/core/cycle.tsrunCycle(engine: BrainEngine | null, opts: CycleOpts): Promise<CycleReport>gbrain_cycle_lockstable (TTL-based, survives PgBouncer)yieldBetweenPhaseshook for Minions lock renewalCycleReport.schema_version: "1"stable for agentsPhaseResult.error: {class, code, message, hint?, docs_url?}structured errorsNew CLI:
gbrain dreamrunCycle--dry-run,--json,--phase <name>,--pull,--dir <path>--dirORsync.repo_pathconfig (fixes footgun)status: "failed";partialis not a failureSchema migration v16
gbrain_cycle_lockstable +idx_cycle_locks_ttlindexschema.sql(fresh installs),pglite-schema.ts(PGLite), andMIGRATIONSarray (upgrades)Precondition fixes (commits 1-3)
runEmbedCorereturnsEmbedResultwith counts + supports dry-runfindOrphans(engine, opts)takes explicit engine; addedfindOrphanPages()toBrainEngineinterface + both implementationsAutopilot + Minions unification (commit 6)
runCycle(gains lint + orphans)autopilot-cycleMinions handler delegates torunCycle(gains lint + orphans + structured report)gbrain autopilot --helpcross-referencesgbrain dreamTests (all in CI, no DATABASE_URL or API keys required)
test/sync.test.ts: +4 cases (first-sync dry-run, incremental dry-run, --full dry-run, embedded field)test/embed.test.ts: +4 cases (dry-run --all/stale/slugs + non-dry-run regression guard)test/orphans.test.ts: +4 cases (engine-injected findOrphans, includePseudo, queryOrphanPages, empty brain)test/core/cycle.test.ts: new, 18 cases — dryRun × phases × lock × engine-null matrixtest/dream.test.ts: rewritten, 11 cases — argv parsing, brainDir resolution, output, exit codesTest plan
gbrain dream --helpshows new help text with cron example + autopilot cross-refgbrain dream --dry-run --jsonagainst real brain produces CycleReport with zero writesgbrain dream --jsonshows expected activity countsautopilot --installusers see 6-phase reports ingbrain jobs get <id>Related
gbrain dream— nightly dream cycle orchestrator #309 with credit to @WinterMute (their original thesis is preserved;Co-Authored-By: Wintermuteon commit 5)skills/migrations/v0.17.0.mdfor the user-facing migration notesCHANGELOG.mdv0.17.0 entry for the release summary🤖 Generated with Claude Code