Skip to content

[#154] Ephemeral workspaces: squash fix-154 + ct03 reconciliation - #39

Open
devguyio wants to merge 5 commits into
mainfrom
squash/ct03
Open

devguyio wants to merge 5 commits into
mainfrom
squash/ct03

Conversation

@devguyio

@devguyio devguyio commented Jun 11, 2026

Copy link
Copy Markdown

Summary

Reconciles two divergent branches from the Epic #85 (Ephemeral Workspaces) implementation:

  • fix-154 (23 commits → squashed): Eradicate old workspace model, D-02 shared credentials, dead code removal
  • ct03 (16 commits → squashed): Tracing, exploratory test fixes, bridge context improvements

The branches were squashed individually, then ct03 was rebased onto fix-154. Post-rebase compilation errors were resolved:

  • Implemented deactivate_session on SessionManager using the finalization infrastructure
  • Added push_with_rebase_retry and DEFAULT_MAX_RETRIES to session::finalization::deactivation
  • Fixed 10 missing struct fields across test files (finalization_agent_pid, project_names, workspace_base)

Additional fixes:

  • Justfile: Export GH_TOKEN=$TESTS_GH_TOKEN in all e2e recipes so preflight_gh_auth() uses the test token instead of falling through to the bot's expired hosts.yml
  • Exploratory Justfile: Fix stale phase-dphase-d-session reference in bridge-and-workspace recipe (pre-existing bug from fix-154)

Test Results

  • Unit: 1,172 passed
  • Integration: 111 passed
  • Conformance: 18 passed
  • Profile: 5 passed
  • E2E: 8/8 scenarios passed (119 cases)
  • Exploratory: 153/154 passed (1 cleanup-ordering issue in G8 — pre-existing orphaned process race)

Supersedes #37 and #38.
Ref: #154, #85

Test plan

  • just test — all unit, conformance, and E2E tests pass
  • just exploratory-test — 153/154 pass (G8 is a pre-existing cleanup race)
  • Rebase clean — no merge conflicts, no compilation warnings

Summary by CodeRabbit

  • New Features

    • Added Sessions page to console UI displaying team sessions with member names, status, and timestamps.
    • New sessions list API endpoint for retrieving session data.
    • Enhanced session lifecycle management with improved finalization workflow and graceful shutdown handling.
    • Support for meeting sessions through ephemeral session workflows.
    • Added Sessions navigation item to console sidebar.
  • Improvements

    • Better credential handling for ephemeral sessions.
    • Improved session finalization with fast-path git push before completion.
    • Enhanced daemon logging with structured tracing output.
    • Better brain workspace assembly and member-state persistence.
  • Bug Fixes

    • Fixed ACP client shutdown ordering to prevent I/O hangs.

devguyio and others added 5 commits June 11, 2026 01:07
- Migrate api.rs from daemon_log to tracing (log module deleted by ct03)
- Add workspace_base field to HydrationWorkspaceOps struct
- Make is_brain_member accessible outside #[cfg(test)]
- Add missing log_level arg to start_daemon call in meeting.rs
- Fix unicode escape in sessions_api.rs test assertion
- Fix .clone/ typo to .claude/ in hydration.rs doc comment
…on errors

Implement the missing deactivate_session method on SessionManager using
the ephemeral session model's finalization infrastructure. The method
inspects dirty state, pushes unpushed repos with rebase-retry (D-07),
and transitions the session to Completed. Push failures are non-fatal.

Key changes:
- Add push_with_rebase_retry to session::finalization::deactivation
  (replaces the removed crate::workspace version)
- Add DeactivationResult return type for deactivate_session
- Fix 10 missing fields in test constructions (AssemblyConfig and
  SessionRecord)

All 1,401 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hase-d ref

The e2e preflight auth check (`preflight_gh_auth`) checks GH_TOKEN env
var first, falling through to `gh auth status` when unset. In workspace
contexts with GH_CONFIG_DIR pointing at a bot's expired hosts.yml, the
fallback always fails. Setting GH_TOKEN=$TESTS_GH_TOKEN bypasses this.

Also fixes a pre-existing bug from fix-154: the `all:` recipe was updated
to reference `phase-d-session` but `bridge-and-workspace:` was missed.

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

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a console sessions view and API, shifts daemon/member workflows to session-based startup and shutdown, expands workspace hydration and credential handling, and updates finalization, logging, and exploratory test coverage to match the new session model.

Changes

Sessions and lifecycle changes

Layer / File(s) Summary
Console sessions page and API
console/src/..., crates/bm/src/web/..., crates/bm/src/daemon/sessions_api.rs
/api/teams/{team}/sessions now serves session summaries to the console, and the console adds a Sessions page plus sidebar entry that renders those summaries.
Daemon command and runtime wiring
crates/bm/src/cli.rs, crates/bm/src/commands/..., crates/bm/src/daemon/..., Justfile
Daemon startup, loop start, and session launch paths now carry a log level, route through the sessions API, and use tracing-based runtime logging.
Workspace hydration and credentials
crates/bm/src/chat/mod.rs, crates/bm/src/brain/bridge_adapter.rs, crates/bm/src/formation/..., crates/bm/src/workspace/hydration.rs
Session workspaces now carry durable member state, shared credential directories, and project-aware .claude/ assembly data, with updated brain launch and credential writing behavior.
Session lifecycle and finalization
crates/bm/src/session/..., crates/bm/src/daemon/sessions_api.rs, crates/bm/src/workspace/{mod.rs,util.rs}
Sessions now track finalization agent PIDs and results, stop/deactivate paths kill finalization processes, dirty-state scanning is recursive, and deactivation uses push/retry helpers before completion.
Tests, docs, and exploratory scenarios
console/e2e/*, crates/bm/tests/e2e/*, crates/bm/tests/exploratory/*, docs/..., invariants/..., profiles/...
Console, E2E, exploratory scripts, docs, and room-creation fixtures are updated for the new sessions, credentials, ACP binary, and finalization flows.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant WebRouter
  participant SessionsApiState
  participant DaemonRun
  Browser->>WebRouter: GET /api/teams/{team}/sessions
  WebRouter->>SessionsApiState: list_for_console()
  SessionsApiState-->>WebRouter: session summaries
  DaemonRun->>SessionsApiState: start_session / stop_session / refresh credentials
  WebRouter-->>Browser: JSON response
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related issues

  • devguyio-bot-squad/may-team-team#160: The PR updates the exploratory session lifecycle scripts and PLAN/REPORT content around daemon/session-driven brain detection and related session-workspace behavior.

Possibly related PRs

Poem

🐇 I hop through logs and session trails,
New rooms, new runs, new bunny tales.
A carrot for each completed task,
A cozy shell beneath the mask.
Hoppity hop—sessions now glow!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch squash/ct03

@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: 16

Caution

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

⚠️ Outside diff range comments (1)
crates/bm/src/daemon/config.rs (1)

72-110: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider consolidating logs directory creation.

log_dir(), log(), and member_log() all independently create the logs/ directory using fs::create_dir_all. The new log_dir() helper could be reused in the other two methods to reduce duplication.

♻️ Possible consolidation
 pub fn log(&self) -> Result<PathBuf> {
-    let logs_dir = self.config_dir.join("logs");
-    fs::create_dir_all(&logs_dir)?;
+    let logs_dir = self.log_dir()?;
     Ok(logs_dir.join(format!("daemon-{}.log", self.team_name)))
 }

 pub fn member_log(&self, member_name: &str) -> Result<PathBuf> {
-    let logs_dir = self.config_dir.join("logs");
-    fs::create_dir_all(&logs_dir)?;
+    let logs_dir = self.log_dir()?;
     Ok(logs_dir.join(format!(
         "member-{}-{}.log",
         self.team_name, member_name
     )))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/daemon/config.rs` around lines 72 - 110, Replace the repeated
creation of the logs directory in log() and member_log() by reusing the existing
log_dir() helper: call self.log_dir()? (or map/and_then as appropriate) to
obtain the logs_dir and then join the file name (format!("daemon-{}.log",
self.team_name) in log and format!("member-{}-{}.log", self.team_name,
member_name) in member_log), removing the duplicate fs::create_dir_all calls;
keep log_dir() as the single place that ensures the directory exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/bm/src/chat/mod.rs`:
- Around line 409-416: The function inject_app_credentials_from_shared_dir
currently only sets GH_CONFIG_DIR; update it to also call
refresh_token_from_keyring(&gh_dir) after confirming hosts.yml exists and before
setting GH_CONFIG_DIR, and remove potential conflicting environment variables by
calling std::env::remove_var("GH_TOKEN") and
std::env::remove_var("GITHUB_TOKEN") so the gh CLI uses the refreshed config;
keep the same boolean return semantics. Ensure you reference the existing helper
refresh_token_from_keyring and perform the env cleanup in
inject_app_credentials_from_shared_dir so behavior matches the legacy credential
injection path.
- Around line 615-629: prepare_meeting_session_from_path currently constructs an
AgentSession with credential_dir: None which diverges from
prepare_chat_session_from_path's daemon-ephemeral behavior and can cause missing
or wrong GitHub credentials; change prepare_meeting_session_from_path to accept
the same team_name and member parameters used by prepare_chat_session_from_path
and set AgentSession.credential_dir to Some(shared_credential_dir_for(team_name,
member)) (mirroring the logic used by prepare_chat_session_from_path), and then
update its call site(s) (e.g., in crates/bm/src/commands/meeting.rs) to pass the
new team_name and member arguments.

In `@crates/bm/src/commands/chat.rs`:
- Line 33: The hardcoded log level string "info" should be replaced with a
configurable value exposed via a CLI flag (e.g., --log-level) or config option:
add a log_level field to the existing CLI options struct (the command parsing
code used by the chat command and the daemon start path), wire that flag into
the logger initialization call instead of the literal "info" in chat.rs, and do
the same for the other start path (start.rs) so both use the provided value.
Locate where "info" is passed (the chat command handler and the daemon start
function) and update those logger initialization sites to read the parsed
opt.log_level (or config.log_level) and validate/normalize the value before
applying it. Ensure the flag is documented in the CLI help text.

In `@crates/bm/src/commands/daemon.rs`:
- Line 14: The handler currently accepts log_level: &str with no checks; update
the code that receives this parameter (the function/command handling log_level)
to validate it against the allowed set {"trace","debug","info","warn","error"}
and either return a clear error (or map invalid values to a default) rather than
proceeding silently, and add a short doc comment on the command/function
describing the accepted values; ensure the validation occurs early (e.g., at the
start of the daemon command handler) and uses the exact symbol log_level for
clarity in the change.
- Line 93: The run_daemon function currently accepts log_level without
validation; add the same validation (or docstring/enforced enum) used by the
start function to ensure accepted log level values are consistent—either call
the existing log level validator used by start (or centralize into a new
validate_log_level function) inside run_daemon (referencing run_daemon and
start) and return an Err on invalid values or update the function docs to list
allowed values.

In `@crates/bm/src/daemon/config.rs`:
- Around line 137-141: In save_poll_state, the code currently discards
fs::write's Result and always emits tracing::trace!("Poll state saved"), which
can mislead when the write fails; change the logic to check the Result from
fs::write and only emit the "Poll state saved" trace on Ok, and emit a
tracing::error/tracing::warn with the error details on Err; also handle the Err
branch of serde_json::to_string_pretty by logging the serialization error
(include function name save_poll_state, type PollState and the fs::write Result
in the messages) so failures are correctly reported instead of silently ignored.

In `@crates/bm/src/web/sessions.rs`:
- Around line 71-134: Add a new tokio::test similar to
list_sessions_returns_summaries_with_required_fields that constructs an
initialized but empty SessionsApiState via
SessionsApiState::new(tmp.path().join("registry.json")), wraps it with
make_test_web_state(Some(sessions_state_clone)), mounts web_router and issues a
GET to "/api/teams/test-team/sessions", then assert the response is 200 and the
JSON body deserializes to an empty array (len == 0); reuse the same tmp/config
setup and request-building pattern from
list_sessions_returns_summaries_with_required_fields to ensure
list_for_console() handles an initialized-empty registry without panicking.
- Around line 11-20: The handler list_sessions currently ignores the AxumPath
parameter AxumPath(_team_name) and always uses State(state).sessions_state;
update list_sessions to validate the requested team name against the daemon’s
configured team (or Sessions registry owner) and return StatusCode::NOT_FOUND
when they differ, similar to list_members/team_overview behavior, or explicitly
remove the unused route param and add a TODO documenting the semantics;
reference the list_sessions function and WebState.sessions_state to locate and
implement the check and 404 behavior.

In `@crates/bm/src/workspace/hydration.rs`:
- Around line 2041-2052: The comment above the assertion incorrectly claims
hydrate_workspace() hardcodes project_names: vec![] in AssemblyConfig; locate
the comment near the test that asserts .claude/agents/pr-review.md exists and
update or remove it: either delete the "FAILS: ..." paragraph or change it to
state that hydrate_workspace() now passes self.project_names.clone() into
AssemblyConfig (so project-level agents are assembled), referencing
hydrate_workspace(), AssemblyConfig, and HydrationWorkspaceOps to ensure the
comment matches current behavior.

In `@crates/bm/tests/e2e/scenarios/session_lifecycle_journey.rs`:
- Around line 877-883: The call to wait_for_new_session_workspace currently uses
Duration::from_secs(10) which is shorter than other session-start waits and can
cause flakiness; update that call so it uses Duration::from_secs(20) instead
(i.e., replace the Duration::from_secs(10) argument passed to
wait_for_new_session_workspace near the new_ws declaration) to match the other
timeouts and improve meeting session stability.

In `@crates/bm/tests/exploratory/Justfile`:
- Around line 182-201: The script hardcodes pnpm paths (PNPM_DIR, use of
/global/5/.pnpm/ to compute ACP_PKG, ACP_INDEX and NP1/NP2/NP3) which is
brittle; update the regeneration in the ssh heredoc to discover the global pnpm
root dynamically (e.g. call pnpm root -g or pnpm store path) and derive
ACP_INDEX/NP* from that result, and add explicit existence checks for ACP_PKG,
ACP_INDEX and each NP* before writing the claude-agent-acp wrapper so the script
fails with a clear message rather than silently using wrong paths.

In `@crates/bm/tests/exploratory/phases/phase-b.sh`:
- Around line 56-83: MEMBER_NAME and MEMBER_ROLE extraction using grep|awk (in
the loop that reads MANIFEST/botminter.yml) is brittle for quoted, multi-word,
or commented YAML values; replace the ad-hoc parsing with a proper YAML query
(use yq to extract .name and .role from "$MANIFEST") and add a fallback to the
existing default values (echo "$MEMBER" and "engineer") if yq is missing or the
keys are not present; ensure you reference MEMBER_NAME and MEMBER_ROLE
assignments and the MANIFEST variable so the sed templating still receives safe,
fully-unquoted values.

In `@crates/bm/tests/exploratory/phases/phase-c.sh`:
- Line 255: The shell redirection on the `bm_hire` invocation is reversed so
stderr still goes to the terminal; change the redirect order on the `bm_hire
engineer --name pre-existing` command so stdout is sent to /dev/null first and
then stderr is redirected to stdout (i.e., use >/dev/null 2>&1) to suppress both
streams.

In `@crates/bm/tests/exploratory/phases/phase-f.sh`:
- Line 13: Quote the unquoted shell variable $TEAM in the bm bridge start
invocations to satisfy shellcheck and avoid word-splitting: locate the command
instances where OUT=$(PATH=/usr/bin:/bin bm bridge start -t $TEAM 2>&1) (and the
similar call later) and change the -t argument to use "$TEAM" (i.e., -t "$TEAM")
in both places so the variable is safely quoted.

In `@crates/bm/tests/exploratory/phases/phase-h.sh`:
- Line 140: The shell redirection order is wrong for the bm bridge start command
occurrences: change occurrences of "bm bridge start -t $TEAM_NAME 2>&1
>/dev/null" to "bm bridge start -t \"$TEAM_NAME\" >/dev/null 2>&1" so stdout is
sent to /dev/null before stderr is redirected to it, and quote $TEAM_NAME to
satisfy shellcheck SC2086; apply the same replacement for the other occurrences
of the same command in this script.
- Around line 16-17: Remove the dead variable definitions BOB_WS and STATE_FILE
that are flagged by ShellCheck (SC2034) because the script uses
session-workspace discovery instead of static paths; locate the declarations of
BOB_WS and STATE_FILE in phase-h.sh and delete those lines so there are no
unused variable assignments left in the script.

---

Outside diff comments:
In `@crates/bm/src/daemon/config.rs`:
- Around line 72-110: Replace the repeated creation of the logs directory in
log() and member_log() by reusing the existing log_dir() helper: call
self.log_dir()? (or map/and_then as appropriate) to obtain the logs_dir and then
join the file name (format!("daemon-{}.log", self.team_name) in log and
format!("member-{}-{}.log", self.team_name, member_name) in member_log),
removing the duplicate fs::create_dir_all calls; keep log_dir() as the single
place that ensures the directory exists.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 318d1882-1f11-4483-98ba-b99a71fb9ecc

📥 Commits

Reviewing files that changed from the base of the PR and between d10952a and f8a9093.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (74)
  • Justfile
  • console/e2e/console-pages.spec.ts
  • console/e2e/fixtures.ts
  • console/src/lib/api.ts
  • console/src/lib/components/Sidebar.svelte
  • console/src/lib/types.ts
  • console/src/routes/teams/[team]/sessions/+page.svelte
  • crates/bm/Cargo.toml
  • crates/bm/src/acp/client.rs
  • crates/bm/src/acp/types.rs
  • crates/bm/src/agent_main.rs
  • crates/bm/src/brain/bridge_adapter.rs
  • crates/bm/src/chat/mod.rs
  • crates/bm/src/cli.rs
  • crates/bm/src/commands/brain_run.rs
  • crates/bm/src/commands/chat.rs
  • crates/bm/src/commands/daemon.rs
  • crates/bm/src/commands/meeting.rs
  • crates/bm/src/commands/start.rs
  • crates/bm/src/daemon/api.rs
  • crates/bm/src/daemon/client.rs
  • crates/bm/src/daemon/config.rs
  • crates/bm/src/daemon/event.rs
  • crates/bm/src/daemon/lifecycle.rs
  • crates/bm/src/daemon/log.rs
  • crates/bm/src/daemon/mod.rs
  • crates/bm/src/daemon/process.rs
  • crates/bm/src/daemon/run.rs
  • crates/bm/src/daemon/sessions_api.rs
  • crates/bm/src/formation/launch.rs
  • crates/bm/src/formation/local/linux/mod.rs
  • crates/bm/src/formation/mod.rs
  • crates/bm/src/formation/start_members.rs
  • crates/bm/src/main.rs
  • crates/bm/src/session/cleanup.rs
  • crates/bm/src/session/dirty_state.rs
  • crates/bm/src/session/finalization/deactivation.rs
  • crates/bm/src/session/finalization/subagent.rs
  • crates/bm/src/session/history.rs
  • crates/bm/src/session/manager.rs
  • crates/bm/src/session/registry.rs
  • crates/bm/src/session/retention.rs
  • crates/bm/src/session/stop.rs
  • crates/bm/src/session/types.rs
  • crates/bm/src/web/files.rs
  • crates/bm/src/web/members.rs
  • crates/bm/src/web/mod.rs
  • crates/bm/src/web/overview.rs
  • crates/bm/src/web/process.rs
  • crates/bm/src/web/sessions.rs
  • crates/bm/src/web/state.rs
  • crates/bm/src/web/sync.rs
  • crates/bm/src/web/teams.rs
  • crates/bm/src/workspace/hydration.rs
  • crates/bm/src/workspace/mod.rs
  • crates/bm/src/workspace/util.rs
  • crates/bm/tests/e2e/scenarios/session_lifecycle_journey.rs
  • crates/bm/tests/exploratory/Justfile
  • crates/bm/tests/exploratory/PLAN.md
  • crates/bm/tests/exploratory/REPORT.md
  • crates/bm/tests/exploratory/phases/phase-acp-isolated.sh
  • crates/bm/tests/exploratory/phases/phase-b.sh
  • crates/bm/tests/exploratory/phases/phase-c.sh
  • crates/bm/tests/exploratory/phases/phase-d-session.sh
  • crates/bm/tests/exploratory/phases/phase-d.sh
  • crates/bm/tests/exploratory/phases/phase-e.sh
  • crates/bm/tests/exploratory/phases/phase-f.sh
  • crates/bm/tests/exploratory/phases/phase-g.sh
  • crates/bm/tests/exploratory/phases/phase-h.sh
  • docs/content/concepts/session-model.md
  • invariants/exploratory-test-scope.md
  • profiles/agentic-sdlc-minimal/bridges/tuwunel/Justfile
  • profiles/agentic-sdlc-planning/bridges/tuwunel/Justfile
  • profiles/scrum/bridges/tuwunel/Justfile
💤 Files with no reviewable changes (4)
  • crates/bm/src/daemon/log.rs
  • crates/bm/tests/exploratory/phases/phase-d.sh
  • crates/bm/tests/exploratory/phases/phase-acp-isolated.sh
  • crates/bm/src/workspace/util.rs

Comment thread crates/bm/src/chat/mod.rs
Comment on lines +409 to +416
pub(crate) fn inject_app_credentials_from_shared_dir(credential_dir: &Path) -> bool {
let gh_dir = credential_dir.join("gh");
if !gh_dir.join("hosts.yml").exists() {
return false;
}
std::env::set_var("GH_CONFIG_DIR", &gh_dir);
true
}

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 | ⚡ Quick win

Missing token refresh and environment variable cleanup in D-02 shared credential path.

The inject_app_credentials_from_shared_dir function only sets GH_CONFIG_DIR but does not:

  1. Call refresh_token_from_keyring(...) to ensure the token in hosts.yml is current
  2. Remove GH_TOKEN and GITHUB_TOKEN to prevent auth conflicts

The legacy workspace path (lines 387-390) performs both operations. Without token refresh, ephemeral sessions could fail with expired installation tokens. Without removing conflicting env vars, the gh CLI might use the wrong credential source.

🔒 Proposed fix to match legacy credential injection behavior
-pub(crate) fn inject_app_credentials_from_shared_dir(credential_dir: &Path) -> bool {
+pub(crate) fn inject_app_credentials_from_shared_dir(
+    credential_dir: &Path,
+    ws_path: &Path,
+    team_name: &str,
+    member_name: &str,
+) -> bool {
     let gh_dir = credential_dir.join("gh");
     if !gh_dir.join("hosts.yml").exists() {
         return false;
     }
+    refresh_token_from_keyring(ws_path, team_name, member_name);
     std::env::set_var("GH_CONFIG_DIR", &gh_dir);
+    std::env::remove_var("GH_TOKEN");
+    std::env::remove_var("GITHUB_TOKEN");
     true
 }

Then update the call site at line 380:

     if let Some(cred_dir) = credential_dir {
-        return inject_app_credentials_from_shared_dir(cred_dir);
+        return inject_app_credentials_from_shared_dir(cred_dir, ws_path, team_name, member_name);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/chat/mod.rs` around lines 409 - 416, The function
inject_app_credentials_from_shared_dir currently only sets GH_CONFIG_DIR; update
it to also call refresh_token_from_keyring(&gh_dir) after confirming hosts.yml
exists and before setting GH_CONFIG_DIR, and remove potential conflicting
environment variables by calling std::env::remove_var("GH_TOKEN") and
std::env::remove_var("GITHUB_TOKEN") so the gh CLI uses the refreshed config;
keep the same boolean return semantics. Ensure you reference the existing helper
refresh_token_from_keyring and perform the env cleanup in
inject_app_credentials_from_shared_dir so behavior matches the legacy credential
injection path.

Comment thread crates/bm/src/chat/mod.rs
Comment on lines +615 to 629
/// Prepares a meeting session from an ephemeral workspace path provided by the daemon.
/// The meeting's `instructions` field IS the system prompt — no meta-prompt assembly.
pub fn prepare_meeting_session_from_path(
workspace_path: &Path,
instructions: &str,
) -> Result<AgentSession> {
if instructions.trim().is_empty() {
bail!("Meeting instructions must not be empty");
}
let ws_path = team_path.join(member);
if !ws_path.join(".botminter.workspace").exists() {
bail!(
"No workspace found for member '{}'. Run `bm teams sync` first.",
member
);
}
Ok(AgentSession {
meta_prompt: instructions.to_string(),
ws_path,
ws_path: workspace_path.to_path_buf(),
credential_dir: None,
})
}

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 | ⚡ Quick win

Meeting sessions should use shared credential directory for consistency with chat sessions.

prepare_meeting_session_from_path sets credential_dir: None (line 627), but meeting sessions are ephemeral and daemon-backed (per the function comment on line 615). Chat ephemeral sessions created by prepare_chat_session_from_path set credential_dir: Some(...) (line 152).

This inconsistency could cause:

  1. Meetings to fail with missing GitHub credentials when the legacy workspace .config/gh doesn't exist
  2. Meetings to use personal auth instead of App credentials, breaking permission boundaries

Since meetings are ephemeral sessions provisioned by the daemon, they should follow the same D-02 shared credential pattern as chat sessions.

🔧 Proposed fix to align meeting credential handling

Update the function signature to accept team_name and member:

 pub fn prepare_meeting_session_from_path(
     workspace_path: &Path,
+    team_name: &str,
+    member: &str,
     instructions: &str,
 ) -> Result<AgentSession> {
     if instructions.trim().is_empty() {
         bail!("Meeting instructions must not be empty");
     }
+    let daemon_paths = crate::daemon::DaemonPaths::new(team_name)?;
+    let credential_dir = daemon_paths
+        .sessions_base()
+        .join("credentials")
+        .join(member);
+
     Ok(AgentSession {
         meta_prompt: instructions.to_string(),
         ws_path: workspace_path.to_path_buf(),
-        credential_dir: None,
+        credential_dir: Some(credential_dir),
     })
 }

Then update the call site in crates/bm/src/commands/meeting.rs line 145.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/chat/mod.rs` around lines 615 - 629,
prepare_meeting_session_from_path currently constructs an AgentSession with
credential_dir: None which diverges from prepare_chat_session_from_path's
daemon-ephemeral behavior and can cause missing or wrong GitHub credentials;
change prepare_meeting_session_from_path to accept the same team_name and member
parameters used by prepare_chat_session_from_path and set
AgentSession.credential_dir to Some(shared_credential_dir_for(team_name,
member)) (mirroring the logic used by prepare_chat_session_from_path), and then
update its call site(s) (e.g., in crates/bm/src/commands/meeting.rs) to pass the
new team_name and member arguments.

0,
team.daemon.interval,
"127.0.0.1",
"info",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider making the log level configurable.

The log level is hardcoded to "info" here and in other daemon start paths (e.g., start.rs). Consider exposing this as a CLI flag (e.g., --log-level) or configuration option to give users control over daemon verbosity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/commands/chat.rs` at line 33, The hardcoded log level string
"info" should be replaced with a configurable value exposed via a CLI flag
(e.g., --log-level) or config option: add a log_level field to the existing CLI
options struct (the command parsing code used by the chat command and the daemon
start path), wire that flag into the logger initialization call instead of the
literal "info" in chat.rs, and do the same for the other start path (start.rs)
so both use the provided value. Locate where "info" is passed (the chat command
handler and the daemon start function) and update those logger initialization
sites to read the parsed opt.log_level (or config.log_level) and
validate/normalize the value before applying it. Ensure the flag is documented
in the CLI help text.

port: u16,
interval: u64,
bind: &str,
log_level: &str,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider validating the log_level parameter.

The log_level parameter is accepted as a raw &str with no validation. Consider validating against known log levels (e.g., "trace", "debug", "info", "warn", "error") or documenting the accepted values in a doc comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/commands/daemon.rs` at line 14, The handler currently accepts
log_level: &str with no checks; update the code that receives this parameter
(the function/command handling log_level) to validate it against the allowed set
{"trace","debug","info","warn","error"} and either return a clear error (or map
invalid values to a default) rather than proceeding silently, and add a short
doc comment on the command/function describing the accepted values; ensure the
validation occurs early (e.g., at the start of the daemon command handler) and
uses the exact symbol log_level for clarity in the change.

/// Handles the hidden `bm daemon-run` command.
pub fn run_daemon(team: &str, mode: &str, port: u16, interval: u64, bind: &str) -> Result<()> {
daemon::run_daemon(team, mode, port, interval, bind)
pub fn run_daemon(team: &str, mode: &str, port: u16, interval: u64, bind: &str, log_level: &str) -> Result<()> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Same validation suggestion applies here.

Consider validating or documenting accepted log_level values for consistency with the start function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/src/commands/daemon.rs` at line 93, The run_daemon function
currently accepts log_level without validation; add the same validation (or
docstring/enforced enum) used by the start function to ensure accepted log level
values are consistent—either call the existing log level validator used by start
(or centralize into a new validate_log_level function) inside run_daemon
(referencing run_daemon and start) and return an Err on invalid values or update
the function docs to list allowed values.

Comment on lines +56 to +83
# B9b: Deploy brain-prompt.md into the team repo member dirs (ephemeral model).
# ConfigAssembler.assemble() (CT-154-14) copies brain-prompt.md from
# team_repo/members/<member>/brain-prompt.md into the ephemeral session workspace.
# Do NOT create permanent workspace directories (~/.botminter/workspaces/<team>/<member>/).
BRAIN_TEMPLATE="$TEAM_REPO/brain/system-prompt.md"
if [ -f "$BRAIN_TEMPLATE" ]; then
SENTINEL_FAIL=false
for MEMBER in engineer-alice engineer-bob; do
MEMBER_DIR="$TEAM_REPO/members/$MEMBER"
MANIFEST="$MEMBER_DIR/botminter.yml"
MEMBER_NAME=$(grep '^name:' "$MANIFEST" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "$MEMBER")
MEMBER_ROLE=$(grep '^role:' "$MANIFEST" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "engineer")
sed \
-e "s|{{member_name}}|${MEMBER_NAME}|g" \
-e "s|{{team_name}}|${TEAM}|g" \
-e "s|{{role}}|${MEMBER_ROLE}|g" \
-e "s|{{gh_org}}|${ORG}|g" \
-e "s|{{gh_repo}}|${REPO}|g" \
"$BRAIN_TEMPLATE" > "$MEMBER_DIR/brain-prompt.md" || SENTINEL_FAIL=true
done
if $SENTINEL_FAIL; then
fail "B9b" "Brain deploy" "sed rendering failed for one or more members"
else
pass "B9b" "Deployed brain-prompt.md to team repo (alice and bob, per-member rendered)"
fi
else
fail "B9b" "Brain template" "brain/system-prompt.md not in team repo at $TEAM_REPO"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Fragile YAML parsing for member name and role extraction.

The brain-prompt deployment at L66-67 uses grep | awk '{print $2}' | tr -d '"' to extract values from botminter.yml:

MEMBER_NAME=$(grep '^name:' "$MANIFEST" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "$MEMBER")

This assumes:

  1. The value is always the second whitespace-separated field
  2. Removing " is sufficient to unquote

This breaks if the YAML contains:

  • Multi-word values: name: "Alice Smith" → extracts "Alice (missing Smith")
  • Inline comments: name: alice # engineer → extracts alice (breaks on #)
  • Flow-style: name: 'alice' → leaves single quotes
♻️ Proposed fix using yq for robust YAML parsing
-        MEMBER_NAME=$(grep '^name:' "$MANIFEST" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "$MEMBER")
-        MEMBER_ROLE=$(grep '^role:' "$MANIFEST" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "engineer")
+        MEMBER_NAME=$(yq eval '.name' "$MANIFEST" 2>/dev/null || echo "$MEMBER")
+        MEMBER_ROLE=$(yq eval '.role' "$MANIFEST" 2>/dev/null || echo "engineer")

If yq is not available in the test environment, add a check in the preflight phase or document the grep/awk limitation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/tests/exploratory/phases/phase-b.sh` around lines 56 - 83,
MEMBER_NAME and MEMBER_ROLE extraction using grep|awk (in the loop that reads
MANIFEST/botminter.yml) is brittle for quoted, multi-word, or commented YAML
values; replace the ad-hoc parsing with a proper YAML query (use yq to extract
.name and .role from "$MANIFEST") and add a fallback to the existing default
values (echo "$MEMBER" and "engineer") if yq is missing or the keys are not
present; ensure you reference MEMBER_NAME and MEMBER_ROLE assignments and the
MANIFEST variable so the sed templating still receives safe, fully-unquoted
values.

# Hire pre-existing as a member so sync will provision them
bm_hire superman --name pre-existing 2>&1 >/dev/null || true
# Hire pre-existing as a member so bridge identity add can provision them
bm_hire engineer --name pre-existing 2>&1 >/dev/null || true

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 | ⚡ Quick win

Incorrect redirect order: 2>&1 before stdout redirect has no effect.

At L255, the command bm_hire engineer --name pre-existing 2>&1 >/dev/null redirects stderr to stdout (2>&1), then redirects stdout to /dev/null. This means:

  1. stderr → current stdout (likely the terminal)
  2. stdout → /dev/null

The intent is likely to suppress both stdout and stderr. The correct order is >/dev/null 2>&1:

🐛 Proposed fix
-    bm_hire engineer --name pre-existing 2>&1 >/dev/null || true
+    bm_hire engineer --name pre-existing >/dev/null 2>&1 || true
📝 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
bm_hire engineer --name pre-existing 2>&1 >/dev/null || true
bm_hire engineer --name pre-existing >/dev/null 2>&1 || true
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 255-255: To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify).

(SC2069)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/tests/exploratory/phases/phase-c.sh` at line 255, The shell
redirection on the `bm_hire` invocation is reversed so stderr still goes to the
terminal; change the redirect order on the `bm_hire engineer --name
pre-existing` command so stdout is sent to /dev/null first and then stderr is
redirected to stdout (i.e., use >/dev/null 2>&1) to suppress both streams.

Source: Linters/SAST tools

if echo "$OUT" | grep -qi "just\|skip\|not found"; then
pass "F1" "Graceful handling when just not in PATH"
# F1: Without just — bm bridge start should handle missing just gracefully
OUT=$(PATH=/usr/bin:/bin bm bridge start -t $TEAM 2>&1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Quote $TEAM variable for shellcheck compliance.

Shellcheck flags unquoted $TEAM at lines 13 and 30. While low-risk in this controlled test environment, quoting the variable follows best practice and silences the linter.

♻️ Shellcheck-compliant quoting
-OUT=$(PATH=/usr/bin:/bin bm bridge start -t $TEAM 2>&1)
+OUT=$(PATH=/usr/bin:/bin bm bridge start -t "$TEAM" 2>&1)
-OUT=$(bm members list -t $TEAM 2>&1)
+OUT=$(bm members list -t "$TEAM" 2>&1)

Also applies to: 30-30

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 13-13: Double quote to prevent globbing and word splitting.

(SC2086)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/tests/exploratory/phases/phase-f.sh` at line 13, Quote the unquoted
shell variable $TEAM in the bm bridge start invocations to satisfy shellcheck
and avoid word-splitting: locate the command instances where
OUT=$(PATH=/usr/bin:/bin bm bridge start -t $TEAM 2>&1) (and the similar call
later) and change the -t argument to use "$TEAM" (i.e., -t "$TEAM") in both
places so the variable is safely quoted.

Source: Linters/SAST tools

Comment on lines +16 to 17
BOB_WS="$TEAM_DIR/engineer-bob"
STATE_FILE="$HOME/.botminter/state.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Remove unused variables BOB_WS and STATE_FILE.

Shellcheck flags these variables as unused (SC2034). The script now uses session-workspace discovery instead of static paths, so these definitions are dead code.

♻️ Cleanup unused definitions
 ALICE_WS="$TEAM_DIR/engineer-alice"
-BOB_WS="$TEAM_DIR/engineer-bob"
-STATE_FILE="$HOME/.botminter/state.json"
📝 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
BOB_WS="$TEAM_DIR/engineer-bob"
STATE_FILE="$HOME/.botminter/state.json"
ALICE_WS="$TEAM_DIR/engineer-alice"
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 16-16: BOB_WS appears unused. Verify use (or export if used externally).

(SC2034)


[warning] 17-17: STATE_FILE appears unused. Verify use (or export if used externally).

(SC2034)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/tests/exploratory/phases/phase-h.sh` around lines 16 - 17, Remove
the dead variable definitions BOB_WS and STATE_FILE that are flagged by
ShellCheck (SC2034) because the script uses session-workspace discovery instead
of static paths; locate the declarations of BOB_WS and STATE_FILE in phase-h.sh
and delete those lines so there are no unused variable assignments left in the
script.

Source: Linters/SAST tools

else
# Try to bring it up
bm teams sync --bridge -v 2>&1 >/dev/null
bm bridge start -t $TEAM_NAME 2>&1 >/dev/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 | 🟡 Minor | ⚡ Quick win

Fix redirection order: 2>&1 must come after output redirect.

At lines 140, 592, and 789, the pattern 2>&1 >/dev/null redirects stderr to stdout's original destination (the terminal) before redirecting stdout to /dev/null, leaving stderr visible. The correct order is >/dev/null 2>&1 to discard both streams.

🐛 Corrected redirection order
-    bm bridge start -t $TEAM_NAME 2>&1 >/dev/null
+    bm bridge start -t "$TEAM_NAME" >/dev/null 2>&1

Apply the same fix at lines 592 and 789. Also quote $TEAM_NAME to satisfy shellcheck SC2086.

Also applies to: 592-592, 789-789

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 140-140: Double quote to prevent globbing and word splitting.

(SC2086)


[warning] 140-140: To redirect stdout+stderr, 2>&1 must be last (or use '{ cmd > file; } 2>&1' to clarify).

(SC2069)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/bm/tests/exploratory/phases/phase-h.sh` at line 140, The shell
redirection order is wrong for the bm bridge start command occurrences: change
occurrences of "bm bridge start -t $TEAM_NAME 2>&1 >/dev/null" to "bm bridge
start -t \"$TEAM_NAME\" >/dev/null 2>&1" so stdout is sent to /dev/null before
stderr is redirected to it, and quote $TEAM_NAME to satisfy shellcheck SC2086;
apply the same replacement for the other occurrences of the same command in this
script.

Source: Linters/SAST tools

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