Skip to content

feat: multi-session parallel support, spec phases, npm cleanup#45

Merged
maxritter merged 6 commits intomainfrom
dev
Feb 6, 2026
Merged

feat: multi-session parallel support, spec phases, npm cleanup#45
maxritter merged 6 commits intomainfrom
dev

Conversation

@maxritter
Copy link
Owner

@maxritter maxritter commented Feb 6, 2026

Summary

  • Multi-session parallel support: Run multiple Pilot sessions in the same project without interference. Each session gets isolated state under ~/.pilot/sessions/{PID}/ for continuation files, context cache, and stop guard. Includes Console dashboard integration, plan association API, statusline task display, and active sessions count indicator.
  • Spec command phases: Split /spec into separate phase skills (spec-plan, spec-implement, spec-verify) for better modularity and skill descriptions.
  • npm cleanup: Clean stale npm temp directories before Claude Code install. Block Explore agent in favor of vexor search.

Test plan

  • All 374 launcher tests pass (uv run pytest launcher/tests -q)
  • All 459 console tests pass (cd console && bun test)
  • Type checking clean (basedpyright launcher — 0 errors)
  • Console type checking clean (cd console && bun run typecheck)
  • Multiple pilot sessions can run in parallel without interference
  • Statusline shows active session count when >1 sessions running
  • Statusline shows current task name in spec mode instead of "don't run /compact"
  • Console dashboard displays active sessions with plan associations

Summary by CodeRabbit

  • New Features

    • Multi-session parallel support — run multiple isolated Pilot sessions in the same project.
    • Codebase search and indexing (Vexor) with status widget and re-index controls.
    • Dashboard: view active sessions with plan associations and plan status.
  • Improvements

    • Enhanced Spec view: active/archived tabs, richer header, delete spec support, and background refresh.
    • Search UI: Memories vs Codebase tabs, cancellation, and improved placeholders.
    • Installer auto-detects devcontainer at runtime.
  • Bug Fixes

    • Removed outdated VSCode extension recommendation.
  • Documentation

    • README and changelog updated; license replaced with new CLAUDE PILOT license.

…kill descriptions

- Split monolithic spec.md into spec-plan, spec-implement, spec-verify commands
- Add standards-design skill for visual design quality and AI aesthetic anti-patterns
- Trim all skill descriptions (~57% reduction) to fit 15k char context budget
- Update accessibility skill, context monitor, spec stop guard, and workflow rules
- Clean up installer, shell config, and remove claudeboard extension
- Add Opus 4.6 badge to README
…xplore agent

- Add _clean_npm_stale_dirs() to installer and launcher to remove .claude-code-* temp
  directories that cause npm ENOTEMPTY errors during install/upgrade
- Fall back to success in installer when npm fails but claude command already exists
- Block Task/Explore agent in tool_redirect hook with vexor-specific guidance
- Update workflow-enforcement and vexor-search rules to ban Explore agent
Enable multiple Pilot sessions in the same project without interference.
Each session gets isolated state under ~/.pilot/sessions/{PID}/ for
continuation files, context cache, and stop guard. Includes Console
dashboard integration, plan association API, statusline task display,
and active sessions count indicator.
@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
claude-pilot Ready Ready Preview, Comment Feb 6, 2026 9:47am

@claude
Copy link

claude bot commented Feb 6, 2026

Claude finished @maxritter's task in 3m 0s —— View job


Code Review Complete ✓

I've completed a thorough review of PR #45. This is a substantial PR with well-architected features, but there are several important issues that need to be addressed.


Overview

This PR introduces three major features:

  1. Multi-session parallel support - Session isolation via ~/.pilot/sessions/{PID}/
  2. Spec command phase splitting - Modular /spec-plan, /spec-implement, /spec-verify
  3. npm cleanup - Removes stale npm temp directories before installation

Files changed: 79 files (+2719/-1274)


Critical Issues

1. Encrypted File Committed to Repository ⚠️

File: launcher/session.py

The file appears to be encrypted with git-crypt:

GITCRYPT �+��i {�u#1.�5-D�(9�A}��������Z���...

Issues:

  • This file is completely unreadable without the encryption key
  • Cannot review the core session management implementation
  • May cause issues for contributors without git-crypt setup
  • No documentation explaining this is an encrypted file

Recommendation:

  • If this file must be encrypted, add clear documentation in the README about git-crypt requirements
  • Consider whether session management logic truly needs encryption
  • Ensure all contributors have access to decrypt

Fix encryption documentation →


2. Session ID Environment Variable Default May Cause Collisions

Files:

  • pilot/hooks/spec_stop_guard.py:35
  • pilot/hooks/context_monitor.py:28
  • pilot/rules/context-continuation.md:28

Code:

session_id = os.environ.get("PILOT_SESSION_ID", "").strip() or "default"

Issue:
If PILOT_SESSION_ID is not set, all sessions fall back to "default", which defeats the purpose of multi-session isolation. Multiple concurrent sessions would share:

  • Stop guard state
  • Context cache
  • Continuation files

Impact: HIGH - This breaks the core multi-session isolation feature

Recommendation:

  • Generate a unique session ID if not provided (e.g., default-{pid} or default-{timestamp})
  • Or raise an error if PILOT_SESSION_ID is not set
  • Add validation to ensure the variable is properly set

Fix session ID fallback →


3. Race Condition in Plan Registration API Call

File: pilot/commands/spec.md:79

Code:

python ~/.claude/pilot/scripts/register_plan.py "<plan_path>" "<status>" 2>/dev/null || true

Issues:

  • Non-blocking fire-and-forget API call with || true
  • No feedback if registration fails
  • Silent failures could lead to dashboard showing stale/wrong information
  • No retry logic for transient network errors

File: pilot/scripts/register_plan.py:43-44

resp = urllib.request.urlopen(req, timeout=5)
return resp.status == 200

Bug: resp.status doesn't exist. Should be resp.getcode() == 200

Impact: MEDIUM - Plan-to-session associations may silently fail

Recommendations:

  1. Fix the resp.status bug (it will always fail)
  2. Consider logging failures even if they're non-blocking
  3. Add retry logic for 5xx errors
  4. Validate Console is running before attempting registration

Fix plan registration →


4. SQL Injection Risk in Plan Routes

File: console/src/services/worker/http/routes/PlanRoutes.ts

While the plan store functions use parameterized queries (good!), the path resolution logic has potential directory traversal issues:

Lines 148-149:

const resolvedPath = path.resolve(projectRoot, requestedPath);
const normalizedPlansDir = path.resolve(plansDir);

Issue: If requestedPath contains ../, an attacker could potentially access files outside docs/plans/

Recommendation:

  • Add validation: if (!resolvedPath.startsWith(normalizedPlansDir)) { return 403 }
  • Validate filename format (e.g., matches YYYY-MM-DD-*.md pattern)

Fix path traversal →


High Priority Issues

5. Missing Error Handling in npm Cleanup

File: installer/steps/dependencies.py:192-214

Code:

def _clean_npm_stale_dirs() -> None:
    """Remove stale .claude-code-* temp dirs that cause npm ENOTEMPTY errors."""
    import shutil
    try:
        result = subprocess.run(["npm", "prefix", "-g"], ...)
        if result.returncode != 0:
            return  # Silently fails
        # ... cleanup logic
    except Exception:
        pass  # Broad exception swallowing

Issues:

  • Silently swallows all exceptions
  • No logging when cleanup fails
  • Users won't know why installation might fail later
  • shutil.rmtree(..., ignore_errors=True) masks permission issues

Recommendation:

  • Log cleanup attempts and failures
  • At minimum, use a logger to track when cleanup is skipped
  • Consider making this step visible in the installer UI

6. Stop Guard Cooldown State Not Cleaned Up

File: pilot/hooks/spec_stop_guard.py:143

Code:

state_file.unlink(missing_ok=True)  # Deletes after successful escape

Issue: If the escape hatch is never used, the state file persists forever. Over many sessions, this could accumulate files.

Recommendation:

  • Add TTL-based cleanup (delete files older than 24 hours)
  • Or use a timestamp in the state file and validate freshness
  • Document cleanup expectations

7. Inconsistent Session Status Display

File: console/src/ui/viewer/views/Dashboard/ActiveSessions.tsx:60

Code:

{session.plan_path ? (session.plan_status || 'Spec') : 'Quick'}

Issue: Falls back to 'Spec' if plan_status is null/empty, which is misleading. A plan with no status is likely broken/corrupt.

Recommendation:

  • Show 'Unknown' or 'Error' for null status
  • Or hide sessions with invalid plan associations
  • Add warning indicator for corrupt data

Medium Priority Issues

8. Context Monitor Cache TTL Too Short for Long-Running Operations

File: pilot/hooks/context_monitor.py:18

CACHE_TTL = 30  # 30 seconds

Issue: For operations like test runs or builds that take >30s, the cache expires mid-operation, causing redundant token counting.

Recommendation: Increase to 60-120 seconds, or make it configurable


9. Spec Stop Guard Assumes Plan Directory Exists

File: pilot/hooks/spec_stop_guard.py:43-45

plans_dir = Path("docs/plans")
if not plans_dir.exists():
    return None, None, False

Issue: Uses hardcoded docs/plans path without checking project root or configuration.

Recommendation:

  • Use project root from environment or config
  • Document that docs/plans is the expected location

10. Missing Migration Tests for session_plans Table

File: console/src/services/sqlite/migrations.ts:28 (migration010)

Observation: While console/tests/sqlite/plan-store.test.ts tests the CRUD operations, there are no tests verifying:

  • Migration runs successfully on existing databases
  • Foreign key cascade works correctly
  • Upgrade path from previous schema

Recommendation: Add migration integration tests


Low Priority / Style Issues

11. Magic Numbers in Context Thresholds

File: pilot/hooks/context_monitor.py:13-16

THRESHOLD_WARN = 80
THRESHOLD_STOP = 90
THRESHOLD_CRITICAL = 95
LEARN_THRESHOLDS = [40, 60, 80]

These are hardcoded for a 200K context window. If the model changes, these become invalid.

Recommendation: Calculate based on actual context limit


12. Inconsistent Error Messaging

The codebase uses different emoji styles:

  • in some files
  • 🚨 in others
  • ⚠️ in others

Recommendation: Standardize error/warning icons for consistency


13. Tool Redirect Hardcodes Subagent Allowlist

File: pilot/hooks/tool_redirect.py:115

data.get("tool_input", {}).get("subagent_type", "") not in ("pilot:spec-verifier", "pilot:plan-verifier")

Issue: Adding new allowed subagents requires hook modification.

Recommendation: Move allowlist to config file


Positive Observations ✓

  1. Excellent test coverage - Plan store has comprehensive unit tests with 0% mocks
  2. Good separation of concerns - Phase skills are properly modularized
  3. Defensive SQL - All database queries use parameterized statements
  4. Session isolation architecture - The PID-based session directory approach is sound
  5. Type safety - TypeScript types are well-defined in plans/types.ts
  6. Documentation - Workflow enforcement rules are clear and comprehensive
  7. Foreign key constraints - Proper CASCADE on session deletion

Test Plan Status

Based on the PR description:

  • All 374 launcher tests pass - Cannot verify (encrypted session.py)
  • All 459 console tests pass - Should pass (good test coverage)
  • Type checking clean - Should pass (no obvious type errors)
  • Console type checking clean - Should pass
  • Multiple pilot sessions can run in parallel - BLOCKED by issue chore(docs): fix readme rendering #2
  • Statusline shows active session count - Should work
  • Statusline shows current task name - Should work
  • Console dashboard displays active sessions - Should work (minor issue chore(deps): bump actions/checkout from 4 to 5 #7)

Summary

Overall Assessment: The architectural direction is excellent, but there are blocking issues that need to be addressed before merge:

Must Fix Before Merge:

  1. ⚠️ Encrypted file documentation or decryption
  2. ⚠️ Session ID collision bug
  3. ⚠️ Plan registration API bug (resp.status)

Should Fix Before Merge:
4. Path traversal protection
5. npm cleanup error handling
6. Session status display logic

Can Fix Post-Merge:

  • Cache TTL tuning
  • Migration tests
  • Code style consistency

@coderabbitai
Copy link

coderabbitai bot commented Feb 6, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

Adds session→plan associations and DB-backed plan store, new plan and Vexor HTTP routes with SSE, an EnsureWorkerDaemon to manage worker lifecycle, removes remote-worker mode and readiness checks, updates installer to detect devcontainer at runtime, broad UI refactors for multi-plan support, many tests, and a license replacement.

Changes

Cohort / File(s) Summary
Dev / Editor config
\.devcontainer/devcontainer.json, \.vscode/extensions.json
Removed dkodr.claudeboard from extension recommendations.
Session-plan DB & API
console/src/services/sqlite/.../migrations.ts, console/src/services/sqlite/SessionStore.ts, console/src/services/sqlite/plans/store.ts, console/src/services/sqlite/plans/types.ts, console/tests/sqlite/plan-store.test.ts
Added session_plans support (migration/version guard), types, CRUD store (associate/get/update/clear/getDashboardSessions) and tests exercising cascade, upsert, dashboard queries.
Plan HTTP routes & Viewer endpoints
console/src/services/worker/http/routes/PlanRoutes.ts, console/src/services/worker/http/routes/VexorRoutes.ts, console/src/services/worker/http/routes/SearchRoutes.ts, console/src/services/worker/http/routes/ViewerRoutes.ts, console/tests/vexor/vexor-routes.test.ts, console/tests/worker/ui-endpoints.test.ts
New/extended routes: PlanRoutes (DB + SSE, association/status/delete), VexorRoutes (status/search/reindex + parsers), SearchRoutes now forwards planPath, Dashboard sessions endpoint added; tests for Vexor and endpoints.
Worker lifecycle & retention
console/src/services/infrastructure/EnsureWorkerDaemon.ts, console/src/services/worker-service.ts, console/tests/infrastructure/ensure-worker-daemon.test.ts, console/src/services/worker/RetentionScheduler.ts
New ensureWorkerDaemon module and tests; worker-service now uses it; added RetentionScheduler with start/stop lifecycle and tests.
Queue & batch processing
console/src/services/queue/SessionQueueProcessor.ts, console/src/services/sqlite/PendingMessageStore.ts, console/src/services/worker/SDKAgent.ts, console/src/services/worker/SessionManager.ts, console/tests/queue/session-queue-processor.test.ts
Added idle-timeout and batch iterators, claimAndDeleteBatch in store, batch processing in SDK agent and session manager; added iterator tests.
Context / Observation scoping
console/src/services/context/ObservationCompiler.ts, console/src/services/context/ContextBuilder.ts, console/src/services/context/types.ts, console/tests/context/*.test.ts
Added plan-scoped query helpers (excluding other plans), plumbed planPath through ContextBuilder, ContextInput gains optional planPath; added isolation tests.
Viewer UI (multi-plan, Vexor) & hooks
console/src/ui/viewer/hooks/useStats.ts, console/src/ui/viewer/views/Dashboard/*, console/src/ui/viewer/views/Search/*, console/src/ui/viewer/components/*, console/src/ui/viewer/layouts/*, console/src/ui/viewer/App.tsx, console/src/ui/viewer/constants/timing.ts, console/tests/ui/*.test.ts
Reworked stats (lastObservationAt, plans, vexorStatus), PlanStatus now accepts multiple plans, added VexorStatus and Codebase search UI, Search view split into Memories/Codebase, many UI prop removals (projects/search/status), new timing constant and related tests.
CLI handlers & worker utils removal
console/src/cli/handlers/*.ts, console/src/shared/remote-config.ts, console/src/shared/worker-utils.ts, console/src/shared/remote-endpoint.ts, console/src/types/remote/*
Removed remote-mode APIs and worker readiness helpers; handlers no longer wait for worker readiness; removed file-edit handler and related EventType variant.
Installer & tests
install.sh, installer/config.py, installer/steps/*.py, installer/tests/unit/*
Removed persistent install_mode, switched to runtime .devcontainer detection, added npm stale-dir cleanup and Playwright cache checks, adjusted finalize messaging for multi-session; tests updated/added for new flows.
Docs, changelog, README, .gitignore, license
README.md, CHANGELOG.md, \.gitignore, docs/site/src/components/*, LICENSE
Updated README badge and features, removed some CHANGELOG bullets, added docs/video ignore, docs tweaks, and replaced AGPL license with a new commercial CLAUDE PILOT license (large license replacement).
Viewer template & small UX removals
console/src/ui/viewer/viewer.html, console/src/ui/viewer/viewer-template.html, console/src/ui/viewer/hooks/useSpinningFavicon.ts, various small component deletions
Removed legacy viewer-template.html, spinning-favicon hook; replaced favicon inline; removed VectorDbStatus and SearchFilters; centralized icons in SummaryCard.
Miscellaneous typing & helper tweaks
console/src/services/sqlite/migrations.ts, console/src/services/sqlite/SessionStore.ts (minor docs), console/src/services/worker/PaginationHelper.ts
Minor doc/comment edits and a type assertion tweak in PaginationHelper; SessionStore calls to ensure session_plans table added.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser / Viewer App
    participant Server as Worker HTTP Routes
    participant DB as SQLite
    participant SSE as SSE clients (useStats)

    Client->>Server: POST /api/plan/associate (sessionId, planPath, status)
    Server->>DB: UPSERT session_plans (session_db_id, plan_path, plan_status)
    DB-->>Server: OK (row)
    Server->>SSE: emit "plan_association_changed" (sessionDbId, planPath, status)
    SSE-->>Client: SSE event triggers
    Client->>Server: GET /api/dashboard/sessions
    Server->>DB: SELECT sdk_sessions LEFT JOIN session_plans ...
    DB-->>Server: sessions with plan info
    Server-->>Client: JSON sessions list
    Client->>Client: update UI (PlanStatus, Dashboard)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.71% 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: multi-session parallel support, spec phases, npm cleanup' clearly and specifically summarizes the three major changes in the changeset: multi-session parallel support, spec command phases, and npm cleanup improvements.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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

@maxritter maxritter self-assigned this Feb 6, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@console/src/services/sqlite/migrations.ts`:
- Around line 525-550: Duplicate DDL creates session_plans in two places:
migration010 (version 10) and SessionStore.ensureSessionPlansTable (version 21);
remove the inline table creation from SessionStore.ensureSessionPlansTable and
any schema version 21 references so the table is created only via migration010,
ensuring you keep the exact columns/constraints from migration010 (id,
session_db_id UNIQUE, plan_path, plan_status default, timestamps, FK to
sdk_sessions) and update SessionStore to rely on the migration-runner path (or
call into the same migration/apply routine) instead of running its own CREATE
TABLE logic.

In `@console/src/services/sqlite/plans/store.ts`:
- Around line 8-21: Remove the dead exported function ensureSessionPlansTable
from store.ts: delete the entire exported function declaration (the CREATE TABLE
SQL block) so the module no longer exports it, and update any module export list
that referenced ensureSessionPlansTable; confirm there are no remaining callers
(the authoritative migration is migration010 and the private
ensureSessionPlansTable in SessionStore.ts is used) so no other changes are
needed.

In `@console/src/services/sqlite/SessionStore.ts`:
- Line 47: The constructor (or initialization) currently calls
ensureSessionPlansTable(); if you removed the duplicate ensureSessionPlansTable
method in favor of using migration010, remove this invocation as well to avoid
calling a now-missing or redundant method; update the constructor/initializer
where ensureSessionPlansTable is invoked so it relies solely on migration010 for
creating the session_plans table (reference ensureSessionPlansTable and
migration010 to locate and update the call).
- Around line 640-663: The ensureSessionPlansTable inline migration in
SessionStore (method ensureSessionPlansTable) duplicates migration010 defined in
migrations.ts and uses a conflicting version number (21 vs 10); remove the
entire ensureSessionPlansTable method and its invocation from the SessionStore
constructor so the MigrationRunner and migrations.ts (migration010) are the
single source of truth for creating session_plans; ensure no other code
references ensureSessionPlansTable after removal and keep migration010 in the
migrations array as the authoritative migration.

In `@installer/steps/dependencies.py`:
- Around line 234-237: When npm install fails in the _run_bash_with_retry(...)
error path but command_exists("claude") is true, don't return the requested
version variable; instead probe the actual installed Claude by invoking the
existing claude binary (e.g., run "claude --version" or use any helper that
parses its output) and return that discovered version (or return a failure flag
plus None) so the UI isn't misled. Update the branch where
_configure_claude_defaults() is called to capture the real_installed_version and
return (True, real_installed_version) or log/warn that npm failed and an older
version remains, ensuring you use the same return shape the caller expects
instead of returning the original version variable.
- Around line 192-221: The _clean_npm_stale_dirs function currently calls "npm
prefix -g" and appends "lib/node_modules/@anthropic-ai", which fails on Windows;
change the subprocess.run call to use ["npm", "root", "-g"] and derive npm_root
= Path(result.stdout.strip()) without adding "lib/node_modules", then target
anthropic_dir = npm_root / "@anthropic-ai"; keep the existing loop that rmtree's
".claude-code-*" dirs. Also add a guard for npm not being available by catching
FileNotFoundError (and treating non-zero returncode as an early return) so the
function returns cleanly when npm is missing. Ensure install_claude_code still
calls _clean_npm_stale_dirs.

In `@installer/tests/unit/steps/test_dependencies.py`:
- Around line 149-162: The test test_install_claude_code_cleans_stale_dirs has
multiple patched parameters that go unused (mocks for _clean_npm_stale_dirs,
_remove_native_claude_binaries, _run_bash_with_retry,
_configure_claude_defaults, _get_forced_claude_version) causing Ruff
ARG002/RUF059; rename unused test parameters by prefixing them with an
underscore (e.g., mock_clean -> _mock_clean, mock_remove -> _mock_remove, etc.)
and if any patched return_value (like _get_forced_claude_version) is not used,
ensure its return is discarded or the patch parameter is prefixed with an
underscore so the linter treats it as intentionally unused; apply the same
renaming pattern to the other affected test(s) referenced around lines 210-226.
🧹 Nitpick comments (5)
installer/tests/unit/steps/test_migration.py (1)

137-144: Assert the migrated value, not just the key presence.
Right now the test only checks that "enable_python" exists, so a wrong value would still pass. Consider asserting the boolean value to strengthen coverage of the migration behavior.

✏️ Suggested assertion
-                assert "enable_python" in json.loads(new_config.read_text())
+                assert json.loads(new_config.read_text())["enable_python"] is True

As per coding guidelines, "Review test code briefly. Focus on: Test coverage for the feature being tested."

installer/steps/shell_config.py (1)

58-59: Detection pattern doesn't match fish shell syntax.

The check 'PATH="$HOME/.bun/bin' uses bash-style syntax with =, but fish uses set -gx PATH "$HOME/.bun/bin" $PATH (space, no equals). This means fish-only Bun PATH exports won't trigger has_old.

In practice this is mitigated since other markers (CLAUDE_ALIAS_MARKER, aliases) are usually present, but for consistency consider also detecting the fish variant:

Suggested fix
         or "function pilot" in content
-        or 'PATH="$HOME/.bun/bin' in content
+        or 'PATH="$HOME/.bun/bin' in content
+        or 'PATH "$HOME/.bun/bin' in content  # fish syntax
     )
console/src/services/sqlite/plans/types.ts (1)

25-33: Remove duplicate DashboardSession interface definition in useStats.ts.

This interface is redundantly defined in both console/src/services/sqlite/plans/types.ts (lines 25-33) and console/src/ui/viewer/hooks/useStats.ts (lines 54-62) with identical fields. The hook should import this type from the dedicated types file to maintain DRY and prevent divergence.

console/src/ui/viewer/hooks/useStats.ts (1)

54-62: Type duplication: DashboardSession is also defined in plans/types.ts.

This interface is identical to the one in console/src/services/sqlite/plans/types.ts (lines 24-32). Consider importing from a shared location to avoid duplication and potential drift.

♻️ Suggested approach

Either:

  1. Import from plans/types.ts if the UI can access the service layer types
  2. Create a shared types file that both can import from
  3. If separation is intentional (frontend/backend boundary), add a comment explaining why
console/src/services/worker/http/routes/PlanRoutes.ts (1)

16-23: Unused import: getAllActivePlans is imported but never used.

The function is imported from the plan store but no handler calls it.

♻️ Remove unused import
 import {
   associatePlan,
   getPlanForSession,
   getPlanByContentSessionId,
-  getAllActivePlans,
   updatePlanStatus,
   clearPlanAssociation,
 } from "../../../sqlite/plans/store.js";

Comment on lines +525 to +550
/**
* Migration 010 - Add session_plans table for session→plan associations
* Tracks which session is working on which plan file
*/
export const migration010: Migration = {
version: 10,
up: (db: Database) => {
db.run(`
CREATE TABLE IF NOT EXISTS session_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_db_id INTEGER NOT NULL UNIQUE,
plan_path TEXT NOT NULL,
plan_status TEXT DEFAULT 'PENDING',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (session_db_id) REFERENCES sdk_sessions(id) ON DELETE CASCADE
)
`);

console.log("✅ Created session_plans table for plan associations");
},

down: (db: Database) => {
db.run(`DROP TABLE IF EXISTS session_plans`);
},
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Duplicate migration: session_plans table is also created in SessionStore.ts with version 21.

This migration creates the session_plans table with version 10, but SessionStore.ts (lines 640-663) also creates the exact same table using schema version 21. This dual-migration approach creates several problems:

  1. Version mismatch: MigrationRunner uses version 10, SessionStore uses version 21
  2. Redundant code: The same DDL exists in two places
  3. Unpredictable behavior: Depending on which path runs first, the table may be created by either

The session_plans table should be created by only one mechanism. Consider removing the inline migration from SessionStore.ensureSessionPlansTable() and relying solely on migration010.

🤖 Prompt for AI Agents
In `@console/src/services/sqlite/migrations.ts` around lines 525 - 550, Duplicate
DDL creates session_plans in two places: migration010 (version 10) and
SessionStore.ensureSessionPlansTable (version 21); remove the inline table
creation from SessionStore.ensureSessionPlansTable and any schema version 21
references so the table is created only via migration010, ensuring you keep the
exact columns/constraints from migration010 (id, session_db_id UNIQUE,
plan_path, plan_status default, timestamps, FK to sdk_sessions) and update
SessionStore to rely on the migration-runner path (or call into the same
migration/apply routine) instead of running its own CREATE TABLE logic.

this.renameSessionIdColumns();
this.repairSessionIdColumnRename();
this.addFailedAtEpochColumn();
this.ensureSessionPlansTable();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove call if inline migration is removed.

If the duplicate ensureSessionPlansTable method is removed in favor of migration010, this call should also be removed.

🤖 Prompt for AI Agents
In `@console/src/services/sqlite/SessionStore.ts` at line 47, The constructor (or
initialization) currently calls ensureSessionPlansTable(); if you removed the
duplicate ensureSessionPlansTable method in favor of using migration010, remove
this invocation as well to avoid calling a now-missing or redundant method;
update the constructor/initializer where ensureSessionPlansTable is invoked so
it relies solely on migration010 for creating the session_plans table (reference
ensureSessionPlansTable and migration010 to locate and update the call).

Comment on lines +640 to +663
private ensureSessionPlansTable(): void {
const applied = this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(21) as
| SchemaVersion
| undefined;
if (applied) return;

this.db.run(`
CREATE TABLE IF NOT EXISTS session_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_db_id INTEGER NOT NULL UNIQUE,
plan_path TEXT NOT NULL,
plan_status TEXT DEFAULT 'PENDING',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (session_db_id) REFERENCES sdk_sessions(id) ON DELETE CASCADE
)
`);

this.db
.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)")
.run(21, new Date().toISOString());

logger.debug("DB", "Created session_plans table for plan associations");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Redundant inline migration duplicates migration010.

This method creates the same session_plans table that migration010 in migrations.ts already defines. The version numbers also conflict (21 here vs 10 in migrations.ts).

If the MigrationRunner is the authoritative migration system, this inline migration should be removed. If inline migrations in SessionStore are the preferred pattern, then migration010 should be removed from migrations.ts.

Given that migration010 is added to the formal migrations array, recommend removing this inline migration and relying on the MigrationRunner to apply migration010.

🛠️ Suggested fix: Remove duplicate inline migration
   private addFailedAtEpochColumn(): void {
     // ... existing code ...
   }

-  private ensureSessionPlansTable(): void {
-    const applied = this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(21) as
-      | SchemaVersion
-      | undefined;
-    if (applied) return;
-
-    this.db.run(`
-      CREATE TABLE IF NOT EXISTS session_plans (
-        id INTEGER PRIMARY KEY AUTOINCREMENT,
-        session_db_id INTEGER NOT NULL UNIQUE,
-        plan_path TEXT NOT NULL,
-        plan_status TEXT DEFAULT 'PENDING',
-        created_at TEXT NOT NULL,
-        updated_at TEXT NOT NULL,
-        FOREIGN KEY (session_db_id) REFERENCES sdk_sessions(id) ON DELETE CASCADE
-      )
-    `);
-
-    this.db
-      .prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)")
-      .run(21, new Date().toISOString());
-
-    logger.debug("DB", "Created session_plans table for plan associations");
-  }

Also remove the call from the constructor (line 47).

🤖 Prompt for AI Agents
In `@console/src/services/sqlite/SessionStore.ts` around lines 640 - 663, The
ensureSessionPlansTable inline migration in SessionStore (method
ensureSessionPlansTable) duplicates migration010 defined in migrations.ts and
uses a conflicting version number (21 vs 10); remove the entire
ensureSessionPlansTable method and its invocation from the SessionStore
constructor so the MigrationRunner and migrations.ts (migration010) are the
single source of truth for creating session_plans; ensure no other code
references ensureSessionPlansTable after removal and keep migration010 in the
migrations array as the authoritative migration.

- Remove dead ensureSessionPlansTable export from plans/store.ts
- Add session.py to MODULE_ORDER in build.py for Cython compilation
- Fix npm cleanup to use `npm root -g` for cross-platform compatibility
- Probe actual installed Claude version on npm fallback path
- Prefix unused mock params with _ to fix lint warnings
- Simplify topbar by removing search and processing components
- Replace prompts stat with last observation time on dashboard
- Rename plan status to Specification Status
- Add timestamps to spec viewer selector and metadata
- Add branded footer component to dashboard layout
@claude
Copy link

claude bot commented Feb 6, 2026

Claude finished @maxritter's task in 2m 32s —— View job


Code Review Complete ✅

I've completed a comprehensive review of PR #45. The PR introduces significant new features with generally solid architecture, but there are 3 critical issues that must be addressed before merge.


Overview

Changes: 86 files (+2849/-1328)

Features:

  1. Multi-session parallel support with PID-based isolation
  2. Spec command split into modular phases (plan/implement/verify)
  3. npm cleanup for stale temp directories
  4. Console dashboard with active session display

Commit 7b6560b addressed several issues from previous reviews, but critical problems remain.


🚨 Critical Issues (Must Fix Before Merge)

1. Duplicate Migration Creates Schema Version Conflict

Files:

  • console/src/services/sqlite/migrations.ts:529-550 (migration010, version 10)
  • console/src/services/sqlite/SessionStore.ts:640-662 (ensureSessionPlansTable, version 21)
  • console/src/services/sqlite/SessionStore.ts:47 (constructor call)

Problem: The session_plans table is created by two different mechanisms with conflicting version numbers:

  • migration010 in migrations.ts uses version 10
  • SessionStore.ensureSessionPlansTable() uses version 21
  • Both create identical DDL for the same table

Impact:

  • Unpredictable behavior depending on which runs first
  • Schema version tracking becomes unreliable
  • Future migrations may fail due to version conflicts
  • Violates single source of truth principle

Recommendation: Remove the duplicate inline migration from SessionStore. The migration010 is already in the migrations array and should be the sole source of truth.

Fix duplicate migration →


2. Python urllib API Bug in register_plan.py

File: pilot/scripts/register_plan.py:44

Code:

resp = urllib.request.urlopen(req, timeout=5)
return resp.status == 200

Problem: HTTPResponse.status does not exist in Python's urllib.request. The correct attribute is getcode().

Impact: This function always fails because it raises AttributeError when accessing resp.status. Plan-to-session associations silently fail despite the || true fallback in the bash script.

Evidence: This is a straightforward Python API error. The urllib.request.HTTPResponse object uses getcode() method, not a status attribute.

Recommendation: Change line 44 to return resp.getcode() == 200

Fix register_plan API bug →


3. Session ID Fallback Breaks Multi-Session Isolation

Files:

  • pilot/hooks/context_monitor.py:28
  • pilot/hooks/spec_stop_guard.py:35

Code:

session_id = os.environ.get("PILOT_SESSION_ID", "").strip() or "default"

Problem: When PILOT_SESSION_ID is not set or empty, all sessions fall back to "default". Multiple concurrent sessions would then share:

  • Context cache (~/.pilot/sessions/default/context-cache.json)
  • Stop guard state
  • Continuation files

Impact: This defeats the core multi-session parallel feature. If users run multiple sessions without the environment variable set, they will collide.

Recommendation: Either:

  1. Generate a unique session ID (e.g., f"default-{os.getpid()}")
  2. Raise an error if PILOT_SESSION_ID is not set
  3. Add validation and warnings

Fix session ID fallback →


⚠️ High Priority Issues (Should Fix)

4. Unused Import in PlanRoutes

File: console/src/services/worker/http/routes/PlanRoutes.ts:20

Problem: getAllActivePlans is imported but never called in any handler.

Recommendation: Remove the unused import.


5. Type Duplication in Dashboard

Files:

  • console/src/services/sqlite/plans/types.ts:25-33 (DashboardSession interface)
  • console/src/ui/viewer/hooks/useStats.ts:54-62 (duplicate definition)

Problem: DashboardSession interface is defined in two places with identical fields.

Recommendation: Import from the shared types file to maintain DRY principle.


✅ Positive Observations

  1. Path traversal protection works correctly - Lines 148-154 in PlanRoutes.ts properly validate paths are within docs/plans/
  2. npm cleanup fixed - Commit 7b6560b correctly uses npm root -g for cross-platform compatibility
  3. Excellent test coverage - plan-store.test.ts has comprehensive CRUD tests with 0% mocks
  4. Type safety - TypeScript types are well-defined throughout
  5. Defensive SQL - All queries use parameterized statements
  6. Foreign key constraints - Proper CASCADE on session deletion
  7. Installer improvements - Now probes actual Claude version on fallback
  8. Clean UI implementation - ActiveSessions component is well-structured

Test Plan Status

Based on PR description requirements:

  • ❓ All 374 launcher tests pass - Cannot verify (many launcher files encrypted)
  • ✅ All 459 console tests pass - Should pass (good coverage)
  • ✅ Type checking clean - No obvious type errors
  • ✅ Console type checking clean - Should pass
  • ⚠️ Multiple pilot sessions can run in parallel - BLOCKED by issue fix: make zsh the default shell #3 (session ID collision)
  • ✅ Statusline shows active session count - Implementation looks correct
  • ✅ Statusline shows current task name - Should work
  • ✅ Console dashboard displays active sessions - Implementation complete

Summary

This PR introduces important architectural improvements with generally good implementation quality. However, 3 critical bugs block merge:

  1. ⚠️ Duplicate migration (schema version conflict)
  2. ⚠️ Python API bug (register_plan.py line 44)
  3. ⚠️ Session ID collision (defeats multi-session isolation)

The first two are straightforward fixes. The third requires design decision on how to handle missing PILOT_SESSION_ID.

Recommendation: Fix these 3 critical issues before merge. The other issues can be addressed post-merge if needed.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@console/src/ui/viewer/views/Spec/index.tsx`:
- Around line 111-120: The periodic interval calls loadContent every
TIMING.SPEC_REFRESH_INTERVAL_MS and that loadContent unconditionally toggles the
isLoadingContent spinner, causing flicker; modify loadContent (or add a
loadContentSilent variant) to accept a silent boolean parameter that skips
setting isLoadingContent (and any UI-loading state) when true, keep the existing
behavior for user-initiated calls, and update the useEffect interval to call
loadContent(true) (or the silent variant) so background refreshes do not flip
the spinner while selectedSpec and content are refreshed in the background.
🧹 Nitpick comments (4)
console/src/ui/viewer/hooks/useStats.ts (1)

54-62: Consider importing DashboardSession from the backend types.

This interface duplicates DashboardSession from console/src/services/sqlite/plans/types.ts (lines 24-32). Importing it would reduce duplication and ensure consistency if the schema evolves.

That said, keeping a separate UI-layer type can be intentional for decoupling—skip if that's the design intent.

♻️ Suggested change
-export interface DashboardSession {
-  session_db_id: number;
-  content_session_id: string;
-  project: string;
-  status: string;
-  started_at: string;
-  plan_path: string | null;
-  plan_status: string | null;
-}
+export type { DashboardSession } from '../../services/sqlite/plans/types';
console/src/ui/viewer/views/Spec/index.tsx (1)

181-192: Deduplicate and harden modifiedAt formatting.
Formatting is duplicated and can render “Invalid Date” if the backend returns a malformed value. A small helper keeps formatting consistent and guards invalid inputs.

♻️ Suggested helper for modifiedAt formatting
 const statusConfig = {
   PENDING: { color: 'warning', icon: 'lucide:clock', label: 'In Progress' },
   COMPLETE: { color: 'info', icon: 'lucide:check-circle', label: 'Complete' },
   VERIFIED: { color: 'success', icon: 'lucide:shield-check', label: 'Verified' },
 } as const;

+const formatModifiedAt = (iso?: string) => {
+  if (!iso) return '';
+  const date = new Date(iso);
+  if (Number.isNaN(date.getTime())) return '';
+  return new Intl.DateTimeFormat(undefined, {
+    month: 'short',
+    day: 'numeric',
+    hour: '2-digit',
+    minute: '2-digit',
+  }).format(date);
+};
-            {specs.map((spec) => {
-              const date = spec.modifiedAt ? new Date(spec.modifiedAt) : null;
-              const dateStr = date
-                ? date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) +
-                  ' ' + date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
-                : '';
-              return (
+            {specs.map((spec) => {
+              const dateStr = formatModifiedAt(spec.modifiedAt);
+              return (
                 <option key={spec.filePath} value={spec.filePath}>
                   {spec.name} ({spec.status}){dateStr ? ` - ${dateStr}` : ''}
                 </option>
               );
             })}
-                      <span>{new Date(currentSpec.modifiedAt).toLocaleString(undefined, {
-                        month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
-                      })}</span>
+                      <span>{formatModifiedAt(currentSpec.modifiedAt)}</span>

Also applies to: 276-283

installer/steps/dependencies.py (2)

192-218: Avoid silent failures in stale-dir cleanup.

This is best-effort, but swallowing exceptions makes diagnosing ENOTEMPTY regressions difficult. Consider logging at debug level so failures are observable without blocking install.

🔧 Suggested change (add debug logging)
 def _clean_npm_stale_dirs() -> None:
@@
-    except Exception:
-        pass
+    except Exception as exc:
+        logging.getLogger(__name__).debug(
+            "Failed to clean npm stale dirs under `@anthropic-ai`: %s", exc
+        )
-import json
+import json
+import logging

As per coding guidelines, Review installer code for: Proper error handling and user feedback.


220-233: Normalize claude --version output before embedding in UI messages.

The returned string goes directly into user-facing messages. If the CLI output includes a prefix or uses stderr, it could display awkwardly. Extract the semantic version (X.Y.Z) and fall back to the raw string only if extraction fails.

Suggested implementation
 def _get_installed_claude_version() -> str | None:
     """Probe the actual installed Claude Code version via claude --version."""
     try:
         result = subprocess.run(
             ["claude", "--version"],
             capture_output=True,
             text=True,
         )
-        if result.returncode == 0 and result.stdout.strip():
-            return result.stdout.strip()
+        if result.returncode == 0:
+            output = (result.stdout or result.stderr or "").strip()
+            match = re.search(r"\b\d+\.\d+\.\d+\b", output)
+            return match.group(0) if match else (output or None)
     except Exception:
         pass
     return None

Add import re at the module level.

…iod, and refine hooks/UI

- Remove remote worker mode from console: delete remote-config, response types,
  file-edit handler; simplify endpoint config and settings to local-only
- Extract worker daemon initialization into testable EnsureWorkerDaemon module
  with dependency injection; hook commands now ensure daemon before executing
- Remove per-handler tryEnsureWorkerRunning guards (daemon ensured at entry)
- Add offline grace period for license validation with is_network_error flag
- Enhance context_monitor and spec_stop_guard hooks with session-scoped plan detection
- Add trivial edit detection to TDD enforcer (imports, removals, constants)
- Clean up console UI: remove unused SVG icons, Footer, ActiveSessions components;
  refine Spec viewer with active plan tabs and archived plan dropdown
- Add unit tests for EnsureWorkerDaemon and SessionQueueProcessor
- Delete dead register_plan.py script and test_register_plan.py
- Add plan registration docs to workflow-enforcement rules
@claude
Copy link

claude bot commented Feb 6, 2026

Claude finished @maxritter's task in 3m 25s —— View job


Code Review Complete ✅

I've completed a comprehensive review of PR #45. This PR introduces significant architectural improvements with multi-session parallel support, spec command phases, and npm cleanup fixes. While the implementation is generally solid, there is 1 critical issue that must be addressed before merge.


Overview

Files changed: 100 files (+3614/-2183)
Commits reviewed: 5 commits, including fixes from previous reviews (7b6560b, 71b7fc0)

Major features:

  1. Multi-session parallel support with PID-based session isolation
  2. Spec command split into modular phases (spec-plan, spec-implement, spec-verify)
  3. npm stale directory cleanup for ENOTEMPTY fix
  4. Console dashboard with active session tracking and plan associations

🚨 Critical Issue (Must Fix Before Merge)

1. Duplicate Migration Creates Schema Version Conflict

Files:

  • console/src/services/sqlite/migrations.ts:529-550 (migration010, version 10)
  • console/src/services/sqlite/SessionStore.ts:641-662 (ensureSessionPlansTable, version 21)
  • console/src/services/sqlite/SessionStore.ts:47 (constructor call)

Problem: The session_plans table is created by two different mechanisms with conflicting version numbers:

  1. migration010 in migrations.ts uses version 10 and is included in the migrations array
  2. ensureSessionPlansTable() in SessionStore.ts uses version 21 and is called in the constructor
  3. Both create identical DDL for the same table

Code Evidence:

In migrations.ts:529-550:

export const migration010: Migration = {
  version: 10,  // ← Version 10
  up: (db: Database) => {
    db.run(`CREATE TABLE IF NOT EXISTS session_plans (...)`);
  },
};

In SessionStore.ts:641-662:

private ensureSessionPlansTable(): void {
  const applied = this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(21) as  // ← Version 21
    | SchemaVersion | undefined;
  if (applied) return;
  this.db.run(`CREATE TABLE IF NOT EXISTS session_plans (...)`);  // Identical DDL
  this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(21, ...);
}

In SessionStore.ts:47:

constructor(dbPath: string) {
  // ...
  this.ensureSessionPlansTable();  // ← Always called
}

Impact:

  • Schema version tracking becomes unreliable - two different version numbers for the same table
  • Unpredictable behavior depending on initialization order
  • Future migrations may fail due to version conflicts
  • Violates single source of truth principle

Why This Matters: The migration system relies on version numbers to track what has been applied. Having the same table created under two different version numbers breaks this contract and creates technical debt that will cause issues during future schema evolution.

Recommendation: Remove the duplicate ensureSessionPlansTable() method and its call from the constructor. Keep only migration010 in the migrations array as the authoritative source.

Fix duplicate migration →


⚠️ High Priority Issues (Should Fix)

2. Session ID Collision Still Exists in Multiple Files

Files:

  • pilot/hooks/context_monitor.py:28
  • pilot/hooks/spec_stop_guard.py:35
  • pilot/rules/context-continuation.md:28

Code:

session_id = os.environ.get("PILOT_SESSION_ID", "").strip() or "default"

Problem: When PILOT_SESSION_ID is not set or is empty, all sessions fall back to "default". This means multiple concurrent sessions would share:

  • Context cache at ~/.pilot/sessions/default/context-cache.json
  • Stop guard state
  • Continuation files
  • Active plan JSON

Impact: This defeats the core multi-session parallel feature. Users running multiple sessions without the environment variable properly set will experience state collisions, with sessions interfering with each other's context and plan tracking.

Recommendation: Generate a unique session ID if not provided (e.g., f"default-{os.getpid()}") or raise a clear error with instructions.

Fix session ID collision →


3. Encrypted Launcher Files Block Review

Files: All Python files in launcher/ directory (e.g., session.py, auth.py, cli.py)

Problem: These files show as binary data with git-crypt headers:

00 47 49 54 43 52 59 50 54 00 ...  (GITCRYPT header)

Impact:

  • Cannot review core session management implementation
  • Cannot verify multi-session isolation logic
  • Contributors without git-crypt setup cannot work on launcher code
  • No documentation explaining encryption requirement

Recommendation: Either:

  1. Add clear documentation in README about git-crypt requirements and setup
  2. Consider whether launcher code truly requires encryption (it appears to be application logic, not secrets)
  3. Ensure all contributors have decryption access

4. Unused Import in PlanRoutes

File: console/src/services/worker/http/routes/PlanRoutes.ts:20

Code:

import {
  associatePlan,
  getPlanForSession,
  getPlanByContentSessionId,
  getAllActivePlans,  // ← Never used
  updatePlanStatus,
  clearPlanAssociation,
} from "../../../sqlite/plans/store.js";

Problem: getAllActivePlans is imported but never called in any handler.

Recommendation: Remove the unused import.

Fix unused import →


5. Type Duplication Between Backend and Frontend

Files:

  • console/src/services/sqlite/plans/types.ts:25-33 (DashboardSession interface)
  • console/src/ui/viewer/hooks/useStats.ts:54-62 (duplicate definition)

Problem: DashboardSession interface is defined identically in two places. This creates maintenance burden and risk of divergence.

Recommendation: Import the type from the shared types file or create a shared UI types module if frontend/backend separation is intentional.


6. Background Refresh May Flash Spinner

File: console/src/ui/viewer/views/Spec/index.tsx:95-112

Code:

const loadContent = useCallback(async (filePath: string, background = false) => {
  if (!background) {
    setIsLoadingContent(true);
  }
  // ... fetch logic
  } finally {
    setIsLoadingContent(false);  // ← Always sets to false
  }
}, []);

Problem: The finally block always sets isLoadingContent(false), even for background refreshes. If a user-initiated load is in progress when a background refresh completes, the spinner disappears prematurely.

Recommendation: Only set isLoadingContent(false) when !background, matching the loading logic.


✅ Positive Observations

This PR demonstrates several strengths:

  1. ✓ Path traversal protection works correctly - PlanRoutes.ts:148-154 properly validates paths are within docs/plans/
  2. ✓ npm cleanup fixed - Commit 7b6560b correctly uses npm root -g for cross-platform compatibility (instead of npm prefix -g)
  3. ✓ Excellent test coverage - plan-store.test.ts has comprehensive CRUD tests with 0% mocks, testing real SQLite behavior
  4. ✓ Defensive SQL - All queries use parameterized statements, preventing SQL injection
  5. ✓ Foreign key constraints - Proper CASCADE on session deletion ensures referential integrity
  6. ✓ Installer improvements - Now probes actual Claude version on npm install failure (line 255)
  7. ✓ Background refresh implemented - The loadContent function accepts a background parameter to prevent UI flicker (good architecture, minor bug in finally block)
  8. ✓ Session isolation architecture - The PID-based session directory approach (~/.pilot/sessions/{PILOT_SESSION_ID}/) is sound
  9. ✓ Type safety - TypeScript types are well-defined throughout
  10. ✓ Clean UI implementation - Dashboard components and SSE integration are well-structured

📋 Previous Review Items - Status Update

Comparing with previous reviews:

✅ Fixed Issues:

  • npm cleanup cross-platform - Now uses npm root -g correctly (commit 7b6560b)
  • Installer fallback - Now probes actual Claude version when npm fails (commit 7b6560b)
  • Test parameter naming - Unused mock parameters prefixed with _ (commit 7b6560b)
  • Background refresh flicker - loadContent now accepts background parameter (commit 71b7fc0)
  • Path traversal protection - Properly validates paths within docs/plans/

❌ Still Present:


Test Plan Status

Based on PR description requirements:

  • All 374 launcher tests pass - Cannot verify (launcher files are encrypted)
  • All 459 console tests pass - Should pass (excellent test coverage observed)
  • Type checking clean - No obvious type errors found
  • Console type checking clean - Types are well-defined
  • ⚠️ Multiple pilot sessions can run in parallel - BLOCKED by issue chore(docs): fix readme rendering #2 (session ID collision)
  • Statusline shows active session count - Implementation looks correct
  • Statusline shows current task name - Should work
  • Console dashboard displays active sessions - Implementation complete and tested

Summary & Recommendation

This PR introduces important architectural improvements with generally high-quality implementation. The multi-session parallel support, spec phase splitting, and dashboard features are well-designed and thoroughly tested.

However, 1 critical issue blocks merge:

  1. ⚠️ Duplicate migration with conflicting versions (blocks merge)

And 2 high-priority issues should be addressed:

  1. ⚠️ Session ID collision defeats multi-session isolation (defeats core feature)
  2. ⚠️ Encrypted launcher files block code review (transparency concern)

Recommendation:

The other issues (#4, #5, #6) can be addressed post-merge if needed, though they're quick fixes.


Overall Assessment: Strong PR with excellent architecture and testing. The critical migration issue and session ID collision are the only blockers. Once those are resolved, this will be a solid addition to the codebase.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@console/src/services/worker/http/routes/PlanRoutes.ts`:
- Around line 205-238: The handlers handleAssociatePlan and
handleAssociatePlanByContentId currently pass req.body.status directly to
associatePlan; add validation against a defined whitelist of allowed status
strings (e.g., an allowedStatuses constant or enum) before calling
associatePlan, and return a 400 Bad Request with a clear message when
req.body.status is missing or not one of the allowed values; apply the same
check to the other similar handler mentioned (lines ~274-283) so no invalid
status is persisted.

In `@console/src/ui/viewer/components/SummaryCard.tsx`:
- Around line 46-48: The Icon used in SummaryCard is decorative and should
support accessibility attributes so screen readers don't read it redundantly;
update the Icon component to accept and pass through aria-hidden and aria-label
props (and any other native SVG/HTML aria attrs) and then set aria-hidden="true"
on the Icon instance in SummaryCard (where Icon is rendered with
SECTION_ICONS[section.key]) so the decorative icon is ignored by assistive tech
while still allowing consumers to provide an aria-label when needed.

In `@console/src/ui/viewer/views/Dashboard/PlanStatus.tsx`:
- Around line 76-78: The Plan rows use plan.name as the React key which can
collide; update the PlanInfo type in useStats.ts to include an optional
filePath: string property and ensure the hook returns the backend-provided
filePath, then in PlanStatus.tsx change the map key to use plan.filePath
(falling back to plan.name if missing) when rendering <PlanRow ... /> so each
row has a stable unique key; update references to PlanInfo and the return value
of the useStats hook accordingly.

In `@console/tests/queue/session-queue-processor.test.ts`:
- Around line 46-51: Tests use real setTimeout to coordinate emitting messages
and aborting; replace these timers with deterministic sequencing by calling the
async iterator's next() to establish the pending wait, then immediately trigger
the event and abort. Concretely, in this test replace the initial setTimeout
block with: call queue[Symbol.asyncIterator]() to get the iterator, call
iterator.next() and await a microtask (or capture the pending promise) before
invoking store.claimAndDelete = () => makeMessage(SESSION_ID) and
events.emit("message"); then await the iterator result and finally call
controller.abort() (or await a subsequent iterator.next() and then abort) so
tests rely on iterator.next() promises instead of setTimeout; apply the same
pattern to the other tests that use setTimeout (the blocks around
controller.abort() and events.emit()).
🧹 Nitpick comments (3)
console/src/ui/viewer/hooks/useStats.ts (2)

32-40: Local PlanInfo is missing fields present in the canonical type.

The PlanInfo interface here omits filePath and modifiedAt properties that exist in PlanRoutes.ts (lines 31-41). If the API returns those fields, they'll be silently dropped. Consider importing the shared type or aligning the interfaces to avoid drift.

💡 Suggested alignment
 interface PlanInfo {
   name: string;
   status: 'PENDING' | 'COMPLETE' | 'VERIFIED';
   completed: number;
   total: number;
   phase: 'plan' | 'implement' | 'verify';
   iterations: number;
   approved: boolean;
+  filePath?: string;
+  modifiedAt?: string;
 }

Alternatively, import from PlanRoutes.ts if it's exported and accessible.


102-104: Minor duplication in activity data extraction.

Lines 102-103 and 129 perform identical extraction logic. This is a minor redundancy that could be consolidated, but it's fine to leave as-is given the small scope.

Also applies to: 129-129

console/src/ui/viewer/components/SummaryCard.tsx (1)

9-23: Type section keys to prevent undefined icon lookups.

Record<string, string> won’t catch missing/typoed keys and SECTION_ICONS[section.key] can be undefined. Consider a SectionKey union to keep SECTION_ICONS and sections in sync.

♻️ Proposed refactor
-const SECTION_ICONS: Record<string, string> = {
+type SectionKey = "investigated" | "learned" | "completed" | "next_steps";
+const SECTION_ICONS: Record<SectionKey, string> = {
   investigated: "lucide:search",
   learned: "lucide:lightbulb",
   completed: "lucide:check-circle",
   next_steps: "lucide:arrow-right-circle",
 };

   const sections = [
-    { key: "investigated", label: "Investigated", content: summary.investigated },
-    { key: "learned", label: "Learned", content: summary.learned },
-    { key: "completed", label: "Completed", content: summary.completed },
-    { key: "next_steps", label: "Next Steps", content: summary.next_steps },
+    { key: "investigated", label: "Investigated", content: summary.investigated },
+    { key: "learned", label: "Learned", content: summary.learned },
+    { key: "completed", label: "Completed", content: summary.completed },
+    { key: "next_steps", label: "Next Steps", content: summary.next_steps },
-  ].filter((section) => section.content);
+  ] as { key: SectionKey; label: string; content: string | null }[];
+  const visibleSections = sections.filter((section) => section.content);

Comment on lines +46 to +48
<Icon
icon={SECTION_ICONS[section.key]}
size={18}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.tsx" -o -name "*.ts" | head -20

Repository: maxritter/claude-pilot

Length of output: 807


🏁 Script executed:

fd -t f "Icon" --extension tsx --extension ts | grep -i icon | head -20

Repository: maxritter/claude-pilot

Length of output: 110


🏁 Script executed:

rg -l "export (function|const) Icon" --type=tsx --type=ts

Repository: maxritter/claude-pilot

Length of output: 93


🏁 Script executed:

cat -n console/src/ui/viewer/components/ui/Icon.tsx

Repository: maxritter/claude-pilot

Length of output: 530


🏁 Script executed:

cat -n console/src/ui/viewer/components/SummaryCard.tsx

Repository: maxritter/claude-pilot

Length of output: 2916


Add accessibility support to Icon component for decorative icons.

The Icon component does not currently support aria-hidden or other accessibility attributes. Since the Icon is used decoratively alongside a section heading in SummaryCard (line 51), it should be marked as aria-hidden="true" to prevent screen readers from announcing it separately and causing redundant announcements with the h3 label.

Update the Icon component to accept and pass through aria-hidden and aria-label props for flexible accessibility handling.

🤖 Prompt for AI Agents
In `@console/src/ui/viewer/components/SummaryCard.tsx` around lines 46 - 48, The
Icon used in SummaryCard is decorative and should support accessibility
attributes so screen readers don't read it redundantly; update the Icon
component to accept and pass through aria-hidden and aria-label props (and any
other native SVG/HTML aria attrs) and then set aria-hidden="true" on the Icon
instance in SummaryCard (where Icon is rendered with SECTION_ICONS[section.key])
so the decorative icon is ignored by assistive tech while still allowing
consumers to provide an aria-label when needed.

Comment on lines +46 to +51
setTimeout(() => {
store.claimAndDelete = () => makeMessage(SESSION_ID) as any;
events.emit("message");
}, 10);
setTimeout(() => controller.abort(), 50);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reduce timing-based flakiness in async iterator tests.

These cases depend on real setTimeout delays to coordinate events/aborts, which can be nondeterministic on slow CI. Prefer deterministic coordination (e.g., iterator.next() then emit once the wait is established) to avoid timer-based flakes.

✅ Example approach (first test)
-      setTimeout(() => {
-        store.claimAndDelete = () => makeMessage(SESSION_ID) as any;
-        events.emit("message");
-      }, 10);
-      setTimeout(() => controller.abort(), 50);
-
-      const results: any[] = [];
-      for await (const msg of processor.createIterator(SESSION_ID, controller.signal)) {
-        results.push(msg);
-        controller.abort();
-      }
+      const iterator = processor.createIterator(SESSION_ID, controller.signal);
+      const pending = iterator.next(); // waitForMessage listener installed
+      store.claimAndDelete = () => makeMessage(SESSION_ID) as any;
+      events.emit("message");
+      const { value, done } = await pending;
+      const results: any[] = [];
+      if (!done && value) results.push(value);
+      controller.abort();

Also applies to: 101-105, 134-150

🤖 Prompt for AI Agents
In `@console/tests/queue/session-queue-processor.test.ts` around lines 46 - 51,
Tests use real setTimeout to coordinate emitting messages and aborting; replace
these timers with deterministic sequencing by calling the async iterator's
next() to establish the pending wait, then immediately trigger the event and
abort. Concretely, in this test replace the initial setTimeout block with: call
queue[Symbol.asyncIterator]() to get the iterator, call iterator.next() and
await a microtask (or capture the pending promise) before invoking
store.claimAndDelete = () => makeMessage(SESSION_ID) and events.emit("message");
then await the iterator result and finally call controller.abort() (or await a
subsequent iterator.next() and then abort) so tests rely on iterator.next()
promises instead of setTimeout; apply the same pattern to the other tests that
use setTimeout (the blocks around controller.abort() and events.emit()).

…r search backend

Session continuation fix:
- Context monitor now prints resolved absolute path instead of
  literal $PILOT_SESSION_ID, preventing Write tool from using
  wrong session directory
- Updated context-continuation rule with explicit path resolution
  instructions and stale file cleanup section
- Added tests for _get_continuation_path and resolved path output

Console UI cleanup:
- Replace VectorDbStatus with VexorStatus widget (codebase indexing
  status, reindex button, polling for completion)
- Add VexorRoutes backend with search, status, and reindex endpoints
  including per-command timeouts and concurrency limits
- Add CodebaseResultCard and codebase search tab in Search view
  with AbortController timeout and error handling
- Remove projects panel from sidebar, layout, and command palette
- Replace favicon images with emoji SVG, remove Monaspace Radon font
- Delete legacy viewer-template.html, unused images, fonts, and
  orphaned components (SearchFilters, SidebarProjects, useSpinningFavicon)
- Rebuild viewer bundle

Other changes:
- Add RetentionScheduler, cross-session isolation tests, and
  observation compiler isolation tests
- Update spec commands, workflow enforcement rules, and plugin config
- Refine worker service, wrapper, and installer dependencies
@claude
Copy link

claude bot commented Feb 6, 2026

Claude finished @maxritter's task in 3m 51s —— View job


Code Review Complete ✅

I've completed a comprehensive review of PR #45. This is a substantial PR introducing multi-session parallel support, spec command phases, and npm cleanup. The implementation is generally solid, and previous critical issues have been resolved. However, there is 1 remaining critical issue and several recommendations.


Overview

PR Summary:

  • Files changed: 100 files (+3,614/-2,183)
  • Commits: 6 commits including fixes from previous reviews
  • Major features:
    1. Multi-session parallel support with PID-based session isolation
    2. Spec command split into modular phases (spec-plan, spec-implement, spec-verify)
    3. npm stale directory cleanup for ENOTEMPTY fix
    4. Console dashboard with active session tracking

🚨 Critical Issue (Must Fix Before Merge)

1. Session ID Collision Risk - Multi-Session Isolation Broken

Files:

  • pilot/hooks/context_monitor.py:28
  • pilot/hooks/spec_stop_guard.py:35 (line 43 also affected)
  • pilot/rules/context-continuation.md:28

Problem: When PILOT_SESSION_ID is not set or is empty, all sessions fall back to "default":

session_id = os.environ.get("PILOT_SESSION_ID", "").strip() or "default"

Impact: This defeats the core multi-session parallel feature. If users run multiple sessions without properly setting the environment variable:

  • All sessions share the same context cache at ~/.pilot/sessions/default/context-cache.json
  • Stop guard state collides
  • Continuation files interfere
  • Active plan JSON conflicts

Why This Matters: The PR's main selling point is "Run multiple Pilot sessions in the same project without interference." This fallback makes that promise unreliable.

Recommendation: Generate a unique session ID when not provided:

session_id = os.environ.get("PILOT_SESSION_ID", "").strip() or f"default-{os.getpid()}"

Or alternatively, raise a clear error with instructions if the variable is missing in multi-session scenarios.

Fix session ID collision →


✅ Previously Identified Issues - RESOLVED

Good news! The following critical issues from previous reviews have been successfully fixed:

  1. Duplicate migration removed - The conflicting migration010 vs ensureSessionPlansTable issue has been resolved. Now using a single authoritative migration in SessionStore.ts (version 21) with proper documentation in migrations.ts:527-529.

  2. npm cleanup cross-platform fix - Now correctly uses npm root -g instead of npm prefix -g (line 201 in dependencies.py), resolving Windows compatibility issues.

  3. Actual version detection on npm failure - When npm install fails but claude command exists, the code now probes the actual installed version via claude --version (lines 220-256).

  4. Plan status validation - PlanRoutes.ts now validates status values against a whitelist before persisting (lines 54-58, 215-217, 234-236, 292-294).

  5. Background refresh spinner fix - The loadContent function in Spec/index.tsx properly handles background refreshes with a background parameter (lines 95-115), preventing UI flicker.

  6. Stable React keys - PlanStatus.tsx:78 now uses plan.filePath as the key with fallback to prevent collisions.


⚠️ High Priority Issues (Should Address)

2. Encrypted Launcher Files Block Full Review

Files: launcher/session.py, launcher/auth.py, launcher/cli.py, and others

Evidence:

$ head -c 20 launcher/session.py | od -An -tx1
 00 47 49 54 43 52 59 50 54 00 e3 97 96 a6 2d d2

The GITCRYPT header (bytes 47 49 54 43 52 59 50 54) confirms these files are encrypted.

Impact:

  • Cannot review core session management implementation
  • Cannot verify multi-session isolation logic in launcher
  • Contributors without git-crypt setup cannot modify launcher code
  • No documentation explaining encryption requirements

Recommendation:

  • Add clear documentation in README about git-crypt requirements and setup
  • Consider whether launcher application logic truly requires encryption (vs secrets)
  • Ensure all contributors have decryption access
  • Document which files are encrypted and why

3. Session ID Documentation Mismatch

File: pilot/rules/context-continuation.md:28

Current text:

"defaults to "default" if not set"

Problem: This documentation matches the current buggy behavior. Once issue #1 is fixed, this documentation will be incorrect.

Recommendation: Update the documentation to reflect the PID-based fallback after fixing the code.


📋 Medium Priority Issues

4. Error Handling in npm Cleanup Could Be More Informative

File: installer/steps/dependencies.py:216-217

Code:

except Exception:
    pass

Issue: Silently swallowing all exceptions makes debugging ENOTEMPTY regressions difficult.

Recommendation: Add debug-level logging so failures are observable without blocking installation:

except Exception as exc:
    logging.getLogger(__name__).debug(
        "Failed to clean npm stale dirs: %s", exc
    )

5. Test Timing Dependencies Could Cause Flakiness

File: console/tests/queue/session-queue-processor.test.ts:93-96

Code:

setTimeout(() => {
  (store as any).claimAndDelete = () => makeMessage(SESSION_ID, 1) as any;
  events.emit("message");
}, 10);

Issue: Tests use real setTimeout delays which can be nondeterministic on slow CI.

Recommendation: Use deterministic coordination via async iterator promises instead of timers. While tests currently pass, this pattern can cause intermittent failures.


6. Version Output Should Be Normalized

File: installer/steps/dependencies.py:228-229

Code:

if result.returncode == 0 and result.stdout.strip():
    return result.stdout.strip()

Issue: The raw output goes directly into user-facing messages. If the CLI includes a prefix or uses stderr, it could display awkwardly.

Recommendation: Extract semantic version (X.Y.Z) using regex and fall back to raw string only if extraction fails.


🎯 Low Priority / Style Issues

7. Icon Component Accessibility

File: console/src/ui/viewer/components/SummaryCard.tsx:46-50

Issue: The Icon component doesn't support aria-hidden for decorative icons, causing redundant screen reader announcements.

Recommendation: Update Icon component to accept and pass through accessibility attributes.


8. Type Safety for Section Keys

File: console/src/ui/viewer/components/SummaryCard.tsx:9

Issue: Record<string, string> allows missing keys, making SECTION_ICONS[section.key] potentially undefined.

Recommendation: Define a SectionKey union type for stricter type checking.


✨ Positive Observations

This PR demonstrates several strengths:

  1. Excellent test coverage - plan-store.test.ts has 0% mocks, testing real SQLite behavior
  2. Defensive SQL - All queries use parameterized statements preventing SQL injection
  3. Foreign key constraints - Proper CASCADE on session deletion for referential integrity
  4. Clean architecture - Session isolation via PID-based directories is well-designed
  5. Path traversal protection - PlanRoutes.ts:148-154 properly validates paths within docs/plans/
  6. Type safety - TypeScript types are comprehensive and well-defined
  7. Comprehensive error handling - The worker daemon infrastructure handles version mismatches gracefully
  8. Good documentation - Workflow enforcement rules are clear
  9. Modular design - Phase skills are properly separated
  10. Responsive to feedback - Previous review findings have been systematically addressed

📊 Test Plan Status

Based on PR description requirements:

  • All 374 launcher tests pass - Cannot verify (launcher files encrypted)
  • All 459 console tests pass - Should pass (excellent test coverage observed)
  • Type checking clean - No obvious type errors
  • Console type checking clean - Types are well-defined
  • ⚠️ Multiple pilot sessions can run in parallel - BLOCKED by issue chore: fix setup docs #1 (session ID collision)
  • Statusline shows active session count - Implementation correct
  • Statusline shows current task name - Should work
  • Console dashboard displays active sessions - Tested and working

Summary & Recommendation

This PR introduces important architectural improvements with high implementation quality. The multi-session parallel support, spec phase splitting, and dashboard features are well-designed and thoroughly tested.

However, 1 critical issue blocks merge:

  1. ⚠️ Session ID collision - Defeats multi-session isolation (issue chore: fix setup docs #1)

High-priority concerns:

  1. ⚠️ Encrypted launcher files - Block full code review and contributor access
  2. ⚠️ Documentation mismatch - Needs update after fixing issue chore: fix setup docs #1

Recommendation:

Overall Assessment: Strong PR with excellent architecture and thorough testing. The session ID collision is the only blocker, and it's a straightforward fix. Once resolved, this will be a valuable addition to the codebase.


@maxritter maxritter merged commit 15e48f9 into main Feb 6, 2026
10 of 11 checks passed
github-actions bot pushed a commit that referenced this pull request Feb 6, 2026
# [6.2.0](v6.1.1...v6.2.0) (2026-02-06)

### Bug Fixes

* address PR [#45](#45) review findings and refine console UI ([7b6560b](7b6560b))
* clean stale npm temp dirs before Claude Code install and block Explore agent ([e674c01](e674c01))
* remove dead code, unused imports, and legacy integrations ([e3357ae](e3357ae))
* remove remote mode, extract worker daemon, add offline grace period, and refine hooks/UI ([71b7fc0](71b7fc0))
* resolve continuation path bug, clean up console UI, and add Vexor search backend ([b255702](b255702))
* split spec command into phases, add design skill, and optimize skill descriptions ([786ab2a](786ab2a))

### Features

* add multi-session parallel support with isolated session state ([8fbb34f](8fbb34f))
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

🎉 This PR is included in version 6.2.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant