Commit Graph

3 Commits

Author SHA1 Message Date
Garry Tan
e362b0ae2f v1.37.0.0 feat: split-engine gbrain (remote MCP brain + local PGLite for code) (#1500)
* feat(gbrain): add lib/gbrain-local-status classifier with 5-state engine status + 60s cache

Foundation for split-engine gbrain: shared classifier used by both
bin/gstack-gbrain-detect (preamble probe) and bin/gstack-gbrain-sync.ts
(orchestrator SKIP-when-not-ok). Single source of truth.

Probes via `gbrain sources list --json` and classifies stderr against the
same patterns lib/gbrain-sources.ts:66-67 already uses ("Cannot connect to
database", "config.json"). Returns one of: ok, no-cli, missing-config,
broken-config, broken-db. Defensive default: unrecognized failures
classify as broken-config so the raw stderr can be surfaced upstream.

Cache at ~/.gstack/.gbrain-local-status-cache.json keyed on
{home, path_hash, gbrain_bin_path, gbrain_version, config_mtime, config_size}
with 60s TTL. Cache invalidates on any invariant change. --no-cache option
busts the cache for callers that just mutated state (/setup-gbrain,
/sync-gbrain after init/migration).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(gbrain): rewrite gstack-gbrain-detect bash→TS + add gbrain_local_status field

Replaces the bash detect helper with a bun shebang script sharing the
gbrain_local_status classifier from lib/gbrain-local-status.ts with the
sync orchestrator. Single source of truth for engine-status classification
between preamble-probe and orchestrator-skip paths.

Filename stays gstack-gbrain-detect (no .ts extension) so existing skill
preamble callers shell out unchanged. Shebang `#!/usr/bin/env -S bun run`
resolves bun at runtime.

Output is key/type backward-compatible with the bash version per plan
codex #5: the 9 pre-existing keys (gbrain_on_path, gbrain_version,
gbrain_config_exists, gbrain_engine, gbrain_doctor_ok, gbrain_mcp_mode,
gstack_brain_sync_mode, gstack_brain_git, gstack_artifacts_remote) stay
identical in name + type + value semantics. One new key added:
gbrain_local_status (5-state string enum).

Updates the existing schema regression at test/gstack-gbrain-detect-mcp-mode.test.ts
to include the new key. Adds test/gbrain-detect-shape.test.ts asserting
the regression contract for future changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(gbrain): orchestrator SKIP when local engine not ok + remote-http transcripts via artifacts pipeline

Two changes in the sync orchestrator, both per plan D11/D12:

1. bin/gstack-gbrain-sync.ts: runCodeImport + runMemoryIngest call
   localEngineStatus() (shared classifier from lib/gbrain-local-status.ts).
   When status is not 'ok', return a SKIP stage result with a clear reason
   instead of crashing with "source registration failed: gbrain not
   configured". Brain-sync stage runs regardless — it doesn't depend on
   local engine. dry-run preview path is gated above the check so it
   continues to show would-do steps even when the engine is broken.

2. bin/gstack-memory-ingest.ts: when gbrain MCP is registered as
   remote-http (Path 4), persist staged transcripts to
   ~/.gstack/transcripts/run-<pid>-<ts>/ instead of the ephemeral
   ~/.gstack/.staging-ingest-<pid>-<ts>/ tmp dir, and SKIP the local
   `gbrain import` call entirely. The artifacts pipeline (gstack-brain-sync
   push to git, brain admin pulls and indexes) handles routing to the
   remote brain. Local PGLite (when present via Step 4.5) stays code-only.

State recording still happens — prepared pages get their mtime+sha256
stamped under remote-http mode so the next /sync-gbrain doesn't
re-stage them. Cleanup is skipped intentionally so the persisted dir
survives until gstack-brain-sync moves it.

Adds test/gbrain-sync-skip.test.ts covering 5 SKIP scenarios (broken-db,
broken-config, no-cli, missing-config, ok pass-through). All 25
sync-related unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(gbrain): v1.34.0.0 migration notice + transcripts allowlist for artifacts pipeline

Per plan D5 + D11. Two pieces of the split-engine rollout:

1. gstack-upgrade/migrations/v1.34.0.0.sh — prints a one-time
   discoverability notice for existing Path 4 (remote-http MCP) users
   whose machine has no local engine yet. Tells them about /setup-gbrain
   Step 4.5 (the new local-PGLite opt-in). Silent for everyone else.
   User can suppress permanently via `gstack-config set
   local_code_index_offered true`. Touchfile at
   ~/.gstack/.migrations/v1.34.0.0.done makes it idempotent.

2. bin/gstack-artifacts-init — adds `transcripts/run-*/*.md` and
   `transcripts/run-*/**/*.md` to the managed allowlist so the
   gstack-memory-ingest persistent staging dir (used in remote-http
   mode per D11) gets pushed to the artifacts repo. Brain admin's
   pull job then indexes transcripts into the remote brain.
   Privacy class: behavioral (matches transcript content).

Adds test/gstack-upgrade-migration-v1_34_0_0.test.ts with 5 cases:
state match, no-MCP, local-config-present, opt-out, and idempotency.
All 5 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(gbrain): /setup-gbrain Step 1.5/4.5 + /sync-gbrain Step 1.5 templates

Per plan D4, D10, D11, D12. Wires the skill prose to the new
split-engine flow + classifier introduced in earlier commits.

setup-gbrain/SKILL.md.tmpl:
  - Step 1: detect output description now includes the v1.34.0.0
    gbrain_local_status field (5 values).
  - Step 1.5 (NEW): broken-db / broken-config remediation. AskUserQuestion
    with 4 options — Retry / Switch to PGLite / Switch brain mode / Quit
    (plan D4). Retry is recommended first since broken-db often = transient
    Postgres outage. PGLite is explicitly one-way + destructive (moves
    existing config to ~/.gbrain/config.json.gstack-bak-<ts>); rollback on
    init failure restores the .bak (plan D7).
  - Step 4d → Step 4.5 (NEW): in Path 4, after the verify step, offer
    local PGLite for code search. AskUserQuestion Yes/No (plan D10/D11).
    Yes path runs gstack-gbrain-install + `gbrain init --pglite --json`
    with the same rollback-safe sequence. No path skips Steps 3/4/5/7.5.
  - Step 10 verdict (Path 4): adds "Code search" row reflecting Step 4.5
    choice. Updates "Transcripts" row to describe the new D11 routing
    (artifacts repo → remote brain).

sync-gbrain/SKILL.md.tmpl:
  - Step 1 split-engine prose: corrects the prior misleading claim that
    "memory routes through whatever setup-gbrain configured, including
    remote-MCP" (codex finding #3). Memory stage shells out to local
    `gbrain import` in local-stdio mode; in remote-http mode it persists
    to ~/.gstack/transcripts/ for the artifacts pipeline.
  - Step 1.5 (NEW): local-engine pre-flight. STOP on no-cli, broken-config,
    broken-db. Soft skip (continue with code+memory SKIP) on
    missing-config + remote-http per plan D12. Surfaces actionable user
    remediation message instead of the orchestrator crashing two stages
    with ERR.

Regenerated SKILL.md for all hosts (claude, kiro, opencode, slate,
cursor, openclaw, hermes, gbrain). All 712 skill-validation + gen-skill-docs
tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gbrain): .bak-rollback contract for Step 1.5 / 4.5 init failure path

Per plan D7 (rollback semantics) and codex #10 (rollback scope). The
/setup-gbrain skill instructs the model to follow a specific shell
sequence when running `gbrain init --pglite` against an existing
config:

  1. mv ~/.gbrain/config.json ~/.gbrain/config.json.gstack-bak-<ts>
  2. gbrain init --pglite --json
  3. on non-zero exit: mv .bak back; surface error

This test verifies that contract using a fake `gbrain` binary that
fails on init. Three cases:

  - FAILURE: gbrain init exits non-zero → broken config restored to
    original path, no leftover .bak.
  - SUCCESS: gbrain init exits 0 → new config in place, .bak survives
    for audit (user reviews + deletes manually).
  - SCOPE: any partial PGLite directory at ~/.gbrain/pglite/ is NOT
    auto-cleaned. We only promise to restore config.json; PGLite
    cleanup is the user's call (codex #10).

If the skill template rewrites this sequence in a future change, this
test should fail until the test's shell is updated too. That's the
point — keep the test and the skill template aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gbrain): periodic E2E for /setup-gbrain Path 4 + Step 4.5 Yes flow

End-to-end coverage of the new opt-in question via runAgentSdkTest.
Stubs the MCP endpoint at /tools/list with a 200 response carrying a
fake gbrain v0.32.3.0 serverInfo, and fakes the gbrain + claude CLIs
so init writes a PGLite config and mcp add succeeds. Asserts the model:

  1. invokes gstack-gbrain-install (Step 4.5 Yes branch)
  2. invokes `gbrain init --pglite --json`
  3. writes a working ~/.gbrain/config.json with engine=pglite
  4. registers the remote MCP via `claude mcp add --transport http`
  5. never leaks the bearer token to CLAUDE.md

Classified as periodic-tier per plan D6 (codex #12 flagged AgentSDK
flakiness; gate-tier coverage of the split-engine behavior lives in the
deterministic unit tests at gbrain-local-status.test.ts and
gbrain-sync-skip.test.ts). Touchfile fires the test when the skill
template, install/verify/init helpers, the local-status classifier, or
the agent-sdk-runner harness changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(gbrain): bump migration to v1.35.0.0 after main merge

main shipped v1.34.0.0 (factory-export submodule) + v1.34.1.0 (update-check
hardening) while this branch was in flight. The migration file I named
v1.34.0.0.sh now belongs at v1.35.0.0 — the next minor on top of main,
matching the scale of split-engine work (new lib + orchestrator skip +
template overhaul + transcripts routing).

Renames the migration script and its test file; updates all internal
version references in both files. Behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* perf(gbrain): memoize gbrain resolution + use --fast doctor in detect

Cuts detect's wall time substantially by sharing fork-exec results
between the helper that walks the JSON output and the localEngineStatus
classifier from lib/gbrain-local-status.ts.

Before: detect made 2x `command -v gbrain` calls (one in detect's
detectGbrain, one in the classifier's resolveGbrainBin) and 2x
`gbrain --version` calls. With memoization keyed on PATH, both
collapse to one fork each (~400ms saved per skill preamble).

Also adds `--fast` to the `gbrain doctor --json` call in detect so a
broken-db config (Garry's repro) doesn't burn a full 5s timeout on the
doctor's DB-connection check. The classifier still probes the DB
directly via `gbrain sources list --json` for engine reachability —
that's `gbrain_local_status`, separate from the coarse
`gbrain_doctor_ok` summary flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(gbrain): relax E2E assertions to smoke-test contract

Per codex #12 (AgentSDK harness is non-deterministic): the E2E now
asserts the model followed the split-engine path WITHOUT requiring a
specific subcommand sequence. Three assertions:

  1. AskUserQuestion was called (model reached interactive branches)
  2. At least one of {gstack-gbrain-install, `gbrain init --pglite`,
     `claude mcp add`} fired (model followed the skill, not a no-op)
  3. The fake bearer token never leaked to CLAUDE.md (security regression)

Deterministic per-step coverage of the same flow lives in the gate-tier
unit tests (gbrain-local-status, gbrain-sync-skip, init-rollback,
upgrade-migration). The E2E exists to catch the "model can't follow
the skill at all" regression class, not to pin the exact tool sequence.

Test passes in 280s against the live Agent SDK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(version): bump CLI smoke-test timeout to 15s (flaky at 5s under load)

The gstack-next-version integration smoke test spawns a child process
that does git operations + sibling-worktree probing. Wall time hovers
4-5s on M-series Macs; flakes at exactly 5001-5002ms when the test
suite runs under load (bun's parallel scheduling). Bumping per-test
timeout to 15s eliminates the flake without changing test logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v1.37.0.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:20:48 -07:00
Garry Tan
b9371d716e v1.34.2.0 fix wave: /codex review on CLI 0.130+, /investigate learnings, /sync-gbrain on Supabase (3 community-reported bugs) (#1478)
* fix(learnings): accept type:"investigation" in gstack-learnings-log

The /investigate skill instructed agents to log learnings with type:"investigation",
but bin/gstack-learnings-log:22 rejected anything not in
[pattern, pitfall, preference, architecture, tool, operational]. Every
investigation run exited 1 to stderr and the learning was dropped, silently
to the user.

Fix: add 'investigation' to ALLOWED_TYPES.

Regression test: round-trips a learning with type:"investigation" and asserts
exit 0 + file write; second test reads investigate/SKILL.md.tmpl and asserts
it emits the literal type:"investigation" string, guarding the
template/validator contract at both ends.

Fixes #1423. Reported by diogolealassis.

* fix(gbrain): engine detection survives gbrain ≥0.25 schema + non-zero doctor exit

freshDetectEngineTier() in lib/gstack-memory-helpers.ts returned engine:
"unknown" for every Supabase user on gbrain ≥0.25. Two stacking bugs:

1. execSync("gbrain doctor --json --fast 2>/dev/null") threw on non-zero
   exit. gbrain doctor exits 1 whenever health_score < 100, which is
   essentially every fresh install due to resolver_health warnings. The
   JSON output never reached the parser.
2. gbrain ≥0.25 shipped schema_version:2 doctor output that dropped the
   top-level 'engine' field entirely.

Result: every /sync-gbrain on Supabase logged 'engine=unknown' and skipped
all sync stages silently.

Fix:
- Replace execSync with execFileSync (no shell, no bash-specific 2>/dev/null
  redirect; portable to Windows).
- Recover stdout from the thrown error object so non-zero exits still parse.
- Fall back to reading gbrain's config.json (respecting GBRAIN_HOME env var,
  defaulting to ~/.gbrain/config.json) when doctor output doesn't surface
  an engine field.
- Add logGbrainError() helper that appends one-line JSONL to
  ~/.gstack/.gbrain-errors.jsonl on parse failure, so future regressions
  leave a forensic trail.

The "supabase" tier here means "remote postgres" in practice — gbrain
config uses engine:"postgres" for both real Supabase and any other
remote postgres (e.g. local-postgres-for-testing). Downstream sync code
treats them identically, so the label compression is intentional and
documented inline.

Regression test: existing detectEngineTier suite now isolates HOME +
GBRAIN_HOME + PATH to temp dirs (closes a flake source where the prior
tests would read whatever was on the reviewer's machine). New test
forces gbrain off PATH, writes a synthetic config.json with
engine:"postgres", asserts detectEngineTier() returns
engine:"supabase".

Fixes #1415. Patch shape contributed by Shiv @shivasymbl (tested on
gstack v1.31.0.0 + gbrain v0.31.3 + Supabase).

* fix(codex): /codex review works on Codex CLI ≥0.130.0

Codex CLI 0.130.0 made [PROMPT] and --base <BRANCH> mutually exclusive at
argv level. Step 2A of codex/SKILL.md.tmpl had always passed both (the
filesystem boundary prefix as the prompt argument + the base branch), so
every /codex review call died with:

  error: the argument '[PROMPT]' cannot be used with '--base <BRANCH>'

Fix: split Step 2A into two paths.

Default (no custom user instructions): bare 'codex review --base <base>'.
Codex's review prompt is internally diff-scoped, so the model focuses on
the changes against base. The filesystem boundary prefix is dropped here
because Codex 0.130 has no documented system-prompt config key
(probed -c 'system_prompt="..."' against 0.130 — the flag is silently
accepted but the value isn't applied). Skill files under .claude/ and
agents/ are public, so this is a token-efficiency concern, not a safety
one.

Custom instructions (/codex review <focus>): route through codex exec
with the diff written to a tempfile, inlined into the prompt between
explicit DIFF_START / DIFF_END markers. The boundary is preserved here
because codex exec isn't auto-scoped to the diff. The DIFF_START/END
delimiters tell the model where data ends and instructions resume, which
materially reduces prompt-injection hijack rates when the diff contains
adversarial content.

Note on bash semantics: codex's earlier review flagged the exec route as
"command injection via $_DIFF interpolation." That framing is wrong —
bash parameter expansion does not re-evaluate $(...) or backticks inside
the expanded value, so a diff containing $(rm -rf /) is plain string
data to codex exec. The real risk is prompt injection (model-side, not
shell-side), which the DIFF_START/END pattern mitigates.

Regression tests in test/codex-hardening.test.ts assert across BOTH
codex/SKILL.md.tmpl AND the generated codex/SKILL.md:
1. No 'codex review' invocation line combines a quoted-string OR variable
   positional argument with --base.
2. Step 2A still contains either bare 'codex review --base' OR 'codex
   exec' (guards against accidental deletion of both fix paths).

Fixes #1428. Reported by Stashub.

* test: raise timeouts for slow integration tests

Two test files were timing out at the default 5s on developer machines,
both pre-existing on origin/main but unrelated to this branch's bug fixes:

- test/gstack-artifacts-init.test.ts: 13 tests spawning real subprocesses
  via fake gh/glab/git shims in PATH. bun's fork+exec overhead pushed
  these past 5s consistently. Added a local test-wrapper that aliases
  test() with a 30s timeout (matches the brain-sync.test.ts pattern
  already in the repo).
- test/gstack-next-version.test.ts: one integration smoke test that
  spawns 'bun run ./bin/gstack-next-version' and parses the resulting
  JSON. The subprocess does a 'gh pr list' against the live GitHub API
  to enumerate claimed version slots. Network latency makes 5s tight;
  raised this single test to 30s.

No production code changed. The tests already passed deterministically
once given enough wall-clock time.

* chore: bump version and changelog (v1.34.2.0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 11:11:52 -04:00
Garry Tan
e4041f7a7f v1.11.0.0 feat(ship): workspace-aware version allocation (#1168)
* feat: bin/gstack-next-version util + workspace_root config key

Host-aware (GitHub + GitLab + unknown) VERSION allocator. Queries the open
PR queue, fetches each PR's VERSION at head, scans configurable Conductor
sibling worktrees for WIP work, and picks the next free slot at the
requested bump level.

Pure reader, never writes files. /ship consumes the JSON and decides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fixture tests for gstack-next-version

21 pure-function tests covering parseVersion / bumpVersion / cmpVersion /
pickNextSlot (with 8 collision scenarios) / markActiveSiblings (4 cases)
plus one CLI smoke test against the live repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(scripts): detect-bump + compare-pr-version helpers

Shared between /ship (legacy path) and the CI version-gate job.
detect-bump: derive bump level from VERSION diff. compare-pr-version:
CI gate logic with three exit paths (pass / block / fail-open).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ci): version-gate + pr-title-sync workflows (GitHub + GitLab)

Merge-time collision gate. Fail-open on util errors (network, auth, bug),
fail-closed on confirmed collisions. pr-title-sync rewrites the PR title
when VERSION changes on push, only for titles that already carry the
v<X.Y.Z.W> prefix (custom titles left alone).

GitLab CI mirrors both jobs for host parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): queue-aware /ship + drift abort in /land-and-deploy + advisory in /review

ship Step 12: queue-aware version pick (FRESH path) + drift detection
(ALREADY_BUMPED path). Prompts user to rebump when queue moved, runs
the full ship metadata path (VERSION, package.json, CHANGELOG header,
PR title) on the rebump so nothing goes stale.

ship Step 19: PR title format v<X.Y.Z.W> <type>: <summary> — version
ALWAYS first. Rerun path updates title (not just body) when VERSION
changed.

land-and-deploy Step 3.4: detect drift, ABORT with instruction to
rerun /ship. Never auto-mutates from land.

review Step 3.4: advisory one-line queue status. Non-blocking.

Goldens refreshed for all three hosts (claude/codex/factory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skill): /landing-report read-only queue dashboard

Standalone skill that renders the current PR queue, sibling worktrees,
and what all four bump levels would claim. Pure reader. Useful when
running many parallel Conductor workspaces to see what's in flight
before shipping anything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: versioning invariant in CLAUDE.md

Document that VERSION is a monotonic sequence, not a strict semver
commitment. Bump level expresses intent; queue-advance within a level
is permitted. Prevents future re-litigation of the workspace-aware
ship design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v1.8.0.0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ship): exclude current PR from queue-awareness (self-reference bug)

Version gate flagged PR #1168 as stale because the util counted the PR
itself as a queued claim. The exclude filter removes that self-reference.

New --exclude-pr <N> flag on bin/gstack-next-version. CI workflows pass
github.event.pull_request.number / CI_MERGE_REQUEST_IID. Local /ship
auto-detects via gh pr view when the flag isn't passed, with a warning
recording the auto-exclusion so it's observable.

Caught during the first live ship through the v1.8.0.0 gate — the kind
of dogfood the whole release is designed for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge remote-tracking branch 'origin/main' into garrytan/workspace-aware-ship

Rebumped v1.8.0.0 -> v1.11.0.0 (minor-past main's v1.10.1.0) using
bin/gstack-next-version — the same queue-aware path this branch introduces.
CHANGELOG repositioned so v1.11.0.0 sits above main's new entries
(v1.10.1.0 / v1.10.0.0 / v1.9.0.0).

Conflicts resolved:
- VERSION, package.json: rebumped to v1.11.0.0 (util-picked)
- bin/gstack-config: merged both lists (workspace_root + gbrain keys)
- CHANGELOG.md: hoisted v1.11.0.0 entry above main's new entries

Pre-existing failures in main (4) documented but not fixed in this PR:
1. gstack-brain-sync secret scan > blocks bearer-json (brain-sync tests)
2. no files larger than 2MB (security-bench fixture, already TODO'd)
3. selectTests > skill-specific change (touchfiles scoping)
4. Opus 4.7 overlay pacing directive (expectation stale after v1.10.1.0
   removed the Fan out nudge)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: re-trigger PR workflows after merge

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 23:03:27 -07:00