feat(cli): add read-only context for lightweight query commands#100
feat(cli): add read-only context for lightweight query commands#100
Conversation
- Add error reporting when --history fetch fails in schedule get (was silently swallowed) - Replace inline import() type with proper top-level ScheduleExecution import
Implement ReadOnlyContext module that skips EventBus, handlers, and WorkerPool for query-only commands (status, logs, list, schedule list/get), reducing startup time from 200-500ms to ~50ms. Changes: - ReadOnlyContext interface with Database + 4 repositories - createReadOnlyContext() factory that bypasses full bootstrap - Query commands (logs, status) now use withReadOnlyContext() - Mutation commands still use full bootstrap via withServices() - Added skipRecovery flag to BootstrapOptions for short-lived CLI commands - 8 new tests covering context creation, round-trip data, and error handling Performance impact: - Query commands: ~200-400ms faster (no EventBus, handlers, WorkerPool) - Mutation commands: Unchanged (still use full bootstrap) - MCP server: Unchanged (full bootstrap with recovery/schedule executor) Related issue: #90 Co-Authored-By: Claude <noreply@anthropic.com>
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
CLI[CLI Command] --> Q{Query or\nMutation?}
Q -- "status / logs /\nschedule list|get" --> ROC[withReadOnlyContext]
Q -- "run / cancel / retry /\nschedule create|cancel|pause|resume" --> WS[withServices]
ROC --> CRC[createReadOnlyContext]
CRC --> CFG[loadConfiguration]
CRC --> DB[(Database\nSQLite)]
DB --> TR[SQLiteTaskRepository]
DB --> OR[SQLiteOutputRepository]
DB --> SR[SQLiteScheduleRepository]
CRC --> CTX[ReadOnlyContext\ntaskRepository\noutputRepository\nscheduleRepository\nclose]
WS --> BS[bootstrap\nskipRecovery: true\nskipScheduleExecutor: true]
BS --> EB[EventBus]
BS --> WP[WorkerPool]
BS --> TM[TaskManager]
BS --> SS[ScheduleService]
BS --> HDLRS[Event Handlers]
CTX --> CMD1[getTaskStatus]
CTX --> CMD2[getTaskLogs]
CTX --> CMD3[scheduleList]
CTX --> CMD4[scheduleGet]
Last reviewed commit: "fix(cli): write task..." |
src/cli/read-only-context.ts
Outdated
| import { SQLiteTaskRepository } from '../implementations/task-repository.js'; | ||
|
|
||
| export interface ReadOnlyContext { | ||
| readonly database: Database; |
There was a problem hiding this comment.
[Architecture] DIP Violation: Concrete Database class on interface
The readonly database: Database exposes the concrete Database class instead of an abstraction. This breaks the Dependency Inversion Principle used throughout the codebase where repository interfaces are abstracted in src/core/interfaces.ts.
Impact:
- Callers are coupled to a specific SQLite implementation
OutputRepositoryis imported from the implementation layer instead of core interfaces
Suggested Fix - Option A (remove database if not needed):
export interface ReadOnlyContext {
readonly taskRepository: TaskRepository;
readonly outputRepository: OutputRepository;
readonly scheduleRepository: ScheduleRepository;
close(): void;
}Option B (expose narrow lifecycle interface):
interface Closeable { close(): void; isOpen(): boolean; }
export interface ReadOnlyContext {
readonly database: Closeable;
readonly taskRepository: TaskRepository;
readonly outputRepository: OutputRepository;
readonly scheduleRepository: ScheduleRepository;
}
src/cli/read-only-context.ts
Outdated
| */ | ||
|
|
||
| import { loadConfiguration } from '../core/configuration.js'; | ||
| import { ScheduleRepository, TaskRepository } from '../core/interfaces.js'; |
There was a problem hiding this comment.
[TypeScript] Missing import type for interface-only imports
Repository interfaces are only used as type annotations in ReadOnlyContext, but imported as values. Use import type to clarify intent:
import type { ScheduleRepository, TaskRepository } from '../core/interfaces.js';
import type { OutputRepository } from '../implementations/output-repository.js';
import { SQLiteOutputRepository } from '../implementations/output-repository.js';This follows the TypeScript skill guideline: "Type-only imports for types."
tests/unit/read-only-context.test.ts
Outdated
| import { tmpdir } from 'os'; | ||
| import { join } from 'path'; | ||
| import { afterEach, beforeEach, describe, expect, it } from 'vitest'; | ||
| import { createReadOnlyContext, ReadOnlyContext } from '../../src/cli/read-only-context.js'; |
There was a problem hiding this comment.
[Code Quality] Unused type import
ReadOnlyContext is imported but never used as a type annotation. It only appears in the describe('ReadOnlyContext', ...) string.
Fix:
import { createReadOnlyContext } from '../../src/cli/read-only-context.js';
src/cli/commands/status.ts
Outdated
| try { | ||
| s.start(taskId ? `Fetching status for ${taskId}...` : 'Fetching tasks...'); | ||
| const { taskManager } = await withServices(s); | ||
| const ctx = withReadOnlyContext(s); |
There was a problem hiding this comment.
[Architecture] Database connection not closed
The ReadOnlyContext opens a database connection but never closes it before process.exit(). While the OS cleanup is fine for short-lived CLI commands, this violates the project's "Resource cleanup - Always use try/finally" principle from CLAUDE.md.
The test suite (read-only-context.test.ts) correctly calls ctx.database.close() in every test, establishing an expectation not met by production code.
Fix - Option 1 (add finally block):
const ctx = withReadOnlyContext(s);
try {
// ... existing logic ...
} finally {
ctx.database.close();
}Option 2 (document intentional reliance on process.exit):
Add a comment explaining that process.exit() handles cleanup for short-lived CLI processes.
| if (result.ok) { | ||
| const logs = result.value; | ||
| // Validate task exists | ||
| const taskResult = await ctx.taskRepository.findById(TaskId(taskId)); |
There was a problem hiding this comment.
[Performance] Redundant sequential database queries
The task existence check (line 13) is followed by an output fetch (line 26), but the output fetch already returns null when no data exists. This creates an unnecessary database round-trip.
Impact: Minor overhead (~1-5ms per invocation on SQLite), but the validation is redundant.
Fix - Option 1 (remove task validation):
const outputResult = await ctx.outputRepository.get(TaskId(taskId));
if (!outputResult.ok) {
s.stop('Failed');
ui.error(`Failed to get task logs: ${outputResult.error.message}`);
process.exit(1);
}
if (!outputResult.value) {
s.stop('Not found');
ui.error('No task output captured — task may not exist');
process.exit(1);
}Option 2 (parallel queries):
const [taskResult, outputResult] = await Promise.all([
ctx.taskRepository.findById(TaskId(taskId)),
ctx.outputRepository.get(TaskId(taskId)),
]);
src/cli/commands/schedule.ts
Outdated
| @@ -265,10 +275,9 @@ async function scheduleList(service: ScheduleService, scheduleArgs: string[]) { | |||
| const { ScheduleStatus } = await import('../../core/domain.js'); | |||
| const statusEnum = status ? (status as keyof typeof ScheduleStatus) : undefined; | |||
There was a problem hiding this comment.
[Security/TypeScript] Unsafe enum value cast without validation
The status value is cast directly to the enum key without validating that it's actually a valid member. Passing --status constructor or --status __proto__ would lookup inherited Object properties rather than enum values.
Impact: LOW — parameterized SQL queries prevent injection, but passing invalid status returns undefined, causing unexpected behavior.
Fix:
const validStatuses = Object.keys(ScheduleStatus).map(k => k.toLowerCase());
if (status && !validStatuses.includes(status.toLowerCase())) {
ui.error(`Invalid status: ${status}. Valid: ${validStatuses.join(', ')}`);
process.exit(1);
}
const statusEnum = status ? (status.toUpperCase() as keyof typeof ScheduleStatus) : undefined;
const result = statusEnum
? await repo.findByStatus(ScheduleStatus[statusEnum], limit)
: await repo.findAll(limit);Note: This is a pre-existing pattern from the old service.listSchedules() code, but worth fixing in the refactored version.
tests/unit/read-only-context.test.ts
Outdated
| expect(findResult.ok).toBe(true); | ||
| if (!findResult.ok) return; | ||
| expect(findResult.value).not.toBeNull(); | ||
| expect(findResult.value!.prompt).toBe('test read-only context'); |
There was a problem hiding this comment.
[TypeScript] Non-null assertions instead of null guards
The test file uses ! non-null assertions (e.g., findResult.value!.prompt) instead of null guards. While each assertion is preceded by an expect().not.toBeNull() check, TypeScript's type narrowing doesn't recognize Vitest assertions.
Consistency Issue: The same file correctly uses if (!result.ok) return; guards in other tests, but switches to ! assertions in the loop. This is inconsistent.
Fix:
const task = findResult.value;
if (!task) return;
expect(task.prompt).toBe('test read-only context');
expect(task.status).toBe(TaskStatus.QUEUED);This follows the codebase's own guard pattern used elsewhere in the file.
Code Review SummaryReview Date: 2026-03-18 Review Results
Issues SummaryInline Comments Created (≥80% Confidence)✅ 8 blocking/should-fix issues identified and commented inline:
Lower-Confidence Findings (60-79%)These are suggestions rather than blocking issues — correct but verbose code, or intentional patterns:
Deduplication NotesMultiple reviewers flagged the same issues:
All inline comments reference the first instance and highest-confidence finding. Next StepsBefore Merge:
Post-Merge (Follow-up Issues):
Review comments generated by Claude Code — See inline discussions for code-specific feedback. |
Replace concrete Database exposure in ReadOnlyContext interface with close() method. Callers no longer depend on Database implementation details. Also switch to import type for repository interfaces used only as type annotations. Co-Authored-By: Claude <noreply@anthropic.com>
…-context tests Replace `value!.property` non-null assertions with proper null guard pattern (`if (!value) return`) at two locations, matching the existing Result-type guard convention used elsewhere in the file. Co-Authored-By: Claude <noreply@anthropic.com>
…ommand - Add try/finally to close ReadOnlyContext before process.exit() - Switch from findAllUnbounded() to findAll() (defaults to 100 tasks) - Remove unnecessary `as Task[]` cast that stripped readonly modifier Co-Authored-By: Claude <noreply@anthropic.com>
Add try/finally to ensure ReadOnlyContext.close() is called on all exit paths, preventing SQLite connection leaks. Co-Authored-By: Claude <noreply@anthropic.com>
- Add try/finally to close ReadOnlyContext DB connection in schedule
list/get paths (prevents leaked SQLite handles)
- Validate --status enum value against ScheduleStatus before use
(prevents prototype property access via user input)
- Exit with code 1 on execution history fetch failure (consistent
with all other repo error paths in CLI)
- Use contextual spinner messages ('Fetching schedules/schedule...')
matching status.ts and logs.ts patterns
Co-Authored-By: Claude <noreply@anthropic.com>
… paths Production code for status, logs, schedule list, and schedule get commands now uses withReadOnlyContext() with direct repository access instead of TaskManager/ScheduleService. The existing tests still validated the old mock-based paths (mockTaskManager.getStatus, mockScheduleService.listSchedules) which provided false confidence. - Add MockReadOnlyContext with mock taskRepository, outputRepository, scheduleRepository matching the ReadOnlyContext interface - Rewrite Status Command tests to use ctx.taskRepository.findById/findAll - Rewrite Logs Command tests to use ctx.taskRepository.findById then ctx.outputRepository.get with tail slicing - Rewrite schedule list tests to use ctx.scheduleRepository.findAll/findByStatus - Rewrite schedule get tests to use ctx.scheduleRepository.findById - Update simulate helper functions to mirror production code paths - Leave mutation command tests (cancel, retry, resume, schedule create/cancel/ pause/resume) untouched as they still use full services correctly Co-Authored-By: Claude <noreply@anthropic.com>
- Bump version 0.5.0 → 0.6.0 - Update release notes with all 8 PRs (was missing #85, #86, #91, #94, #100, #106, #107) - Mark v0.6.0 as released in ROADMAP.md - Update FEATURES.md architecture section for hybrid event model - Expand "What's New in v0.6.0" with architectural simplification, bug fixes, tech debt - Fix README roadmap: v0.6.1 → v0.7.0 for loops - Update bug report template example version to 0.6.0
## Summary - Bump version `0.5.0` → `0.6.0` (package.json + package-lock.json) - Expand release notes with all 8 PRs (#78, #85, #86, #91, #94, #100, #106, #107) — was only covering #78 - Mark v0.6.0 as released in ROADMAP.md, update status and version timeline - Update FEATURES.md architecture section for hybrid event model (was describing old fully event-driven architecture with removed services) - Expand "What's New in v0.6.0" in FEATURES.md with architectural simplification, additional bug fixes, tech debt, breaking changes, migration 9 - Fix README roadmap version: `v0.6.1` → `v0.7.0` for task/pipeline loops - Update bug report template example version `0.5.0` → `0.6.0` ### GitHub Issues - Closed #82 (cancelTasks scope — PR #106) - Closed #95 (totalSize tail-slicing — PR #106) - Updated #105 release tracker checklist (all items checked) ## Test plan - [x] `npm run build` — clean compilation - [x] `npm run test:all` — full suite passes (822 tests, 0 failures) - [x] `npx biome check src/ tests/` — no lint errors - [x] `package.json` version is `0.6.0` - [x] Release notes file exists and covers all PRs - [ ] After merge: trigger Release workflow from GitHub Actions - [ ] After release published: close #105 --------- Co-authored-by: Dean Sharon <deanshrn@gmain.com>
Summary
Implement
ReadOnlyContextmodule for query-only commands (status, logs, list, schedule list/get). Skips EventBus, handlers, WorkerPool, and recovery, reducing startup overhead from 200-500ms to ~50ms.Changes
withReadOnlyContext()Performance Impact
Testing
read-only-context.test.tscovering all scenariosbeat status,beat logsshould be noticeably fasterRelated
Co-Authored-By: Claude noreply@anthropic.com