Skip to content

feat: [ENG-2933] add brv channel oclif command stubs + daemon round-t…#748

Merged
leehpham merged 1 commit into
proj/multi-agent-collabfrom
feat/ENG-2933
Jun 1, 2026
Merged

feat: [ENG-2933] add brv channel oclif command stubs + daemon round-t…#748
leehpham merged 1 commit into
proj/multi-agent-collabfrom
feat/ENG-2933

Conversation

@leehpham
Copy link
Copy Markdown
Member

@leehpham leehpham commented Jun 1, 2026

…rip client.

Summary

  • Problem:
  • Why it matters:
  • What changed:
  • What did NOT change (scope boundary):

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Documentation
  • Test
  • Chore (build, dependencies, CI)

Scope (select all touched areas)

  • TUI / REPL
  • Agent / Tools
  • LLM Providers
  • Server / Daemon
  • Shared (constants, types, transport events)
  • CLI Commands (oclif)
  • Hub / Connectors
  • Cloud Sync
  • CI/CD / Infra

Linked issues

  • Closes #
  • Related #

Root cause (bug fixes only, otherwise write N/A)

  • Root cause:
  • Why this was not caught earlier:

Test plan

  • Coverage added:
    • Unit test
    • Integration test
    • Manual verification only
  • Test file(s):
  • Key scenario(s) covered:

User-visible changes

List user-visible changes (including defaults, config, or CLI output).
If none, write None.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented above)
  • Branch is up to date with main

Risks and mitigations

List real risks for this PR. If none, write None.

  • Risk:
    • Mitigation:

@leehpham leehpham requested a review from bao-byterover as a code owner June 1, 2026 03:23
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Claude finished @leehpham's task in 3m 52s —— View job


Claude PR Review

  • Read changed files
  • Analyze correctness, security, code quality
  • Check adherence to CLAUDE.md guidelines (TDD, Outside-In, TS conventions)
  • Post inline comments for specific issues
  • Post summary review with score

Summary

This PR lands the brv channel oclif surface (7 subcommands) on top of a shared ChannelCommand base, a thin channel-client wrapper over withDaemonRetry, and a real in-process Socket.IO harness that exercises the full command → daemon → typed-error → render round-trip. The CLI stops short of real behavior on purpose — every event resolves to CHANNEL_NOT_IMPLEMENTED (enabled) or CHANNEL_DISABLED (disabled) — and the tests verify exactly that contract. Scope matches the commit title; no behavior leaks into other surfaces.

Strengths

  • praise: Clear separation of concerns — channel-client.ts owns the transport/error normalization seam, channel-command.ts owns the --json flag + dispatch/handleError, and each subcommand is a thin args/flags declaration. Easy to extend per-event once handlers gain behavior.
  • praise: test/integration/channel-command-surface.test.ts boots a real SocketIOTransportServer and registers handlers through the same channelsEnabled() gate production uses, then drives the actual oclif command classes — high-signal coverage for the wire shape.
  • praise: toChannelClientError is the right shape: preserves the code from TransportRequestError and falls back to formatConnectionError for connection/spawn failures, so daemon-thrown wire codes flow through unchanged.
  • praise: Both enabled=1 and enabled=0 surfaces are covered, including the schema-validation failure (CHANNEL_INVALID_REQUEST when @ prefix is missing) — confirms zod runs end-to-end.

Findings (see inline)

  • issue (formatting, non-blocking): 6 of 7 command files (invite.ts, list-turns.ts, list.ts, mention.ts, onboard.ts, show.ts) have public static description / examples / flags at column 0 instead of indented two spaces. Only new.ts is correctly indented. Will likely fail npm run lint. Run npm run lint:fix.
  • suggestion (low): channel-test-harness.ts has module-scoped nextPort and global process.env.* mutation — fine for Mocha sequential, brittle if --parallel is ever enabled. Consider binding to port 0 and noting "one harness per process" in a comment.
  • nitpick: The EVENT_SUFFIX_PATTERN strip in channel-client.ts:8/44 duplicates the same scrub in daemon-client.ts:234. One source of truth would prevent drift.
  • question: ChannelClient.disconnect() is part of the interface but has no current caller — likely reserved for the upcoming channel:subscribe long-lived stream. Worth confirming the intent.
  • thought: channelClientOptions is reassigned per-instance in tests; works today but a protected overrideable hook (or constructor-injected option) is the more conventional DI seam.

Conformance with CLAUDE.md

  • Outside-In: ✅ Each command (consumer) drives a single transport event; no premature domain entities were extracted. ChannelCommand base only fulfills what all seven consumers actually need.
  • TS conventions: ✅ No any, no ! non-null assertions, no as casts, ?? used appropriately, prefer-undefined honored. ChannelClient is an interface (behavioral contract); ChannelClientOptions is a type (data shape) — matches the guidance.
  • JSDoc voice: ✅ Third-person singular present-tense ("Normalizes…", "Connects…", "Registers a stub…"). No ephemeral milestone/issue references in comments.
  • Import boundary:oclif/ does not import from server/; the integration test does (it boots the daemon in-process), which is the intended testing pattern.
  • TDD: ⚠️ Cannot verify ordering from a single squashed commit, but the test surface is real and covers both the disabled and enabled paths.

Risks

Low. The wire surface is stubbed (CHANNEL_NOT_IMPLEMENTED/CHANNEL_DISABLED) and gated behind BRV_CHANNELS_ENABLED. No regressions for non-channel surfaces, and the new topics.channel entry in package.json is additive.

Overall score: 4 / 5

Solid skeleton with thoughtful tests; main blocker before merge is the indentation formatting on 6 command files (likely a one-shot lint:fix).
| Branch

@leehpham leehpham merged commit ef9c797 into proj/multi-agent-collab Jun 1, 2026
6 checks passed
Comment on lines +7 to +14
public static description = 'List channels'
public static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --archived --json',
]
public static flags = {
archived: Flags.boolean({description: 'Include archived channels'}),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (non-blocking, formatting): public static description, public static examples, and public static flags are flush against column 0 rather than indented two spaces as class members. Same pattern appears in invite.ts, list-turns.ts, mention.ts, onboard.ts, and show.ts — only new.ts is correctly indented. This will almost certainly fail npm run lint (Prettier) and reads awkwardly next to the indented args / run() members.

Suggested change
public static description = 'List channels'
public static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --archived --json',
]
public static flags = {
archived: Flags.boolean({description: 'Include archived channels'}),
}
public static description = 'List channels'
public static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --archived --json',
]
public static flags = {
archived: Flags.boolean({description: 'Include archived channels'}),
}

Fix this →

Comment on lines +19 to +44
let nextPort = 9740

/** A booted in-process channel daemon plus the seam a command connects through. */
export type ChannelHarness = {
/** Connector to inject into a command's `channelClientOptions()` (DI seam). */
connector: TransportConnector
/** Port the in-process transport server is bound to. */
port: number
/** Disconnects clients, stops the server, and restores mutated env. */
teardown: () => Promise<void>
}

/**
* Boots a real {@link SocketIOTransportServer} in-process and registers the
* channel handler surface exactly the way production does — gated through the
* real {@link channelsEnabled} check. With `enabled` the 10 per-event handlers
* are wired (so a `channel:*` request resolves to `CHANNEL_NOT_IMPLEMENTED`);
* without it, {@link registerDisabledStubs} is used (resolving to
* `CHANNEL_DISABLED`). The returned `connector` drives the actual oclif command
* against this server with no subprocess and no daemon spawn.
*/
export async function startChannelHarness(options: {enabled: boolean; port?: number}): Promise<ChannelHarness> {
const previousSessionLog = process.env.BRV_SESSION_LOG
const previousChannelsFlag = process.env.BRV_CHANNELS_ENABLED
process.env.BRV_SESSION_LOG = '/dev/null'
process.env.BRV_CHANNELS_ENABLED = options.enabled ? '1' : '0'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (low): Two pieces of shared global state make this harness brittle to parallel execution:

  1. let nextPort = 9740 at module scope is allocated per-process. With Mocha's default sequential mode this is fine; if anyone later flips on --parallel, two workers would each start at 9740 and collide.
  2. process.env.BRV_SESSION_LOG and process.env.BRV_CHANNELS_ENABLED are mutated globally with the previous value snapshotted on startChannelHarness. If two harnesses overlap in the same process, the second snapshot captures the first harness's value, and teardown order can leak state into adjacent tests.

For (1), await server.start(0) and read server.address()?.port (or whatever the equivalent on SocketIOTransportServer is) to let the OS pick a free port. For (2), worth a comment in the harness noting "single-harness-at-a-time per process" so a future contributor doesn't combine harnesses inside one describe.

Neither is blocking — just guardrails for the next person.

Comment on lines +5 to +48
/** Fallback wire code when a thrown error carries no channel-specific code. */
const CHANNEL_REQUEST_FAILED = 'CHANNEL_REQUEST_FAILED'
/** TransportRequestError appends this suffix to its message; strip it for display. */
const EVENT_SUFFIX_PATTERN = / for event '[^']+'$/

/** Daemon options a channel command forwards to the shared transport layer. */
export type ChannelClientOptions = Pick<
DaemonClientOptions,
'maxRetries' | 'projectPath' | 'projectRootFlag' | 'retryDelayMs' | 'transportConnector'
>

/** A channel-domain error surfaced to the CLI, carrying the wire `code`. */
export class ChannelClientError extends Error {
public readonly code: string
public readonly details?: unknown

public constructor(code: string, message: string, details?: unknown) {
super(message)
this.name = 'ChannelClientError'
this.code = code
this.details = details
}
}

/** Minimal channel request seam over the daemon transport. */
export interface ChannelClient {
disconnect(): Promise<void>
request<TResponse = unknown>(event: string, payload?: unknown): Promise<TResponse>
}

/**
* Normalizes any thrown transport/daemon error into a {@link ChannelClientError}.
* A daemon-thrown channel error arrives as a {@link TransportRequestError} whose
* `code` is forwarded verbatim from the `{success, code, error}` ack envelope;
* connection/spawn failures fall back to {@link formatConnectionError}.
*/
export function toChannelClientError(error: unknown): ChannelClientError {
if (error instanceof ChannelClientError) return error
if (error instanceof TransportRequestError) {
return new ChannelClientError(error.code ?? CHANNEL_REQUEST_FAILED, error.message.replace(EVENT_SUFFIX_PATTERN, ''))
}

return new ChannelClientError(CHANNEL_REQUEST_FAILED, formatConnectionError(error))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The / for event '[^']+'$/ regex and the "strip the event suffix from a TransportRequestError" logic also live in src/oclif/lib/daemon-client.ts:234. Two copies of the same scrub rule will eventually drift. Consider exporting the pattern (or a stripEventSuffix(msg) helper) from daemon-client.ts and reusing it here — daemon-client.ts is already the source of formatConnectionError, which is the natural home.

Not a blocker; just one source of truth.

Comment on lines +30 to +33
export interface ChannelClient {
disconnect(): Promise<void>
request<TResponse = unknown>(event: string, payload?: unknown): Promise<TResponse>
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: disconnect() is exposed on the ChannelClient interface but isn't called anywhere in the current PR — withChannelClient delegates cleanup to withDaemonRetry, and there are no callers of connectChannelClient yet. Is it intentionally kept for the M0-2 callers (e.g. the channel subscribe long-lived stream that isn't wired through oclif yet)? If so, fine to leave; if not, dropping it until a consumer exists would tighten the surface (per CLAUDE.md Outside-In: "Only what the consumer actually requires").

Comment on lines +22 to +24
public channelClientOptions(): ChannelClientOptions {
return {projectPath: process.cwd()}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: Making channelClientOptions() a regular public method that the integration test reassigns on the instance (command.channelClientOptions = () => (...) in test/integration/channel-command-surface.test.ts:69) works because the call site is this.channelClientOptions() and the method isn't bound — but instance reassignment of class methods is a slightly unusual seam and easy to miss when refactoring (e.g. if anyone ever converts this to an arrow property or destructures it).

Two more conventional alternatives:

  • Make it protected and have tests use a tiny subclass override.
  • Accept a channelClientOptions?: ChannelClientOptions via the constructor or a setChannelClientOptions(opts) setter, and remove the method-as-seam.

Functional today; just noting it for the next person who has to extend the base.

Comment on lines +114 to +168
describe('enabled surface (BRV_CHANNELS_ENABLED=1)', () => {
let harness: ChannelHarness

beforeEach(async () => {
harness = await startChannelHarness({enabled: true})
})

afterEach(async () => {
await harness.teardown()
})

for (const {argv, commandClass, label} of COMMAND_CASES) {
it(`\`channel ${label}\` builds a schema-valid request that reaches CHANNEL_NOT_IMPLEMENTED`, async () => {
const {exitCode, logs} = await runInProcess(commandClass, config, harness.connector, argv)

expect(exitCode, label).to.equal(1)
const parsed = parseEnvelope(logs)
expect(isErrorEnvelope(parsed), label).to.equal(true)
if (isErrorEnvelope(parsed)) {
expect(parsed.code, label).to.equal('CHANNEL_NOT_IMPLEMENTED')
}
})
}

it('emits a `{success:false, code, error}` envelope on stdout in JSON mode', async () => {
const {exitCode, logs} = await runInProcess(ChannelNew, config, harness.connector, ['x', '--json'])

expect(exitCode).to.equal(1)
const parsed = parseEnvelope(logs)
expect(isErrorEnvelope(parsed)).to.equal(true)
if (isErrorEnvelope(parsed)) {
expect(parsed.code).to.equal('CHANNEL_NOT_IMPLEMENTED')
expect(parsed.success).to.equal(false)
expect(parsed.error).to.have.length.greaterThan(0)
}
})

it('renders a [CODE] message to stderr in text mode', async () => {
const {exitCode, stderrLogs} = await runInProcess(ChannelNew, config, harness.connector, ['x'])

expect(exitCode).to.equal(1)
expect(stderrLogs.join('\n')).to.include('[CHANNEL_NOT_IMPLEMENTED]')
})

it('surfaces CHANNEL_INVALID_REQUEST when an invited handle is missing the @ prefix', async () => {
const {exitCode, logs} = await runInProcess(ChannelInvite, config, harness.connector, ['my-channel', 'codex', '--json'])

expect(exitCode).to.equal(1)
const parsed = parseEnvelope(logs)
expect(isErrorEnvelope(parsed)).to.equal(true)
if (isErrorEnvelope(parsed)) {
expect(parsed.code).to.equal('CHANNEL_INVALID_REQUEST')
}
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Nice end-to-end coverage here — booting a real SocketIOTransportServer, registering the production handler set behind the real channelsEnabled() gate, and driving the actual oclif command class (no mocks, no subprocess) is exactly the kind of test that catches transport-shape regressions. The schema-failure case (CHANNEL_INVALID_REQUEST when @ is missing) is also a great smoke check that the zod schemas are actually wired through. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant