Skip to content

feat: OpenCode 런타임 통합 + 모델 분산 실행 - #3

Open
hjshin-ubob wants to merge 4 commits into
sigco3111:mainfrom
hjshin-ubob:main
Open

feat: OpenCode 런타임 통합 + 모델 분산 실행#3
hjshin-ubob wants to merge 4 commits into
sigco3111:mainfrom
hjshin-ubob:main

Conversation

@hjshin-ubob

@hjshin-ubob hjshin-ubob commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • OpenCode 어댑터 수정: opencode run 서브커맨드 사용, 기존에 잘못된 --agent/-p 플래그 제거
  • 동시성 큐잉: 슬롯 부족 시 throw 대신 acquire() 큐에 대기하여 무한 재시도 루프 방지
  • 모델 분산 설정: setup 마법사에서 OpenCode 선택 시 4개 GLM 모델에 6개 에이전트 분산 배치
  • config 스키마 유연화: opencode 프로바이더에서 api.baseUrl 불필요 (model만 지정 가능)
  • README 갱신: v0.4.0 상태, 모델 분산 전략, Quick Start 업데이트

Changes

Commit 1: b0caff6 — fix: OpenCode adapter

  • opencode --agent '...' -p '...'opencode run --dangerously-skip-permissions '...'
  • 시스템 프롬프트 + 태스크 지시를 메시지로 결합

Commit 2: 4e703fc — fix: concurrency queuing

  • startAgent()의 pre-check throw 제거 → acquire() 큐잉으로 대기
  • api.baseUrl, api.model optional화 (opencode는 model만 필요)
  • 동시성 테스트를 "throw 기대" → "큐잉 후 슬롯 해제 시 실행"으로 업데이트

Commit 3: de424a7 — feat: model-diverse setup

  • 에이전트별 GLM 모델 분산: CTO=glm-5.1, CEO/PO=glm-4.7, Designer=glm-4.6v, QA/Marketer=glm-4.6
  • README v0.4.0 갱신

Test Plan

  • 145/145 tests pass (npm test)
  • Build clean (npm run build)
  • CEO 에이전트 opencode run으로 정상 실행 확인
  • 동시성 큐잉: CEO 실행 중 PO 디스패치 시 큐 대기 후 정상 실행
  • 모델 분산: PO 로그에 glm-4.7 표시 확인

Ultraworked with Sisyphus

Summary by CodeRabbit

  • New Features

    • Multi-provider configuration supporting Claude Code, OpenCode, ZAI API, and mixed provider modes
    • OpenCode runtime integration with model-distributed parallel execution
    • Interactive provider selection in setup wizard
    • Concurrency limiting and task queuing per model to optimize agent execution
    • Provider and model tracking in dashboard API responses
  • Improvements

    • ZAI API account or billing no longer required to run agents
    • Environment variable substitution for API keys and configuration values

hjshin-ubob and others added 4 commits April 20, 2026 14:49
Wire provider adapters (Claude Code / OpenCode / HTTP API) into
SessionManager, PromptBuilder, Dashboard API, and setup wizard.

- config.ts: env var interpolation (${VAR}), provider validation,
  getProviderForAgent/getDefaultProvider helpers
- session-manager.ts: provider routing in startAgent/ephemeral/meeting,
  ConcurrencyLimiter integration with slot tracking
- prompt-builder.ts: EnhancedAgentSkill instruction rendering for
  non-CLI providers, slash commands preserved for Claude Code
- dashboard/server.ts: provider summary in /api/status
- setup.ts + prompts.ts: ZAI provider setup wizard
- Delete duplicate providers/provider-adapter.ts interface
- Fix adapter imports to use canonical ../provider-adapter.js
- 50 new tests (145 total, 0 failures)
Previous adapter used invalid flags (--agent with system prompt string, -p shorthand).

New adapter uses `opencode run --dangerously-skip-permissions 'message'` with

system prompt + task instruction combined into the message argument.

Model is specified via --model when configured.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Previously startAgent() threw when concurrency slots were full, causing

infinite retry loops in the orchestrator. Now acquire() queues tasks

internally and waits for a slot to open.

Also makes api.baseUrl and api.model optional in schemas since opencode

providers only need api.model, not a full HTTP endpoint.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Setup wizard now generates 4 opencode providers with different GLM models:

CTO=glm-5.1, CEO/PO=glm-4.7, Designer=glm-4.6v, QA/Marketer=glm-4.6.

Each model has independent concurrency slots so 6 agents run in parallel.

No ZAI API key or credits required — runs entirely on opencode coding plan.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR integrates multi-provider support with configuration-driven provider selection during setup, replaces monolithic provider-adapter interfaces with distributed adapter implementations, introduces concurrency limiting by model/provider in SessionManager, and adds provider/model tracking to sessions and dashboard status APIs.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added v0.4.0 status section describing OpenCode runtime integration, model-distributed parallel execution, and setup wizard provider selection; updated architecture diagram and provider examples; removed ZAI rate-limit section.
CLI Setup & Prompts
src/cli/commands/setup.ts, src/cli/utils/prompts.ts
Added provider-selection prompt flow with ZAI API key and model collection; implemented multi-provider configuration branches (claude-code, opencode, zai-api, mixed variants) with agent-provider mapping and concurrency rules.
Configuration Utilities
src/core/project/config.ts, src/core/session/provider-adapter.ts
Added exported helpers: resolveEnvVars, validateProviderConfig, getProviderForAgent, getDefaultProvider; made api.baseUrl and api.model optional in provider schema; added environment variable resolution in loadConfig.
Provider Adapter Refactoring
src/core/session/providers/provider-adapter.ts
Deleted file containing exported ProviderConfig, BuildCommandResult, McpServerConfig, ProviderAdapter interface definitions (moved to individual adapter implementations).
Adapter Implementations
src/core/session/providers/claude-code-adapter.ts, src/core/session/providers/http-api-adapter.ts, src/core/session/providers/opencode-adapter.ts
Updated import paths (./provider-adapter.js../provider-adapter.js); changed OpenCode adapter to generate opencode run commands with optional --model flag and consolidated argument building via buildRunCommand helper; removed unconditional default model.
Session Management & Concurrency
src/core/session/session-manager.ts
Added provider-aware session command construction via adapter path fallback; introduced ConcurrencyLimiter with model/provider-specific limits; added concurrency slot acquisition/release lifecycle; updated stopAgent to resolve slot resolvers; added configureConcurrency, toAdapterConfig, buildMcpConfigObject helpers.
Prompt Building
src/core/agent/prompt-builder.ts
Extended buildSkillsSection with optional providerType parameter; added hasInstruction type guard; implemented branching for slash-command rendering (claude-code/undefined) vs. instruction-based rendering (other providers) with conditional "지침:" blocks.
Dashboard API
src/dashboard/server.ts
Added providers field to /api/status response computed from running agents, counting occurrences per provider with 'claude-code' default for missing provider values.
Unit Tests
tests/unit/cli/setup-provider.test.ts, tests/unit/core/config-zai.test.ts, tests/unit/core/concurrency-limiter.test.ts, tests/unit/core/prompt-builder-skills.test.ts, tests/unit/core/provider-factory.test.ts, tests/unit/core/session-manager-provider.test.ts
Added comprehensive test coverage for provider config validation, environment variable resolution, concurrency limiting by model/provider, provider-aware prompt building, adapter factory behavior, and SessionManager provider/concurrency integration.
E2E Test
tests/e2e/dashboard-api.e2e.test.ts
Added E2E test starting ephemeral CTO session and verifying /api/status includes providers field and /api/agents returns session with provider/model metadata.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI Setup
    participant PM as ProjectManager
    participant SM as SessionManager
    participant PF as ProviderFactory
    participant PA as ProviderAdapter
    participant CL as ConcurrencyLimiter
    participant Tmux as Tmux

    CLI->>PM: saveConfig(multi-provider config)
    PM->>PM: resolveEnvVars(apiKey)
    PM->>PM: validateProviderConfig()
    
    Note over CLI,PM: startAgent(agent role)
    SM->>PM: loadConfig()
    SM->>PM: getProviderForAgent(role)
    PM-->>SM: ProviderConfigType
    
    SM->>PF: createProviderAdapter(providerConfig)
    PF->>PA: return adapter instance
    PA-->>SM: ProviderAdapter
    
    SM->>SM: toAdapterConfig(providerConfig)
    SM->>PA: buildCommand(systemPrompt, ...)
    PA-->>SM: { command, useScriptFile }
    
    SM->>CL: configureConcurrency(config.concurrency.rules)
    SM->>CL: acquire(model, provider)
    CL-->>SM: slot acquired / queued
    
    SM->>Tmux: new-session(command)
    Tmux-->>SM: AgentSession { provider, model }
    
    Note over SM,CL: Session running...
    
    SM->>SM: stopAgent(role)
    SM->>Tmux: kill-session
    SM->>CL: release(slot)
    CL-->>SM: slot released, queue processed
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • fix: add missing provider-adapter.ts interface #1 — Removes exported provider-adapter interfaces while this PR deletes the provider-adapter.ts file entirely; these directly touch the same type contract and interface deletions.
  • feat: ZAI multi-provider runtime integration #2 — Makes identical code-level changes: adds provider-selection prompts, extends config helpers, modifies PromptBuilder with providerType parameter, integrates provider adapters and concurrency into SessionManager, and adds matching test coverage.

Poem

🐰 Hops through providers with glee,
Models distributed, concurrency free,
OpenCode runs at the prompts' delight,
Sessions now queue when they might,
A multi-patch warren, crisp and tight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: OpenCode 런타임 통합 + 모델 분산 실행' accurately and concisely summarizes the two main features: OpenCode runtime integration and model distribution, which align with the primary changes across the PR including adapter updates, concurrency queuing, and multi-provider configuration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/core/agent/prompt-builder.ts (2)

25-30: ⚠️ Potential issue | 🟠 Major

Move the language rule before the agent template.

The working-language rule is currently pushed after agent.system_prompt_template, so it is not the topmost instruction.

Proposed fix
-    // 에이전트 기본 프롬프트
-    parts.push(agent.system_prompt_template);
-
     // 언어 룰 — 가장 먼저 주입 (모든 출력 형식의 기본 전제)
     parts.push(this.buildLanguageRule(config));
+
+    // 에이전트 기본 프롬프트
+    parts.push(agent.system_prompt_template);

As per coding guidelines, “All agent system prompts must include the working language from config.localization at the topmost position”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/agent/prompt-builder.ts` around lines 25 - 30, The language rule is
pushed after the agent template so it isn't the topmost system instruction;
update the assembly order in the prompt builder (where
parts.push(agent.system_prompt_template) and
parts.push(this.buildLanguageRule(config)) are called) so that
this.buildLanguageRule(config) is pushed before agent.system_prompt_template,
ensuring the working language from config.localization is the first system-level
prompt element.

13-23: ⚠️ Potential issue | 🟠 Major

Thread providerType through buildSystemPrompt() and update session call sites.

The new non-CLI skill formatting (full instructions instead of slash-commands) is only reachable by calling buildSkillsSection() with providerType set; production code always calls it without that parameter, so OpenCode/HTTP/AICP agents incorrectly receive slash-command instructions instead of formatted instructions.

Add providerType?: string to the buildSystemPrompt() params object and pass it to buildSkillsSection(agent, providerType) at line 42. Then update all three session call sites to determine the provider type before building the system prompt:

  1. Line 139 (startAgent): Call getProviderForAgent(config, agent.role) before buildSystemPrompt(), extract adapter.type, and pass it.
  2. Line 470 (startEphemeralAgent): Same pattern—determine provider type first, pass to buildSystemPrompt().
  3. Line 606 (startMeetingSession): Same pattern—determine provider type first, pass to buildSystemPrompt().

This requires reordering the logic at each site so getProviderForAgent()createProviderAdapter() → extract .type happens before the system prompt is built, not after.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/agent/prompt-builder.ts` around lines 13 - 23, Add an optional
providerType?: string to buildSystemPrompt(params) and forward it into
buildSkillsSection(agent, providerType) inside buildSystemPrompt; then update
each session starter (startAgent, startEphemeralAgent, startMeetingSession) to
determine the provider type before building the system prompt by calling
getProviderForAgent(config, agent.role), creating the provider adapter
(createProviderAdapter or equivalent) and extracting adapter.type, then pass
that type into buildSystemPrompt; reorder the existing logic so provider
lookup/adapter creation → const providerType = adapter.type →
buildSystemPrompt({... , providerType }) happens before any prompt construction
or session creation.
src/core/session/session-manager.ts (2)

130-135: ⚠️ Potential issue | 🟠 Major

Use the required ip-<role> tmux session name.

Persistent agent sessions are still created and looked up with the bare role name. This violates the one-session-per-role contract and can make role matching ambiguous. Use ip-${agent.role} consistently for hasSession(), createTmuxSession(), killSession(), and log names. As per coding guidelines, Each role can have only one active tmux session at a time - session names must follow ip-<role> pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/session-manager.ts` around lines 130 - 135, The code uses
the bare role as the tmux session name; change it to the required ip-<role>
pattern by prefixing the role with "ip-" wherever session names are constructed
or looked up (e.g., the sessionName variable used with this.tmux.hasSession and
this.activeSessions.get), and ensure the same ip-<role> string is used when
calling createTmuxSession(), killSession(), and in any log messages; update all
references so session naming is consistent and enforces one-session-per-role.

325-328: ⚠️ Potential issue | 🟠 Major

Release limiter slots when stopping all sessions.

stopAll() kills tmux sessions but does not resolve slotResolvers, so provider execute() promises can remain pending and ConcurrencyLimiter running counts may never release. Resolve and clear slot/create-complete resolvers in a finally block.

Minimal fix
   async stopAll(): Promise<void> {
-    await this.tmux.killAllSessions();
-    this.activeSessions.clear();
+    try {
+      await this.tmux.killAllSessions();
+    } finally {
+      for (const resolve of this.slotResolvers.values()) {
+        resolve();
+      }
+      this.slotResolvers.clear();
+      this.createCompleteResolvers.clear();
+      this.activeSessions.clear();
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/session-manager.ts` around lines 325 - 328, stopAll()
currently kills tmux sessions via tmux.killAllSessions() and clears
activeSessions but does not resolve pending
slotResolvers/createCompleteResolvers, leaving ConcurrencyLimiter.execute()
promises unresolved and counts stuck; update stopAll() to, in a finally block
after killing sessions, iterate and call each resolver in slotResolvers and
createCompleteResolvers (resolving with an appropriate error or undefined), then
clear those maps/arrays so pending execute() callers are settled and the
limiter's counts can release; reference stopAll(), tmux.killAllSessions(),
slotResolvers, createCompleteResolvers, and ConcurrencyLimiter.execute() when
making the change.
🧹 Nitpick comments (2)
tests/unit/cli/setup-provider.test.ts (1)

94-107: Validate the real concurrency rules instead of a copied list.

This test only checks local fixtures, so changes to createZaiDefaultConfig().concurrency.rules can introduce an invalid regex without failing this suite.

🧪 Proposed test improvement
-import { createDefaultConfig, validateProviderConfig } from '../../../src/core/project/config.js';
+import { createDefaultConfig, createZaiDefaultConfig, validateProviderConfig } from '../../../src/core/project/config.js';
@@
   it('concurrency rules have valid regex patterns', () => {
-    const rules = [
-      { model: 'glm-5-turbo', limit: 1 },
-      { model: 'glm-5', limit: 2 },
-      { model: 'glm-5\\.1', limit: 1 },
-      { model: 'glm-4\\.5$', limit: 10 },
-      { model: 'glm-4-plus', limit: 20 },
-    ];
+    const rules = createZaiDefaultConfig().concurrency.rules;
 
     for (const rule of rules) {
       expect(() => new RegExp(rule.model)).not.toThrow();
       expect(rule.limit).toBeGreaterThan(0);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/cli/setup-provider.test.ts` around lines 94 - 107, The test
currently validates a hard-coded rules array; instead load and validate the real
rules from createZaiDefaultConfig().concurrency.rules: call
createZaiDefaultConfig(), get its concurrency.rules, then for each rule assert
that new RegExp(rule.model) does not throw and that rule.limit is > 0 (mirror
the existing checks). Update the spec in tests/unit/cli/setup-provider.test.ts
to reference createZaiDefaultConfig() and iterate its .concurrency.rules so any
invalid regexes in the actual config fail the test.
src/cli/utils/prompts.ts (1)

114-129: Collapse the duplicate ProviderSetupAnswers declaration.

TypeScript will merge these two interfaces, but keeping both makes future edits error-prone. Keep the second declaration (which includes the providerChoice property) and remove the first one.

Proposed cleanup
-export interface ProviderSetupAnswers {
-  useZaiProvider: boolean;
-  apiKey?: string;
-  codingModel?: string;
-  generalModel?: string;
-}
-
 export type ProviderChoice = 'claude-code' | 'opencode' | 'zai-api' | 'mixed-opencode-zai' | 'mixed-claude-zai';
 
 export interface ProviderSetupAnswers {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/utils/prompts.ts` around lines 114 - 129, There are two declarations
of the interface ProviderSetupAnswers causing an accidental merge; remove the
first (simpler) declaration and keep the second one that includes providerChoice
so ProviderSetupAnswers (and usages) only exist once alongside the
ProviderChoice type; update imports/usages if any tooling flagged the duplicate
after removal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 198: The fenced code block in README.md is missing a language specifier
which triggers markdownlint; update the opening triple-backtick for the ASCII
architecture diagram (the code fence that currently starts with ```) to include
the language tag text (i.e., change ``` to ```text) so the block is marked as
plain text for the diagram.

In `@src/cli/commands/setup.ts`:
- Around line 47-49: When the user selects 'claude-code' the setup currently
only sets config.default_provider but leaves any existing
config.agent_providers.mapping in place, causing stale provider routing; update
the setup branch that handles choice === 'claude-code' (the code that sets
config.default_provider) to also clear or reset config.agent_providers.mapping
(e.g., assign an empty object or remove mappings) so that agent provider
resolution no longer prefers old ZAI/OpenCode entries.
- Around line 120-138: The mixed "opencode" provider lacks an explicit model, so
update config.providers for the 'opencode' entry to include an api.model (mirror
the coding model used for 'zai-coding') so the adapter can emit the model flag
and the session/concurrency layer can track CTO by model; specifically modify
the config.providers 'opencode' object (the one referenced by
agent_providers.mapping's 'cto') to include an api.model set from
providerAnswers.codingModel ?? 'glm-5-turbo' (or the project’s default coding
model).

In `@src/core/project/config.ts`:
- Around line 127-136: The getProviderForAgent function currently returns early
for missing/default or 'claude-code' before honoring role-specific overrides;
change the logic in getProviderForAgent so that you first check
config.agent_providers?.overrides[role] and return it if present (overrides →
mapping → default), then resolve providerName from mapping or default and only
then apply the guard that returns null for missing or 'claude-code'; update
references to mapping, overrides, providerName, and config.providers
accordingly.

In `@src/core/session/providers/opencode-adapter.ts`:
- Around line 77-80: The code pushes this.model directly into the shell argument
array (parts) which can allow whitespace or shell metacharacters to break or
inject into the executed command; update the logic around where
parts.push('--model', this.model) is used (the code that builds the parts array
in the Opencode adapter) to ensure the model value is safely shell-quoted or
escaped before being appended (e.g., use a robust escaping/quoting helper or
wrap the model in a safe-quote function) so the resulting argument is treated as
a single literal token by the shell.

In `@src/core/session/session-manager.ts`:
- Around line 195-215: The code passes mcpConfig (built by buildMcpConfigObject)
into adapter.buildCommand but the OpenCode provider ignores it, causing agents
with required_mcp_tools to start without required tooling; update the OpenCode
adapter's buildCommand implementation to accept and honor the mcpConfig
parameter (inject required_mcp_tools into the generated command/environment or
file mounts) or, before calling adapter.buildCommand from the session manager,
validate that agent.required_mcp_tools are satisfied by the adapter and emit a
clear error or reject the launch (use symbols mcpConfig, buildMcpConfigObject,
adapter.buildCommand, and the OpenCode adapter implementation) so launches
either include the MCP wiring or fail fast with a warning.
- Around line 620-642: The meeting-path currently sets commandToUse and
sessionProvider but never sets the session/model value from provider-backed
adapters; fetch adapterConfig.api?.model (like startAgent/startEphemeralAgent
do) and assign it to the session's model variable before returning/creating the
session so provider-backed meetings report the model; update the code inside the
adapter.isCLIBased branch (and the equivalent non-CLI branch around the 678-684
region) to mirror the model assignment logic used by startAgent() and
startEphemeralAgent(), using adapterConfig.api?.model as the source.
- Around line 42-50: configureConcurrency currently only adds 'http-api' rules
via concurrencyLimiter.addRule and never reads providers.*.maxConcurrency,
causing mismatch with acquire (which uses adapter.type + api.model) and leading
to missing or duplicate rules when startAgent calls it repeatedly; update
configureConcurrency to iterate configured providers and their maxConcurrency,
build rules keyed by adapter.type and api.model (the same keys used in acquire),
and replace or load rules atomically (e.g., clear existing provider/model rules
before adding) so rules are created once per provider+model and avoid
accumulating stale duplicates; ensure the implementation references
configureConcurrency, acquire, concurrencyLimiter.addRule, and startAgent to
keep keys consistent.
- Around line 241-245: The createComplete Promise can hang if
this.concurrencyLimiter.acquire(...) rejects (e.g., adapter.buildCommand() or
createTmuxSession() throws) because createComplete is never rejected; update
startAgent to propagate acquirePromise rejections into createComplete by either
wiring acquirePromise.catch(err => rejectCreateComplete(err)) when creating
createComplete, or replace await createComplete with awaiting
Promise.race([createComplete, acquirePromise]) so any rejection from
this.concurrencyLimiter.acquire(providerKey, modelKey, agent.role, execute) will
reject startAgent; reference the symbols acquirePromise, createComplete,
this.concurrencyLimiter.acquire, execute, adapter.buildCommand,
createTmuxSession, and startAgent when making the change.
- Around line 223-225: The provider path currently calls getLogDir() and
createTmuxSession() before this.projectRoot is assigned, causing logs to be
written to the process CWD and preventing the script-file fallback for very long
tmux commands; to fix, assign this.projectRoot = projectRoot immediately after
computing projectRoot and before invoking the provider.execute() branch so
getLogDir() reflects the correct root, and ensure the logic in
createTmuxSession() (or the code that builds fullCmd) follows the existing
guideline to split tmux command arguments > ~16KB into a shell script and
execute via bash /path rather than inlining a huge command string.

In `@tests/e2e/dashboard-api.e2e.test.ts`:
- Around line 157-195: The test currently never asserts provider/model and a
blank providers object passes; update the test to (1) configure the session to
use MockTmuxAdapter/explicit CLI provider and model before calling
sessionManager.startEphemeralAgent (so startEphemeralAgent receives known
provider/model values), (2) assert on the returned session from
startEphemeralAgent and the runningSession derived from agentsBody that
session.provider and session.model (and runningSession.provider and
runningSession.model) are defined and equal to the expected values, and (3)
tighten the /api/status assertion to check that statusBody.providers contains
the expected provider key(s) and non-empty model info rather than only using
toBeDefined().

In `@tests/unit/core/session-manager-provider.test.ts`:
- Around line 315-333: The test currently asserts a Claude CLI fallback even
when configWithProvider() sets an HTTP provider; update the assertions in the
test that call SessionManager.startEphemeralAgent (and the similar test at lines
390-409) to expect the HTTP adapter command produced by the HTTP provider rather
than checking for 'claude'. Locate usage of SessionManager (constructor/instance
sm), MockTmuxAdapter.createCalls and the variable cmd in these tests and replace
the expect(cmd).toContain('claude') checks with an assertion that verifies the
HTTP adapter command (e.g., check for the HTTP adapter executable or URL pattern
that the HTTP provider should use based on configWithProvider()).

---

Outside diff comments:
In `@src/core/agent/prompt-builder.ts`:
- Around line 25-30: The language rule is pushed after the agent template so it
isn't the topmost system instruction; update the assembly order in the prompt
builder (where parts.push(agent.system_prompt_template) and
parts.push(this.buildLanguageRule(config)) are called) so that
this.buildLanguageRule(config) is pushed before agent.system_prompt_template,
ensuring the working language from config.localization is the first system-level
prompt element.
- Around line 13-23: Add an optional providerType?: string to
buildSystemPrompt(params) and forward it into buildSkillsSection(agent,
providerType) inside buildSystemPrompt; then update each session starter
(startAgent, startEphemeralAgent, startMeetingSession) to determine the provider
type before building the system prompt by calling getProviderForAgent(config,
agent.role), creating the provider adapter (createProviderAdapter or equivalent)
and extracting adapter.type, then pass that type into buildSystemPrompt; reorder
the existing logic so provider lookup/adapter creation → const providerType =
adapter.type → buildSystemPrompt({... , providerType }) happens before any
prompt construction or session creation.

In `@src/core/session/session-manager.ts`:
- Around line 130-135: The code uses the bare role as the tmux session name;
change it to the required ip-<role> pattern by prefixing the role with "ip-"
wherever session names are constructed or looked up (e.g., the sessionName
variable used with this.tmux.hasSession and this.activeSessions.get), and ensure
the same ip-<role> string is used when calling createTmuxSession(),
killSession(), and in any log messages; update all references so session naming
is consistent and enforces one-session-per-role.
- Around line 325-328: stopAll() currently kills tmux sessions via
tmux.killAllSessions() and clears activeSessions but does not resolve pending
slotResolvers/createCompleteResolvers, leaving ConcurrencyLimiter.execute()
promises unresolved and counts stuck; update stopAll() to, in a finally block
after killing sessions, iterate and call each resolver in slotResolvers and
createCompleteResolvers (resolving with an appropriate error or undefined), then
clear those maps/arrays so pending execute() callers are settled and the
limiter's counts can release; reference stopAll(), tmux.killAllSessions(),
slotResolvers, createCompleteResolvers, and ConcurrencyLimiter.execute() when
making the change.

---

Nitpick comments:
In `@src/cli/utils/prompts.ts`:
- Around line 114-129: There are two declarations of the interface
ProviderSetupAnswers causing an accidental merge; remove the first (simpler)
declaration and keep the second one that includes providerChoice so
ProviderSetupAnswers (and usages) only exist once alongside the ProviderChoice
type; update imports/usages if any tooling flagged the duplicate after removal.

In `@tests/unit/cli/setup-provider.test.ts`:
- Around line 94-107: The test currently validates a hard-coded rules array;
instead load and validate the real rules from
createZaiDefaultConfig().concurrency.rules: call createZaiDefaultConfig(), get
its concurrency.rules, then for each rule assert that new RegExp(rule.model)
does not throw and that rule.limit is > 0 (mirror the existing checks). Update
the spec in tests/unit/cli/setup-provider.test.ts to reference
createZaiDefaultConfig() and iterate its .concurrency.rules so any invalid
regexes in the actual config fail the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: feb729c7-6d9f-412c-8e2c-5767664d63de

📥 Commits

Reviewing files that changed from the base of the PR and between c157a4b and de424a7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • README.md
  • src/cli/commands/setup.ts
  • src/cli/utils/prompts.ts
  • src/core/agent/prompt-builder.ts
  • src/core/project/config.ts
  • src/core/session/provider-adapter.ts
  • src/core/session/providers/claude-code-adapter.ts
  • src/core/session/providers/http-api-adapter.ts
  • src/core/session/providers/opencode-adapter.ts
  • src/core/session/providers/provider-adapter.ts
  • src/core/session/session-manager.ts
  • src/dashboard/server.ts
  • tests/e2e/dashboard-api.e2e.test.ts
  • tests/unit/cli/setup-provider.test.ts
  • tests/unit/core/concurrency-limiter.test.ts
  • tests/unit/core/config-zai.test.ts
  • tests/unit/core/prompt-builder-skills.test.ts
  • tests/unit/core/provider-factory.test.ts
  • tests/unit/core/session-manager-provider.test.ts
💤 Files with no reviewable changes (1)
  • src/core/session/providers/provider-adapter.ts

Comment thread README.md
## 🏗️ 아키텍처
## 아키텍처

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language to the architecture code fence.

Markdownlint flags this fenced block. Use text for the ASCII diagram.

-```
+```text
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 198-198: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 198, The fenced code block in README.md is missing a
language specifier which triggers markdownlint; update the opening
triple-backtick for the ASCII architecture diagram (the code fence that
currently starts with ```) to include the language tag text (i.e., change ``` to
```text) so the block is marked as plain text for the diagram.

Comment thread src/cli/commands/setup.ts
Comment on lines +47 to +49
if (choice === 'claude-code') {
config.default_provider = 'claude-code';
logger.info('Claude Code CLI를 사용합니다.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear provider routing when selecting Claude Code.

Line 48 only changes default_provider; existing agent_providers.mapping from a previous ZAI/OpenCode setup will still win during provider resolution, so rerunning setup and choosing Claude can continue routing agents to stale providers.

Proposed fix
 if (choice === 'claude-code') {
   config.default_provider = 'claude-code';
+  config.agent_providers = { mapping: {}, overrides: {} };
+  config.providers = {};
+  config.concurrency = { defaults: {}, rules: [] };
   logger.info('Claude Code CLI를 사용합니다.');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (choice === 'claude-code') {
config.default_provider = 'claude-code';
logger.info('Claude Code CLI를 사용합니다.');
if (choice === 'claude-code') {
config.default_provider = 'claude-code';
config.agent_providers = { mapping: {}, overrides: {} };
config.providers = {};
config.concurrency = { defaults: {}, rules: [] };
logger.info('Claude Code CLI를 사용합니다.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/setup.ts` around lines 47 - 49, When the user selects
'claude-code' the setup currently only sets config.default_provider but leaves
any existing config.agent_providers.mapping in place, causing stale provider
routing; update the setup branch that handles choice === 'claude-code' (the code
that sets config.default_provider) to also clear or reset
config.agent_providers.mapping (e.g., assign an empty object or remove mappings)
so that agent provider resolution no longer prefers old ZAI/OpenCode entries.

Comment thread src/cli/commands/setup.ts
Comment on lines +120 to +138
} else if (choice === 'mixed-opencode-zai') {
config.default_provider = 'zai-general';
config.providers = {
opencode: { type: 'opencode', binary: 'opencode', maxConcurrency: 1 },
'zai-coding': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.codingModel ?? 'glm-5-turbo', headers: {} },
maxConcurrency: 1,
},
'zai-general': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.generalModel ?? 'glm-4.5', headers: {} },
maxConcurrency: 10,
},
};
config.agent_providers = {
mapping: { cto: 'opencode', ceo: 'zai-general', po: 'zai-general', designer: 'zai-general', qa: 'zai-general', marketer: 'zai-general' },
overrides: {},
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Give the mixed OpenCode provider an explicit model.

Line 123 configures CTO on OpenCode without api.model, so the adapter cannot emit the intended model flag and the session/concurrency layer cannot track CTO by model. This also diverges from the model-distribution behavior configured for the pure OpenCode path.

Proposed fix
-          opencode: { type: 'opencode', binary: 'opencode', maxConcurrency: 1 },
+          opencode: {
+            type: 'opencode',
+            binary: 'opencode',
+            api: { model: 'zai-coding-plan/glm-5.1', headers: {} },
+            maxConcurrency: 1,
+          },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (choice === 'mixed-opencode-zai') {
config.default_provider = 'zai-general';
config.providers = {
opencode: { type: 'opencode', binary: 'opencode', maxConcurrency: 1 },
'zai-coding': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.codingModel ?? 'glm-5-turbo', headers: {} },
maxConcurrency: 1,
},
'zai-general': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.generalModel ?? 'glm-4.5', headers: {} },
maxConcurrency: 10,
},
};
config.agent_providers = {
mapping: { cto: 'opencode', ceo: 'zai-general', po: 'zai-general', designer: 'zai-general', qa: 'zai-general', marketer: 'zai-general' },
overrides: {},
};
} else if (choice === 'mixed-opencode-zai') {
config.default_provider = 'zai-general';
config.providers = {
opencode: {
type: 'opencode',
binary: 'opencode',
api: { model: 'zai-coding-plan/glm-5.1', headers: {} },
maxConcurrency: 1,
},
'zai-coding': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.codingModel ?? 'glm-5-turbo', headers: {} },
maxConcurrency: 1,
},
'zai-general': {
type: 'http-api',
api: { baseUrl, apiKey: providerAnswers.apiKey, model: providerAnswers.generalModel ?? 'glm-4.5', headers: {} },
maxConcurrency: 10,
},
};
config.agent_providers = {
mapping: { cto: 'opencode', ceo: 'zai-general', po: 'zai-general', designer: 'zai-general', qa: 'zai-general', marketer: 'zai-general' },
overrides: {},
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/setup.ts` around lines 120 - 138, The mixed "opencode"
provider lacks an explicit model, so update config.providers for the 'opencode'
entry to include an api.model (mirror the coding model used for 'zai-coding') so
the adapter can emit the model flag and the session/concurrency layer can track
CTO by model; specifically modify the config.providers 'opencode' object (the
one referenced by agent_providers.mapping's 'cto') to include an api.model set
from providerAnswers.codingModel ?? 'glm-5-turbo' (or the project’s default
coding model).

Comment on lines +127 to +136
export function getProviderForAgent(config: ProjectConfig, role: string): ProviderConfigType | null {
const mapping = config.agent_providers?.mapping ?? {};
const providerName = mapping[role] ?? config.default_provider;

if (!providerName || providerName === 'claude-code') return null;

const overrides = config.agent_providers?.overrides ?? {};
if (overrides[role]) return overrides[role];

return config.providers[providerName] ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply role overrides before mapped/default provider resolution.

Line 131 returns null for claude-code/missing defaults before Line 134 checks overrides, so a role-specific override is ignored unless the role also has a non-Claude mapping/default. That contradicts the intended precedence: overrides → mapping → default.

🐛 Proposed fix
 export function getProviderForAgent(config: ProjectConfig, role: string): ProviderConfigType | null {
+  const overrides = config.agent_providers?.overrides ?? {};
+  if (overrides[role]) return overrides[role];
+
   const mapping = config.agent_providers?.mapping ?? {};
   const providerName = mapping[role] ?? config.default_provider;
 
   if (!providerName || providerName === 'claude-code') return null;
 
-  const overrides = config.agent_providers?.overrides ?? {};
-  if (overrides[role]) return overrides[role];
-
   return config.providers[providerName] ?? null;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getProviderForAgent(config: ProjectConfig, role: string): ProviderConfigType | null {
const mapping = config.agent_providers?.mapping ?? {};
const providerName = mapping[role] ?? config.default_provider;
if (!providerName || providerName === 'claude-code') return null;
const overrides = config.agent_providers?.overrides ?? {};
if (overrides[role]) return overrides[role];
return config.providers[providerName] ?? null;
export function getProviderForAgent(config: ProjectConfig, role: string): ProviderConfigType | null {
const overrides = config.agent_providers?.overrides ?? {};
if (overrides[role]) return overrides[role];
const mapping = config.agent_providers?.mapping ?? {};
const providerName = mapping[role] ?? config.default_provider;
if (!providerName || providerName === 'claude-code') return null;
return config.providers[providerName] ?? null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/project/config.ts` around lines 127 - 136, The getProviderForAgent
function currently returns early for missing/default or 'claude-code' before
honoring role-specific overrides; change the logic in getProviderForAgent so
that you first check config.agent_providers?.overrides[role] and return it if
present (overrides → mapping → default), then resolve providerName from mapping
or default and only then apply the guard that returns null for missing or
'claude-code'; update references to mapping, overrides, providerName, and
config.providers accordingly.

Comment on lines +77 to +80
// 모델이 지정된 경우에만 --model 추가
if (this.model) {
parts.push('--model', this.model);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Quote the configured model argument before shell execution.

this.model is config-derived and currently appended raw. A model value containing whitespace or shell metacharacters can break the command or inject extra shell syntax.

🛡️ Proposed fix
     // 모델이 지정된 경우에만 --model 추가
     if (this.model) {
-      parts.push('--model', this.model);
+      parts.push('--model', `'${this.escapeSingleQuote(this.model)}'`);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/providers/opencode-adapter.ts` around lines 77 - 80, The
code pushes this.model directly into the shell argument array (parts) which can
allow whitespace or shell metacharacters to break or inject into the executed
command; update the logic around where parts.push('--model', this.model) is used
(the code that builds the parts array in the Opencode adapter) to ensure the
model value is safely shell-quoted or escaped before being appended (e.g., use a
robust escaping/quoting helper or wrap the model in a safe-quote function) so
the resulting argument is treated as a single literal token by the shell.

Comment on lines +223 to +225
const logFile = path.join(this.getLogDir(), `${sessionName}.log`);
const fullCmd = `cd '${projectRoot}' && ${commandToUse} 2>&1 | tee '${logFile}'`;
await this.createTmuxSession(sessionName, fullCmd);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Initialize projectRoot before provider tmux creation.

In the provider path, getLogDir() and createTmuxSession() run before Line 269 sets this.projectRoot. That can write logs to the process CWD and, for long commands, skip the script-file fallback because getLogDir() returns empty. Set this.projectRoot = projectRoot before entering the provider execute() path. As per coding guidelines, tmux command arguments longer than ~16KB must be split into separate shell script files and executed via bash /path.

Minimal fix
     const sessionId = randomUUID();
     const escapedPrompt = systemPrompt.replace(/'/g, "'\\''");
+    this.projectRoot = projectRoot;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/session-manager.ts` around lines 223 - 225, The provider
path currently calls getLogDir() and createTmuxSession() before this.projectRoot
is assigned, causing logs to be written to the process CWD and preventing the
script-file fallback for very long tmux commands; to fix, assign
this.projectRoot = projectRoot immediately after computing projectRoot and
before invoking the provider.execute() branch so getLogDir() reflects the
correct root, and ensure the logic in createTmuxSession() (or the code that
builds fullCmd) follows the existing guideline to split tmux command arguments >
~16KB into a shell script and execute via bash /path rather than inlining a huge
command string.

Comment on lines +241 to +245
// enqueue/acquire slot — don't await full acquire (which resolves when session ends)
// but wait until the tmux session creation step finishes so callers observe created session.
const acquirePromise = this.concurrencyLimiter.acquire(providerKey, modelKey, agent.role, execute);
// wait until tmux session is created inside execute
await createComplete;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Propagate provider creation failures instead of waiting forever.

If adapter.buildCommand() or createTmuxSession() throws inside execute(), acquirePromise rejects but createComplete is never resolved/rejected. startAgent() then hangs on await createComplete. Wire the acquire rejection into the create-complete promise or race it explicitly.

Potential direction
-      const createComplete = new Promise<void>((resolve) => this.createCompleteResolvers.set(agent.role, resolve));
+      let rejectCreateComplete!: (err: unknown) => void;
+      const createComplete = new Promise<void>((resolve, reject) => {
+        rejectCreateComplete = reject;
+        this.createCompleteResolvers.set(agent.role, resolve);
+      });
...
       const acquirePromise = this.concurrencyLimiter.acquire(providerKey, modelKey, agent.role, execute);
+      acquirePromise.catch((err) => {
+        this.createCompleteResolvers.delete(agent.role);
+        rejectCreateComplete(err);
+      });
       // wait until tmux session is created inside execute
       await createComplete;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/session-manager.ts` around lines 241 - 245, The
createComplete Promise can hang if this.concurrencyLimiter.acquire(...) rejects
(e.g., adapter.buildCommand() or createTmuxSession() throws) because
createComplete is never rejected; update startAgent to propagate acquirePromise
rejections into createComplete by either wiring acquirePromise.catch(err =>
rejectCreateComplete(err)) when creating createComplete, or replace await
createComplete with awaiting Promise.race([createComplete, acquirePromise]) so
any rejection from this.concurrencyLimiter.acquire(providerKey, modelKey,
agent.role, execute) will reject startAgent; reference the symbols
acquirePromise, createComplete, this.concurrencyLimiter.acquire, execute,
adapter.buildCommand, createTmuxSession, and startAgent when making the change.

Comment on lines +620 to +642
let commandToUse: string;
let sessionProvider: string | undefined;

if (configProvider) {
const adapterConfig = this.toAdapterConfig(configProvider);
if (adapterConfig.api?.apiKey && adapterConfig.api.apiKey.includes('${')) {
throw new Error(`Provider for role '${initiator.role}' has unresolved environment variable in API key: ${adapterConfig.api.apiKey}`);
}

const adapter = createProviderAdapter(adapterConfig);
// Meetings require filesystem access; if adapter is CLI-based use it, otherwise fall back to Claude CLI
if (adapter.isCLIBased) {
const mcpConfig = this.buildMcpConfigObject(initiator, config);
const buildResult = adapter.buildMeetingCommand({
systemPrompt,
sessionId,
projectRoot,
meetingAgenda: taskInstruction,
mcpConfig,
});
commandToUse = buildResult.command;
sessionProvider = adapter.type;
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Populate model for meeting sessions too.

Meeting sessions record provider but drop adapterConfig.api?.model, so dashboard/API model reporting is incomplete for provider-backed meetings. Mirror the startAgent() and startEphemeralAgent() behavior.

Minimal fix
     let commandToUse: string;
     let sessionProvider: string | undefined;
+    let sessionModel: string | undefined;
...
         commandToUse = buildResult.command;
         sessionProvider = adapter.type;
+        sessionModel = adapterConfig.api?.model;
...
       status: 'working',
       startedAt: new Date().toISOString(),
       provider: sessionProvider,
+      model: sessionModel,
     };

Also applies to: 678-684

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/session/session-manager.ts` around lines 620 - 642, The meeting-path
currently sets commandToUse and sessionProvider but never sets the session/model
value from provider-backed adapters; fetch adapterConfig.api?.model (like
startAgent/startEphemeralAgent do) and assign it to the session's model variable
before returning/creating the session so provider-backed meetings report the
model; update the code inside the adapter.isCLIBased branch (and the equivalent
non-CLI branch around the 678-684 region) to mirror the model assignment logic
used by startAgent() and startEphemeralAgent(), using adapterConfig.api?.model
as the source.

Comment on lines +157 to +195
it('Provider/model fields in agent response and /api/status providers summary', async () => {
// Start an agent using the SessionManager injected in beforeEach via DashboardServer constructor
// We'll start CTO with provider override from default project config (createZaiDefaultConfig used by ProjectManager.init)
const sessionManager = (dashboard as any).sessionManager as SessionManager;
const pm = new ProjectManager(project.root);
const config = await pm.loadConfig();

// Start CTO agent session (ephemeral start not needed) — use startAgent to ensure provider/model are populated
const agentRegistry = await (async () => { const reg = (dashboard as any).agentRegistry as any; await reg.load(); return reg; })();
const agents = agentRegistry.getAll();
const ctoConfig = agents.find((a: any) => a.role === 'cto');
expect(ctoConfig).toBeDefined();

// startAgent requires many params; we'll call startEphemeralAgent which also sets provider/model
const session = await sessionManager.startEphemeralAgent({
sessionName: 'cto-test',
agent: ctoConfig,
config,
projectRoot: project.root,
message: 'test provider model exposure',
});

// verify /api/agents includes provider/model on current_session
const agentsRes = await fetch(`${baseUrl}/api/agents`);
expect(agentsRes.status).toBe(200);
const agentsBody = await agentsRes.json() as Array<any>;
const cto = agentsBody.find(a => a.role === 'cto');
expect(cto).toBeDefined();
// current_session may be null if sessionName differs; check for provider/model in running sessions list instead
const runningSession = agentsBody.map(a => a.current_session).find(s => s && s.role === 'cto-test');
// For CLI-based adapters provider/model may be undefined; ensure fields exist (may be undefined) on session object
const statusRes = await fetch(`${baseUrl}/api/status`);
expect(statusRes.status).toBe(200);
const statusBody = await statusRes.json() as any;
expect(statusBody.providers).toBeDefined();

// cleanup: stop the ephemeral session we created
await sessionManager.stopAgent('cto-test');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert the provider/model behavior this test is meant to cover.

runningSession and session are never asserted, and expect(statusBody.providers).toBeDefined() passes even for {}. This test can pass while provider/model exposure is broken.

🧪 Proposed assertion tightening
-    const runningSession = agentsBody.map(a => a.current_session).find(s => s && s.role === 'cto-test');
-    // For CLI-based adapters provider/model may be undefined; ensure fields exist (may be undefined) on session object
+    expect(session.role).toBe('cto-test');
+
     const statusRes = await fetch(`${baseUrl}/api/status`);
     expect(statusRes.status).toBe(200);
     const statusBody = await statusRes.json() as any;
     expect(statusBody.providers).toBeDefined();
+    expect(statusBody.agents_running).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ role: 'cto-test' }),
+      ]),
+    );
+    expect(Object.values(statusBody.providers).reduce((sum: number, count: any) => sum + count, 0)).toBeGreaterThan(0);

If the intent is to validate non-undefined provider/model, configure this test with a CLI provider/model explicitly before starting the session, then assert those exact values.

Based on learnings, “E2E tests must use MockTmuxAdapter for tmux mocking while keeping file system, chokidar, and Express as real implementations”.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('Provider/model fields in agent response and /api/status providers summary', async () => {
// Start an agent using the SessionManager injected in beforeEach via DashboardServer constructor
// We'll start CTO with provider override from default project config (createZaiDefaultConfig used by ProjectManager.init)
const sessionManager = (dashboard as any).sessionManager as SessionManager;
const pm = new ProjectManager(project.root);
const config = await pm.loadConfig();
// Start CTO agent session (ephemeral start not needed) — use startAgent to ensure provider/model are populated
const agentRegistry = await (async () => { const reg = (dashboard as any).agentRegistry as any; await reg.load(); return reg; })();
const agents = agentRegistry.getAll();
const ctoConfig = agents.find((a: any) => a.role === 'cto');
expect(ctoConfig).toBeDefined();
// startAgent requires many params; we'll call startEphemeralAgent which also sets provider/model
const session = await sessionManager.startEphemeralAgent({
sessionName: 'cto-test',
agent: ctoConfig,
config,
projectRoot: project.root,
message: 'test provider model exposure',
});
// verify /api/agents includes provider/model on current_session
const agentsRes = await fetch(`${baseUrl}/api/agents`);
expect(agentsRes.status).toBe(200);
const agentsBody = await agentsRes.json() as Array<any>;
const cto = agentsBody.find(a => a.role === 'cto');
expect(cto).toBeDefined();
// current_session may be null if sessionName differs; check for provider/model in running sessions list instead
const runningSession = agentsBody.map(a => a.current_session).find(s => s && s.role === 'cto-test');
// For CLI-based adapters provider/model may be undefined; ensure fields exist (may be undefined) on session object
const statusRes = await fetch(`${baseUrl}/api/status`);
expect(statusRes.status).toBe(200);
const statusBody = await statusRes.json() as any;
expect(statusBody.providers).toBeDefined();
// cleanup: stop the ephemeral session we created
await sessionManager.stopAgent('cto-test');
});
it('Provider/model fields in agent response and /api/status providers summary', async () => {
// Start an agent using the SessionManager injected in beforeEach via DashboardServer constructor
// We'll start CTO with provider override from default project config (createZaiDefaultConfig used by ProjectManager.init)
const sessionManager = (dashboard as any).sessionManager as SessionManager;
const pm = new ProjectManager(project.root);
const config = await pm.loadConfig();
// Start CTO agent session (ephemeral start not needed) — use startAgent to ensure provider/model are populated
const agentRegistry = await (async () => { const reg = (dashboard as any).agentRegistry as any; await reg.load(); return reg; })();
const agents = agentRegistry.getAll();
const ctoConfig = agents.find((a: any) => a.role === 'cto');
expect(ctoConfig).toBeDefined();
// startAgent requires many params; we'll call startEphemeralAgent which also sets provider/model
const session = await sessionManager.startEphemeralAgent({
sessionName: 'cto-test',
agent: ctoConfig,
config,
projectRoot: project.root,
message: 'test provider model exposure',
});
// verify /api/agents includes provider/model on current_session
const agentsRes = await fetch(`${baseUrl}/api/agents`);
expect(agentsRes.status).toBe(200);
const agentsBody = await agentsRes.json() as Array<any>;
const cto = agentsBody.find(a => a.role === 'cto');
expect(cto).toBeDefined();
// current_session may be null if sessionName differs; check for provider/model in running sessions list instead
expect(session.role).toBe('cto-test');
const statusRes = await fetch(`${baseUrl}/api/status`);
expect(statusRes.status).toBe(200);
const statusBody = await statusRes.json() as any;
expect(statusBody.providers).toBeDefined();
expect(statusBody.agents_running).toEqual(
expect.arrayContaining([
expect.objectContaining({ role: 'cto-test' }),
]),
);
expect(Object.values(statusBody.providers).reduce((sum: number, count: any) => sum + count, 0)).toBeGreaterThan(0);
// cleanup: stop the ephemeral session we created
await sessionManager.stopAgent('cto-test');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/dashboard-api.e2e.test.ts` around lines 157 - 195, The test
currently never asserts provider/model and a blank providers object passes;
update the test to (1) configure the session to use MockTmuxAdapter/explicit CLI
provider and model before calling sessionManager.startEphemeralAgent (so
startEphemeralAgent receives known provider/model values), (2) assert on the
returned session from startEphemeralAgent and the runningSession derived from
agentsBody that session.provider and session.model (and runningSession.provider
and runningSession.model) are defined and equal to the expected values, and (3)
tighten the /api/status assertion to check that statusBody.providers contains
the expected provider key(s) and non-empty model info rather than only using
toBeDefined().

Comment on lines +315 to +333
it('Ephemeral with HTTP API provider → falls back to CLI', async () => {
const tmux = new MockTmuxAdapter();
const sm = new SessionManager(tmux);
const projectRoot = await setupProjectRoot();

try {
const config = configWithProvider();
const session = await sm.startEphemeralAgent({
sessionName: 'ephemeral-ceo',
agent: baseAgent,
config,
projectRoot,
message: 'Please advise on roadmap',
});

expect(session).toBeDefined();
expect(tmux.createCalls.length).toBe(1);
const cmd = tmux.createCalls[0].command;
expect(cmd).toContain('claude');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t assert Claude fallback when an HTTP provider is configured.

These tests lock ephemeral and meeting sessions to Claude even when configWithProvider() selects http-api. That breaks provider-only setups where Claude is not installed/configured; assert the HTTP adapter command instead.

Proposed test adjustment
-  it('Ephemeral with HTTP API provider → falls back to CLI', async () => {
+  it('Ephemeral with HTTP API provider → uses provider adapter', async () => {
@@
-      expect(cmd).toContain('claude');
+      expect(cmd).toContain('node');
+      expect(cmd).not.toMatch(/\bclaude\b/);
@@
-  it('Meeting with HTTP API provider → falls back to CLI', async () => {
+  it('Meeting with HTTP API provider → uses provider adapter', async () => {
@@
-      expect(cmd).toContain('claude');
+      expect(cmd).toContain('node');
+      expect(cmd).not.toMatch(/\bclaude\b/);

Also applies to: 390-409

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/core/session-manager-provider.test.ts` around lines 315 - 333, The
test currently asserts a Claude CLI fallback even when configWithProvider() sets
an HTTP provider; update the assertions in the test that call
SessionManager.startEphemeralAgent (and the similar test at lines 390-409) to
expect the HTTP adapter command produced by the HTTP provider rather than
checking for 'claude'. Locate usage of SessionManager (constructor/instance sm),
MockTmuxAdapter.createCalls and the variable cmd in these tests and replace the
expect(cmd).toContain('claude') checks with an assertion that verifies the HTTP
adapter command (e.g., check for the HTTP adapter executable or URL pattern that
the HTTP provider should use based on configWithProvider()).

@sigco3111 sigco3111 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

🔍 Code Review — Request Changes

전반적으로 OpenCode 통합, 동시성 큐잉, 모델 분산 아키텍처는 잘 설계되었습니다. 테스트 커버리지도 좋습니다. 하지만 3개 Critical 이슈5개 Major 이슈가 있어 수정이 필요합니다.


🔴 Critical

C1. configureConcurrency가 provider를 항상 'http-api'로 하드코딩 — 모델 분산 동시성 제어 동작 안 함

  • File: session-manager.tsconfigureConcurrency()
  • provider: 'http-api'로 룰을 등록하는데, OpenCode 어댑터는 type: 'opencode'를 반환합니다. getMaxConcurrency('opencode', 'glm-5.1')는 절대 매칭되지 않아 모델 분산 설정의 핵심 기능이 동작하지 않습니다.
  • Fix: config에서 실제 provider type을 읽어서 룰 등록, 또는 concurrency rule 매칭을 model-only로 변경

C2. configureConcurrencystartAgent() 호출마다 실행 — 룰 무한 누적

  • File: session-manager.tsstartAgent()
  • addRule()이 매번 push라서 100번 에이전트 시작하면 100x 중복 룰이 생깁니다. 메모리 릭이고 regex 매칭 성능이 저하됩니다.
  • Fix: constructor/setProjectRoot/init에 한 번만 호출, 또는 resetRules() 추가

C3. stopAgent의 폴링 루프 race condition — 동시성 슬롯 해제 불안정

  • File: session-manager.tsstopAgent()
  • slotResolver()로 resolve 후, ConcurrencyLimiter.finally(() => this.release(...))가 마이크로태스크로 스케줄링되는데, 50×20ms 폴링 윈도우 안에 실행된다는 보장이 없습니다. 타임아웃되면 다음 startAgent()가 큐에서 영원히 대기하게 됩니다. 이 PR이 고치려던 버그와 동일한 클래스의 버그입니다.
  • Fix: 폴링 대신 release 이벤트/콜백 패턴 사용, 또는 acquire() promise를 직접 await

🟠 Major

M1. prompts.tsProviderSetupAnswers 인터페이스 중복 정의

  • 첫 번째 (incomplete) 선언을 제거하세요. TypeScript는 merge하지만 유지보수에 혼란을 줍니다.

M2. resolveEnvVarsapiKey에만 적용 — baseUrl, binary 무시

  • baseUrl에도 ${VAR} 사용 가능하게 하거나, 모든 string 필드에 recursive 적용하세요.

M3. getProviderForAgent'claude-code' 문자열을 sentinel로 사용

  • config에 claude-code라는 이름의 프로바이더를 정의하면 null 반환. provider type이 아닌 존재 여부로 체크하세요.

M4. Ephemeral/Meeting 세션이 동시성 리미터를 우회

  • startEphemeralAgent(), startMeetingSession()acquire() 호출이 없습니다. 의도적이라면 문서화 필요.

M5. hasInstruction 타입 가드가 'instruction' in skill로 취약

  • 명시적으로 (skill as any).instruction !== undefined 체크가 더 안전합니다.

💡 Suggestions

  • S1: startEphemeralAgent, startMeetingSession, startAgent의 프로바이더 명령 생성 로직이 3중 복제 → 공통 메서드 추출
  • S2: acquire()AbortSignal 또는 timeout 추가
  • S3: configureConcurrency 중복 호출 테스트 추가
  • S4: loadConfig() 시점에 validateProviderConfig() 호출하여 조기 검증
  • S5: stopAll()에서 pending slot resolvers resolve 누락

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.

2 participants