Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
# Changelog

## [1.60.2.0] - 2026-07-15

## **Windows Codex installs now ship the runtime modules their GBrain tools import.**
## **Worktree sync preserves path boundaries and refuses to widen maintenance across the whole brain.**

The Windows Codex runtime could install executable GStack helpers without the sibling `lib` modules they import. Once the helpers ran, source sync had three more sharp edges: equivalent Windows path spellings looked like drift, paths containing spaces passed through shell parsing, and a source-scoped maintenance request could assume a CLI capability the installed GBrain did not expose. This release closes all four defects as one boundary-hardening patch.

### The numbers that matter

Source: the DEV-206 regression suite and adjacent GBrain orchestration tests (`bun test test/dev-206-windows-gbrain.test.ts test/gbrain-cycle-completed.test.ts test/gbrain-dream-stage.test.ts test/gbrain-exec-invariant.test.ts test/gbrain-sources.test.ts test/gbrain-spawn-windows-shell.test.ts test/gstack-gbrain-sync.test.ts`).

| Windows worktree-sync boundary | Before | After |
|---|---:|---:|
| Required runtime module trees installed with Codex helpers | 0 | 1 (`lib`) |
| Equivalent slash/case path forms that trigger re-registration | possible | 0 in regression coverage |
| Shell parsing layers around GBrain argv | 1 | 0 |
| Brain-wide maintenance fallbacks from a source-scoped request | possible | 0 (fails closed) |
| Focused regression and orchestration tests | — | 92 passing |

### What this means for you

Codex-hosted GStack tools keep their imports after both setup and upgrade. A Windows worktree path keeps its spaces as one argument, case or slash differences do not cause a destructive source remove/re-add, and `/sync-gbrain` reports an actionable compatibility error when the installed CLI cannot perform source-scoped maintenance. It never substitutes a brain-wide cycle silently.

### Itemized changes

#### Fixed

- Codex runtime metadata and `./setup` now install `lib` beside shipped `bin` helpers in both the global runtime and project sidecar.
- Source drift checks resolve Windows drive/UNC paths, normalize separators, and compare case-insensitively before considering destructive re-registration or hostname-fold migration.
- GBrain subprocesses resolve a direct executable and use structural argv with `shell: false`; Windows `.cmd`, `.bat`, and `.ps1` launchers are rejected with setup remediation instead of rebuilding a shell command.
- The dream stage probes `gbrain dream --help` for `--source` support before acquiring its maintenance marker. Missing capability or an unresolvable worktree returns an error and never falls back to bare `gbrain dream`.

#### For contributors

- Cross-platform fake GBrain harnesses now compile executable test shims, so spaced-path and direct-spawn behavior is exercised without shell quoting artifacts.
- DEV-206 coverage simulates Codex install/upgrade, verifies normalized drift, rejects unsafe Windows launchers, and runs the real wrapper against a brain-wide-only fake CLI to prove no maintenance dispatch occurs.

## [1.60.1.0] - 2026-07-09

## **The /autoplan dual-voice eval is back on the board, catching real regressions.**
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan-
| `/open-gstack-browser` | **GStack Browser** — launch GStack Browser with sidebar, anti-bot stealth, auto model routing (Sonnet for actions, Opus for analysis), one-click cookie import, and Claude Code integration. Clean up pages, take smart screenshots, edit CSS, and pass info back to your terminal. |
| `/setup-deploy` | **Deploy Configurator** — one-time setup for `/land-and-deploy`. Detects your platform, production URL, and deploy commands. |
| `/setup-gbrain` | **GBrain Onboarding** — from zero to running gbrain in under 5 minutes. PGLite local, Supabase existing URL, or auto-provision a new Supabase project via Management API. MCP registration for Claude Code + per-repo trust triad (read-write/read-only/deny). [Full guide](USING_GBRAIN_WITH_GSTACK.md). |
| `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Idempotent; safe to re-run. |
| `/sync-gbrain` | **Keep Brain Current** — re-index this repo's code into gbrain via `gbrain sources add` + `gbrain sync --strategy code`, refresh the `## GBrain Search Guidance` block in CLAUDE.md, and auto-remove guidance when the capability check fails. `--incremental` (default), `--full`, `--dry-run`. Source-scoped maintenance fails closed when the installed GBrain only supports a brain-wide cycle. Idempotent; safe to re-run. |
| `/gstack-upgrade` | **Self-Updater** — upgrade gstack to latest. Detects global vs vendored install, syncs both, shows what changed. |
| `/ios-qa` | **iOS Live-Device QA (v1.43.0.0+)** — drive a real iPhone over USB CoreDevice via an embedded `StateServer` in the app. Read Swift source, codegen typed `@Observable` accessors, run the agent loop. Optional `--tailnet` flag exposes the device to OpenClaw or any HTTP-capable agent on your Tailscale tailnet so remote agents can run iOS QA without ever touching the hardware. Capability-tier allowlist (observe/interact/mutate/restore), per-device session lock, audit log. |
| `/ios-fix`, `/ios-design-review`, `/ios-clean`, `/ios-sync` | iOS bug-fix loop, designer's-eye HIG audit, debug-bridge cleanup, and accessor resync. See `docs/skills.md`. End-to-end walkthrough: [docs/howto-ios-testing-with-gstack.md](docs/howto-ios-testing-with-gstack.md). |
Expand Down Expand Up @@ -405,7 +405,7 @@ Four paths, pick one:

After init, the skill offers to register gbrain as an MCP server for Claude Code (`claude mcp add gbrain -- gbrain serve`) so `gbrain search`, `gbrain put`, etc. show up as first-class typed tools — not bash shell-outs.

**Keeping the brain current.** Run `/sync-gbrain` from any repo to re-index its code into gbrain (incremental by default, `--full` for a full reindex, `--dry-run` to preview). The skill registers the cwd as a federated source via `gbrain sources add`, runs `gbrain sync --strategy code`, and writes a `## GBrain Search Guidance` block to your project's CLAUDE.md so the agent prefers `gbrain search`/`code-def`/`code-refs` over Grep. The block is removed automatically if the capability check fails — no stale guidance pointing at tools that aren't installed.
**Keeping the brain current.** Run `/sync-gbrain` from any repo to re-index its code into gbrain (incremental by default, `--full` for a full reindex, `--dry-run` to preview). The skill registers the cwd as a federated source via `gbrain sources add`, runs `gbrain sync --strategy code`, and writes a `## GBrain Search Guidance` block to your project's CLAUDE.md so the agent prefers `gbrain search`/`code-def`/`code-refs` over Grep. The block is removed automatically if the capability check fails — no stale guidance pointing at tools that aren't installed. If a full sync needs call-graph maintenance, GStack requires the installed CLI to support `gbrain dream --source`; it will not silently widen that request to a brain-wide cycle.

**Per-remote trust policy.** Each repo on your machine gets one of three tiers:

Expand Down
4 changes: 3 additions & 1 deletion USING_GBRAIN_WITH_GSTACK.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,13 @@ The skill runs three stages — code, memory, brain-sync — independently. A fa
**What it does on a fresh worktree:**

1. **Pre-flight.** Checks `gbrain_local_status` (the local engine's health). If the engine is `broken-db` or `broken-config`, the skill STOPs with a remediation menu — it refuses to silently degrade. If the local engine is missing and you're in remote-MCP mode (Path 4), the code stage SKIPs cleanly and only brain-sync runs.
2. **Code stage.** Registers the cwd as a federated source via `gbrain sources add`, writes a `.gbrain-source` pin file in the repo root (kubectl-style context — every worktree gets its own pin, so Conductor sibling worktrees don't collide), runs `gbrain sync --strategy code`.
2. **Code stage.** Registers the cwd as a federated source via `gbrain sources add`, writes a `.gbrain-source` pin file in the repo root (kubectl-style context — every worktree gets its own pin, so Conductor sibling worktrees don't collide), runs `gbrain sync --strategy code`, and normalizes Windows slash/case differences before deciding that a registered path has drifted.
3. **Memory stage.** Stages your `~/.gstack/` transcripts + curated memory. In local-stdio MCP mode, ingests into the local engine. In remote-http MCP mode, persists staged markdown to `~/.gstack/transcripts/run-<pid>-<ts>/` for the remote brain admin's pull pipeline. The ingest timeout is 30 minutes by default; raise it for a big brain with `GSTACK_INGEST_TIMEOUT_MS` (accepts 1 min–24h). On timeout the gbrain import checkpoint is preserved, so the next `/sync-gbrain` resumes instead of starting over.
4. **Brain-sync stage.** Pushes curated artifacts (plans, designs, retros) to your private artifacts repo if you have one configured.
5. **CLAUDE.md guidance.** Capability-checks the round-trip (write a page → search → find it). If green, writes the `## GBrain Search Guidance` block to your project's CLAUDE.md. If red, REMOVES the block — the agent should never be told to use a tool that isn't installed.

`--full` may also need a call-graph maintenance cycle for this worktree. GStack first checks whether the installed GBrain exposes `gbrain dream --source`; if it does not, sync reports an error and leaves maintenance to the operator. It never substitutes a brain-wide `gbrain dream` for a worktree-scoped request.

**The watermark.** Sync state advances by commit hash. If gbrain hits a file it can't index (5 MB hard limit per file, or a file vanished mid-sync), the watermark stays put and subsequent syncs retry. To acknowledge an unfixable failure and move past it:

```bash
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.60.1.0
1.60.2.0
99 changes: 79 additions & 20 deletions bin/gstack-gbrain-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { createHash } from "crypto";

import "../lib/conductor-env-shim";
import { detectEngineTier, withErrorContext, canonicalizeRemote } from "../lib/gstack-memory-helpers";
import { ensureSourceRegistered, sourcePageCount, parseSourcesList, cycleCompleted, type CycleStatus } from "../lib/gbrain-sources";
import { ensureSourceRegistered, sourcePageCount, parseSourcesList, cycleCompleted, sourcePathsEqual, type CycleStatus } from "../lib/gbrain-sources";
import { detectAutopilot, decideSourceRemove, decideCodeSync } from "../lib/gbrain-guards";
import { localEngineStatus, type LocalEngineStatus } from "../lib/gbrain-local-status";
import { buildGbrainEnv, spawnGbrain, execGbrainJson, NEEDS_SHELL_ON_WINDOWS } from "../lib/gbrain-exec";
Expand Down Expand Up @@ -499,7 +499,7 @@ export function planHostnameFoldMigration(
if (oldPath === null) {
return { kind: "none", reason: "no-legacy-source" };
}
if (oldPath !== currentRoot) {
if (!sourcePathsEqual(oldPath, currentRoot)) {
return {
kind: "skipped-path-drift",
oldId: legacyPathHashId,
Expand Down Expand Up @@ -1188,11 +1188,43 @@ export function shouldRunDream(args: CliArgs, cycle: CycleStatus | null): boolea
return false;
}

export type SourceScopedDreamPlan =
| { ok: true; args: string[] }
| { ok: false; error: string };

/**
* Run `gbrain dream` — the brain-global maintenance cycle whose
* resolve_symbol_edges phase builds the call graph. Runs LOCK-FREE (called
* after the sync lock releases) so it never freezes sibling worktrees; the
* `.dream-in-progress` marker dedupes concurrent dreams instead.
* Turn `gbrain dream --help` into a source-scoped command or a fail-closed
* compatibility error. A brain-wide `dream` is never an implicit fallback for
* a worktree-scoped sync request.
*/
export function planSourceScopedDream(
sourceId: string,
helpStatus: number | null,
helpOutput: string,
): SourceScopedDreamPlan {
if (helpStatus !== 0) {
return {
ok: false,
error:
`could not verify source-scoped gbrain dream support (help exited ${helpStatus}); ` +
"upgrade gbrain or review a brain-wide maintenance run manually; refusing to widen scope",
};
}
if (!/--source(?:\s|=|<|\[|$)/m.test(helpOutput)) {
return {
ok: false,
error:
"installed gbrain dream does not support --source; upgrade gbrain or review a " +
"brain-wide maintenance run manually; refusing to widen scope",
};
}
return { ok: true, args: ["dream", "--source", sourceId] };
}

/**
* Run a proven source-scoped `gbrain dream --source <id>` maintenance cycle.
* Runs LOCK-FREE (called after the sync lock releases) so it never freezes
* sibling worktrees; the `.dream-in-progress` marker dedupes concurrent dreams.
*
* Returns a StageResult (never throws). SKIP (ran:false, ok:true) for: dry-run
* preview, local engine not ok, or a fresh marker present. ERR (ran:true,
Expand All @@ -1213,7 +1245,7 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
duration_ms: 0,
summary: sourceId
? `would: gbrain dream --source ${sourceId} (build this source's call graph)`
: "would: gbrain dream (call-graph build)",
: "would refuse: cannot derive a worktree source id (no brain-wide fallback)",
};
}

Expand All @@ -1224,6 +1256,45 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
return skipStageForLocalStatus("dream", localStatus, t0);
}

const root = repoRoot();
if (!root) {
return {
name: "dream",
ran: true,
ok: false,
duration_ms: Date.now() - t0,
summary: "cannot derive a worktree source id; refusing to run brain-wide gbrain dream",
};
}
const sourceId = deriveCodeSourceId(root);
let dreamPlan: SourceScopedDreamPlan;
try {
const help = spawnGbrain(["dream", "--help"], {
stdio: ["ignore", "pipe", "pipe"],
timeout: 10_000,
baseEnv: process.env,
});
dreamPlan = planSourceScopedDream(
sourceId,
help.status,
`${help.stdout || ""}${help.stderr || ""}`,
);
} catch (err) {
dreamPlan = {
ok: false,
error: `could not inspect gbrain dream capability: ${(err as Error).message}; refusing to widen scope`,
};
}
if (!dreamPlan.ok) {
return {
name: "dream",
ran: true,
ok: false,
duration_ms: Date.now() - t0,
summary: dreamPlan.error,
};
}

// Dedupe concurrent dreams across worktrees (lock-free path).
if (!acquireDreamMarker()) {
const pid = dreamMarkerPid();
Expand All @@ -1243,18 +1314,6 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
DEFAULT_DREAM_TIMEOUT_MS,
);

// Scope the cycle to THIS worktree's code source: `gbrain dream --source <id>`.
// Verified empirically (not just from `gbrain --help`): plain `gbrain dream`
// cycles the brain's default source and never runs the source-scoped `extract`
// phase for our code source, so the call graph for the pinned source stays
// empty. `gbrain dream --source <id>` runs the per-source cycle (the form
// `gbrain doctor` recommends for stale sources) and is what actually populates
// code-callers/code-callees for this worktree. Falls back to plain `dream`
// only when we can't derive the source id (not in a git repo).
const root = repoRoot();
const sourceId = root ? deriveCodeSourceId(root) : null;
const dreamArgs = sourceId ? ["dream", "--source", sourceId] : ["dream"];

// spawnGbrain seeds DATABASE_URL from gbrain's config via buildGbrainEnv.
//
// We CAPTURE output (pipe) rather than inherit because `gbrain dream` exits 0
Expand All @@ -1268,7 +1327,7 @@ export async function runDream(args: CliArgs): Promise<StageResult> {
}
let result: ReturnType<typeof spawnGbrain>;
try {
result = spawnGbrain(dreamArgs, {
result = spawnGbrain(dreamPlan.args, {
stdio: ["ignore", "pipe", "pipe"],
timeout: dreamTimeoutMs,
baseEnv: process.env,
Expand Down
4 changes: 2 additions & 2 deletions hosts/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ const codex: HostConfig = {
],

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
globalSymlinks: ['bin', 'lib', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
globalFiles: {
'review': ['checklist.md', 'TODOS-format.md'],
},
},
sidecar: {
path: '.agents/skills/gstack',
symlinks: ['bin', 'browse', 'review', 'qa', 'ETHOS.md'],
symlinks: ['bin', 'lib', 'browse', 'review', 'qa', 'ETHOS.md'],
},

install: {
Expand Down
Loading
Loading