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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable user-facing changes to ByteRover CLI will be documented in this file.

## [3.10.1]

### Added
- **`brv review --disable` / `--enable` per project.** Opt a project out of the human-in-the-loop review prompts and backups, or turn them back on. Run `brv review` with no flags to see the current state. Existing `brv review pending / approve / reject` are unchanged.

### Changed
- **`brv curate` returns to your prompt right away.** The final summary-rebuild step now runs in the background instead of making you wait. Nothing is skipped, just moved off your wait time (about 18 seconds saved on a typical run).

### Fixed
- **Clearer `brv vc status` and `brv vc pull` errors.** Status now catches a few staged-then-edited file states it used to hide. When a pull, checkout, or merge would overwrite local changes, the error now lists the affected files instead of just saying "local changes would be overwritten."
- **OpenAI-compatible providers fail loudly when the URL is wrong.** First-time setup for Ollama, LM Studio, and similar providers now checks the base URL up front and shows the error inline. Bad URLs no longer pre-select a placeholder `llama3` model or hang the REPL on disconnect / reconnect.

## [3.10.0]

### Added
Expand Down
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ npm run dev:ui # Vite dev server for the web UI
- `server/` — Daemon infrastructure: `config/`, `core/` (domain/interfaces), `infra/` (31 modules, including vc, git, hub, mcp, cogit, project, provider-oauth, space, dream, webui), `templates/`, `utils/`
- `shared/` — Cross-module: constants, types, transport events, utils
- `tui/` — React/Ink TUI: app (router/pages), components, features (23 modules, including vc, worktree, source, hub, curate), hooks, lib, providers, stores
- `webui/` — Browser dashboard (React/Vite). Entry `src/webui/index.tsx`; `features/` (15 panels), `pages/` (home, changes, configuration, contexts, tasks, analytics, project-selector), `layouts/`, `stores/`. Connects to the daemon via Socket.IO; no imports from `server/`, `agent/`, or `tui/` (same boundary rule)
- `webui/` — Browser dashboard (React/Vite). Entry `src/webui/index.tsx`; `features/` (15 panels), `pages/` (8 pages: home, changes, configuration, contexts, tasks, analytics, project-selector, not-found), `layouts/`, `stores/`. Connects to the daemon via Socket.IO; no imports from `server/`, `agent/`, or `tui/` (same boundary rule)
- `oclif/` — Commands grouped by topic (`vc/`, `hub/`, `worktree/`, `source/`, `space/`, `review/`, `connectors/`, `curate/`, `model/`, `providers/`, `swarm/`, `query-log/`) + top-level `.ts` commands (incl. `webui.ts`); hooks, lib (daemon-client, task-client, json-response)

**Import boundary** (ESLint-enforced): `tui/` must not import from `server/`, `agent/`, or `oclif/`. Use transport events or `shared/`.
Expand Down Expand Up @@ -101,7 +101,8 @@ npm run dev:ui # Vite dev server for the web UI
- Canonical project resolver: `resolveProject()` in `server/infra/project/` — priority `flag > direct > linked > walked-up > null`. `projectRoot` and `worktreeRoot` are threaded through transport schemas, task routing, and all executors
- All commands are daemon-routed: `oclif/` and `tui/` never import from `server/`
- Oclif: `src/oclif/commands/{vc,worktree,source}/`; TUI: `src/tui/features/{vc,worktree,source}/`; slash commands (`vc-*`, `worktree`, `source`) in `src/tui/features/commands/definitions/`
- `brv curate` blocks execution by default and rejects overlapping runs for the same project; pass `--detach` to run in background. Behavioral contract lives in `src/server/templates/sections/` (`brv-instructions.md`, `workflow.md`, `skill/SKILL.md`) — the in-daemon agent reads these at runtime
- `brv curate` runs Phases 1–3 in the foreground and detaches Phase 4 (post-curate finalization: summary regeneration, manifest rebuild) to the daemon's `PostWorkRegistry`, which serializes per project and coordinates with `dream-lock-service.ts` to prevent concurrent `_index.md` writes. `--detach` makes the entire run background. Overlapping curate runs for the same project are still rejected. Behavioral contract lives in `src/server/templates/sections/` (`brv-instructions.md`, `workflow.md`, `skill/SKILL.md`) — the in-daemon agent reads these at runtime
- `brv review [--disable | --enable]` — toggle the project-scoped HITL review log; `brv review pending` lists items, `brv review approve <id>` / `brv review reject <id>` resolve them. When disabled, sync curate skips the "X operations require review" prompt, detached curate stops emitting per-operation review markers, and `brv dream` no longer surfaces `needsReview` operations. The flag is snapshotted at task creation and propagated via `AsyncLocalStorage` (`resolveReviewDisabled`) so mid-task toggles do not race
- `brv login` defaults to OAuth (interactive provider picker); pass `--api-key` only for CI. `brv logout` clears credentials

### Agent (`src/agent/`)
Expand Down Expand Up @@ -142,6 +143,9 @@ npm run dev:ui # Vite dev server for the web UI
- `BRV_UI_SOURCE` — `submodule` | `package` — forces Vite's shared-UI resolution mode
- `BRV_DATA_DIR` — override the global data dir (default `~/.brv`)
- `BRV_GIT_REMOTE_BASE_URL` — override git remote base URL (beta vs prod testing)
- `BRV_QUEUE_TRACE` — set to `1` to log queue/agent map traces (cipher-agent, abstract-queue)
- `BRV_SESSION_LOG` — file path for daemon/agent session logs (auto-set by `brv-server`; can override for debugging)
- `BRV_E2E_MODE` — `true` switches the daemon to e2e-friendly stdio handling

## Stack

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"coding-assistant",
"knowledge-management"
],
"version": "3.10.0",
"version": "3.10.1",
"author": "ByteRover",
"bin": {
"brv": "./bin/run.js"
Expand Down Expand Up @@ -231,6 +231,9 @@
"ink-spinner",
"ink-text-input",
"ink-scroll-list",
"ink-scroll-view"
"ink-scroll-view",
"@tanstack/react-query",
"react-router-dom",
"zustand"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {AsyncLocalStorage} from 'node:async_hooks'

/**
* Async-context scope for the daemon-stamped `reviewDisabled` value.
*
* The daemon snapshots the project's reviewDisabled flag once at task-create
* and forwards it via TaskExecute. The agent process opens an
* `AsyncLocalStorage` scope around the task body so any descendant async
* callsite — direct curate-tool invocation, sandbox `tools.curate(...)` via
* CurateService, or anything else awaiting under the same chain — observes the
* single snapshot value instead of re-reading `.brv/config.json` (which can
* race with mid-task user toggles).
*
* Same propagation pattern as `CurateResultCollector`
* (src/agent/infra/sandbox/curate-result-collector.ts): AsyncLocalStorage flows
* through the LLM streaming pipeline and the in-process sandbox, so callers
* without an explicit taskId still see the right value.
*
* Outside any scope, `getCurrentReviewDisabled()` returns `undefined` and the
* caller falls back to its own resolution path (currently a `.brv/config.json`
* read in `executeCurate`).
*/
const reviewDisabledStorage = new AsyncLocalStorage<boolean>()

export function runWithReviewDisabled<T>(reviewDisabled: boolean, fn: () => Promise<T>): Promise<T> {
return reviewDisabledStorage.run(reviewDisabled, fn)
}

export function getCurrentReviewDisabled(): boolean | undefined {
return reviewDisabledStorage.getStore()
}
71 changes: 59 additions & 12 deletions src/agent/infra/tools/implementations/curate-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {toSnakeCase} from '../../../../server/utils/file-helpers.js'
import {deriveImpactFromLoss, detectStructuralLoss} from '../../../core/domain/knowledge/conflict-detector.js'
import {resolveStructuralLoss} from '../../../core/domain/knowledge/conflict-resolver.js'
import {ToolName} from '../../../core/domain/tools/constants.js'
import {getCurrentReviewDisabled} from './curate-tool-task-context.js'

/**
* Called after each successful context file write so callers can
Expand Down Expand Up @@ -469,7 +470,13 @@ export interface CurateOutput {
* @param filePath - Absolute path to the context tree file being modified
* @param basePath - Context tree base path (e.g., '.brv/context-tree')
*/
async function backupBeforeWrite(filePath: string, basePath: string): Promise<void> {
async function backupBeforeWrite(filePath: string, basePath: string, reviewDisabled: boolean): Promise<void> {
// Honor `brv review --disable`: backups exist solely to support review rejection
// (restore from backup). With reviews disabled, they are dead state — skip creation
// so review-backups/ stays empty. Snapshot taken once at executeCurate top so all
// ops in this tool call observe a consistent value even if the user toggles mid-task.
if (reviewDisabled) return

try {
const brvDir = dirname(resolve(basePath))
const relativePath = relative(resolve(basePath), resolve(filePath))
Expand All @@ -487,6 +494,30 @@ async function backupBeforeWrite(filePath: string, basePath: string): Promise<vo
}
}

/**
* Type guard: narrows an unknown JSON value to a shape that may carry the
* `reviewDisabled` flag. Used as the fallback when the agent process has no
* AsyncLocalStorage scope (direct sandbox callers without a TaskExecute).
*/
function hasReviewDisabledField(value: unknown): value is {reviewDisabled?: unknown} {
return typeof value === 'object' && value !== null
}

/**
* Reads `<brvDir>/config.json` and returns the `reviewDisabled` flag.
* Returns false (review enabled) on any error so a missing/corrupt config never
* silently swallows backups that protect the rejection path.
*/
async function isReviewDisabledForBrvDir(brvDir: string): Promise<boolean> {
try {
const raw = await DirectoryManager.readFile(join(brvDir, 'config.json'))
const parsed: unknown = JSON.parse(raw)
return hasReviewDisabledField(parsed) && parsed.reviewDisabled === true
} catch {
return false
}
}

function generateDomainContextMarkdown(domainName: string, context: DomainContext): string {
const sections: string[] = [`# Domain: ${domainName}`, '', '## Purpose', context.purpose, '', '## Scope']

Expand Down Expand Up @@ -880,6 +911,7 @@ function maxImpact(
async function executeUpdate(
basePath: string,
operation: Operation,
reviewDisabled: boolean,
onAfterWrite?: WriteCallback,
runtimeSignalStore?: IRuntimeSignalStore,
logger?: ILogger,
Expand Down Expand Up @@ -990,7 +1022,7 @@ async function executeUpdate(
summary,
timestamps,
})
await backupBeforeWrite(contextPath, basePath)
await backupBeforeWrite(contextPath, basePath, reviewDisabled)
await DirectoryManager.writeFileAtomic(contextPath, contextContent)
onAfterWrite?.(contextPath, contextContent)

Expand Down Expand Up @@ -1031,6 +1063,7 @@ async function executeUpdate(
async function executeUpsert(
basePath: string,
operation: Operation,
reviewDisabled: boolean,
onAfterWrite?: WriteCallback,
runtimeSignalStore?: IRuntimeSignalStore,
logger?: ILogger,
Expand Down Expand Up @@ -1082,7 +1115,7 @@ async function executeUpsert(

if (exists) {
// File exists - delegate to UPDATE logic
const result = await executeUpdate(basePath, {...operation, type: 'UPDATE'}, onAfterWrite, runtimeSignalStore, logger)
const result = await executeUpdate(basePath, {...operation, type: 'UPDATE'}, reviewDisabled, onAfterWrite, runtimeSignalStore, logger)
// Return with UPSERT type but indicate it was an update
return {
...result,
Expand Down Expand Up @@ -1117,6 +1150,7 @@ async function executeUpsert(
async function executeMerge(
basePath: string,
operation: Operation,
reviewDisabled: boolean,
onAfterWrite?: WriteCallback,
runtimeSignalStore?: IRuntimeSignalStore,
logger?: ILogger,
Expand Down Expand Up @@ -1229,8 +1263,8 @@ async function executeMerge(
const previousSummary = targetParsedContent.summary

// Backup both files before merge modifies target and deletes source
await backupBeforeWrite(targetContextPath, basePath)
await backupBeforeWrite(sourceContextPath, basePath)
await backupBeforeWrite(targetContextPath, basePath, reviewDisabled)
await backupBeforeWrite(sourceContextPath, basePath, reviewDisabled)

// Capture source sidecar signals BEFORE any destructive operation so a
// mid-flow crash cannot leave the target unmerged with an orphaned
Expand Down Expand Up @@ -1321,6 +1355,7 @@ async function executeMerge(
async function executeDelete(
basePath: string,
operation: Operation,
reviewDisabled: boolean,
runtimeSignalStore?: IRuntimeSignalStore,
logger?: ILogger,
): Promise<OperationResult> {
Expand Down Expand Up @@ -1358,7 +1393,7 @@ async function executeDelete(
// Best-effort: summary extraction failure must never block delete
}

await backupBeforeWrite(filePath, basePath)
await backupBeforeWrite(filePath, basePath, reviewDisabled)
await DirectoryManager.deleteFile(filePath)
await deleteDerivedSiblings(filePath)

Expand Down Expand Up @@ -1417,7 +1452,7 @@ async function executeDelete(
// Best-effort: summary extraction failure must never block delete
}

await Promise.all(mdFiles.map((f) => backupBeforeWrite(f, basePath)))
await Promise.all(mdFiles.map((f) => backupBeforeWrite(f, basePath, reviewDisabled)))
await DirectoryManager.deleteTopicRecursive(fullPath)

// Dual-write: drop sidecar entries for every markdown file that was
Expand Down Expand Up @@ -1490,8 +1525,20 @@ export async function executeCurate(

const {basePath, operations} = parseResult.data

// Prefer the daemon-stamped value (snapshotted at task-create on the daemon side,
// forwarded via TaskExecute, opened as an AsyncLocalStorage scope in agent-process so
// it propagates through any async chain — direct tool call, sandbox `tools.curate(...)`
// via CurateService, or ingest-resource-tool). The `.brv/config.json` read is the
// fall-back for callers outside any scope. Sharing one value across daemon
// (CurateLogHandler) and agent (this tool) keeps reviewStatus and backups consistent:
// without it, a user toggle mid-task could mark ops as pending review (daemon snapshot
// held) while skipping backups (agent re-read picks up new value), making rejection
// un-restorable.
const scopedReviewDisabled = getCurrentReviewDisabled()
const reviewDisabled = scopedReviewDisabled ?? (await isReviewDisabledForBrvDir(dirname(resolve(basePath))))

const onAfterWrite: undefined | WriteCallback = abstractQueue
? (contextPath, content) => { abstractQueue.enqueue({contextPath, fullContent: content}) }
? (contextPath, content) => abstractQueue.enqueue({contextPath, fullContent: content})
: undefined

const applied: OperationResult[] = []
Expand All @@ -1516,31 +1563,31 @@ export async function executeCurate(
}

case 'DELETE': {
result = await executeDelete(basePath, operation, runtimeSignalStore, logger)
result = await executeDelete(basePath, operation, reviewDisabled, runtimeSignalStore, logger)

if (result.status === 'success') summary.deleted++

break
}

case 'MERGE': {
result = await executeMerge(basePath, operation, onAfterWrite, runtimeSignalStore, logger)
result = await executeMerge(basePath, operation, reviewDisabled, onAfterWrite, runtimeSignalStore, logger)

if (result.status === 'success') summary.merged++

break
}

case 'UPDATE': {
result = await executeUpdate(basePath, operation, onAfterWrite, runtimeSignalStore, logger)
result = await executeUpdate(basePath, operation, reviewDisabled, onAfterWrite, runtimeSignalStore, logger)

if (result.status === 'success') summary.updated++

break
}

case 'UPSERT': {
result = await executeUpsert(basePath, operation, onAfterWrite, runtimeSignalStore, logger)
result = await executeUpsert(basePath, operation, reviewDisabled, onAfterWrite, runtimeSignalStore, logger)

// UPSERT counts as either added or updated based on what happened
if (result.status === 'success') {
Expand Down
Loading
Loading