chore: tech debt sweep — #96, #97, #98, #99, #102#109
Conversation
Extract worker state creation, map registration, and DB registration into a private registerWorker method, reducing spawn() from ~99 to ~65 lines. Includes UNIQUE violation rollback logic (Edge Case J).
Add two guard helpers to services.ts that eliminate repeated if-not-ok/if-null guard blocks across CLI commands. Refactors status.ts (2 sites), logs.ts (3 sites), and schedule.ts (8 sites).
Simplifier refinements: withReadOnlyContext and withServices now use exitOnError internally, exitOnNull gains configurable stopMsg, status.ts findAll branch uses exitOnError, schedule.ts functions get explicit return types.
Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CLI Command] --> B{exitOnError}
B -->|result.ok = false| C[s?.stop stopMsg]
C --> D[ui.error prefix + message]
D --> E[process.exit 1]
B -->|result.ok = true| F[return result.value]
F --> G{exitOnNull}
G -->|value == null| H[s?.stop Not found]
H --> I[ui.error msg]
I --> J[process.exit 1]
G -->|value != null| K[return narrowed T]
K --> L[Continue command logic]
Last reviewed commit: "fix: biome import or..." |
src/cli/services.ts
Outdated
| } | ||
|
|
||
| /** Guard: exit on Result error, returning the unwrapped value on success. */ | ||
| export function exitOnError<T>(result: Result<T>, s?: Spinner, prefix?: string): T { |
There was a problem hiding this comment.
⚠️ Missing Unit Tests (HIGH - Blocking)
The new exitOnError and exitOnNull helpers (lines 15-37) are now the single point of error handling for the entire CLI layer (used in 4 command files, ~15 call sites), but they have no unit tests.
Problem:
- If either function regresses (wrong exit code, missing spinner stop, broken type narrowing), all CLI commands fail silently
- These call
process.exit(1)— getting them wrong means critical behavior breaks
Suggested Fix:
Add unit tests in tests/unit/cli/services.test.ts covering:
- (1) success path returns unwrapped value
- (2) error path calls
process.exit(1) - (3) error path stops spinner with correct message
- (4)
exitOnNullhandles bothnullandundefined
Feedback via Claude Code • /code-review
src/cli/services.ts
Outdated
| /** Guard: exit on Result error, returning the unwrapped value on success. */ | ||
| export function exitOnError<T>(result: Result<T>, s?: Spinner, prefix?: string): T { | ||
| if (!result.ok) { | ||
| s?.stop('Failed'); |
There was a problem hiding this comment.
⚠️ UX Regression: Spinner Stop Message (MEDIUM - Should Fix)
The spinner stop message changed from descriptive "Initialization failed" to generic "Failed" in exitOnError (line 17).
Problem:
- Original:
withReadOnlyContext()andwithServices()both calleds?.stop('Initialization failed') - New:
exitOnErrorhardcodess?.stop('Failed') - This loses context about what failed on initialization errors
Suggested Fix:
Add a stopMsg parameter to exitOnError (like exitOnNull already has):
export function exitOnError<T>(
result: Result<T>,
s?: Spinner,
prefix?: string,
stopMsg = 'Failed',
): T {
if (!result.ok) {
s?.stop(stopMsg);
// ...
}
return result.value;
}Then restore context:
export function withServices(...) {
const container = exitOnError(
await bootstrap({ mode: 'cli' }),
s,
'Bootstrap failed',
'Initialization failed' // <-- restore this
);
}Feedback via Claude Code • /code-review
| id: workerId, | ||
| taskId: task.id, | ||
| pid, | ||
| startedAt: Date.now(), |
There was a problem hiding this comment.
ℹ️ Duplicate Date.now() Calls (MEDIUM - Pre-existing)
The registerWorker method calls Date.now() twice independently at lines 222 and 238, creating two separate timestamps that should be identical.
Problem:
- Line 222:
const startedAt: Date.now()forWorkerState.startedAt - Line 238:
startedAt: Date.now()for database registration - These can differ by microseconds despite being conceptually the same value
- Semantic inconsistency between in-memory and DB timestamps could cause confusing diagnostics
Suggested Fix:
Capture once and reuse:
private registerWorker(...): Result<WorkerState> {
const now = Date.now(); // Capture once
const worker: WorkerState = { ..., startedAt: now, ... };
// ...
this.workerRepository.register({ ..., startedAt: now, ... }); // Reuse
}Note: This is a pre-existing issue moved as-is during the extract refactor. Not introduced by this PR.
Feedback via Claude Code • /code-review
| @@ -22,13 +48,7 @@ export function errorMessage(error: unknown): string { | |||
| * **MCP server**: Uses full `bootstrap()` directly. | |||
| */ | |||
| export function withReadOnlyContext(s?: Spinner): ReadOnlyContext { | |||
There was a problem hiding this comment.
Code Review Summary — PR #109 (chore/tech-debt-sweep)
Analysis Complete
8 reviewers across 8 reports (Architecture, Performance, Tests, Regression, TypeScript, Security, Complexity, Consistency)
High-confidence (≥80%) issues: 3 inline comments ☝️
- Missing unit tests for exitOnError/exitOnNull (95%, HIGH)
- Spinner message regression (90%, MEDIUM)
- Duplicate Date.now() in registerWorker (82%, MEDIUM pre-existing)
Lower-Confidence Suggestions (60-79%)
These don't warrant inline comments but are worth considering:
Architecture (65% confidence):
exitOnErrorreturn type relies onprocess.exitbeing typednever. Addingthrowafterprocess.exit(1)would clarify control flow.
TypeScript (65% confidence):
exitOnNullreturn value discarded inlogs.ts:17but used instatus.ts:17. Capturing the return would be more consistent.- Asymmetry:
exitOnNullhasstopMsgparameter butexitOnErrorhardcodes'Failed'.
Complexity (65% confidence):
- Duplicate detail-formatting pattern in
schedule.ts:199-207andschedule.ts:242-247. A sharedformatScheduleDetailshelper could consolidate.
Pre-existing Issues (Not Blocking)
Architecture — Medium (82%):
Incomplete exitOnError/exitOnNull adoption: 6 CLI files still use old pattern (cancel.ts, resume.ts, pipeline.ts, run.ts, agents.ts, init.ts). Not introduced by this PR; consider follow-up sweep.
Tests — Medium (85%):
CLI tests use simulation functions instead of testing actual command code. Tests validate "what code should do" rather than "what code actually does." Pre-existing architectural pattern; no action needed for this PR.
Overall Assessment
✅ Clean architectural refactoring with strong DRY improvements
registerWorkerextraction: method extraction (40 → 6 lines)exitOnError/exitOnNull: DRY helper (eliminates ~60 lines boilerplate)- Self-dogfooding: Uses own helpers in
withReadOnlyContext/withServices
✅ No regressions introduced (aside from minor spinner message simplification)
✅ Type safety maintained (strict TypeScript, no any types, proper narrowing)
✅ Security unchanged (pure refactor, no new inputs, no changed control flow)
Next Steps
- Add unit tests for
exitOnError/exitOnNull(blocker) - Consider
stopMsgparameter onexitOnErrorto restore context - Document or plan follow-up for remaining 6 CLI files (nice-to-have)
Review completed by Claude Code at 2026-03-20 17:59 UTC
Automated via /code-review operation
exitOnError hardcoded s?.stop('Failed'), losing descriptive spinner
messages in withReadOnlyContext/withServices. Add stopMsg parameter
(default 'Failed') matching exitOnNull pattern, and pass
'Initialization failed' from bootstrap call sites.
Add cli-services.test.ts with 19 tests covering exitOnError,
exitOnNull, and errorMessage (success paths, exit paths, spinner
stop messages, prefix concatenation, null/undefined handling).
Co-Authored-By: Claude <noreply@anthropic.com>
Simplifier refinement: extract vi.mocked(ui.error) to module-level mockError constant, matching project patterns (checkpoint-handler, agent-adapters). Remove redundant comment.
Fix biome import sort order in cli-services.test.ts (CI failure). Chain exitOnNull(exitOnError(...)) in logs.ts to avoid discarded return value. Fix multi-line formatting in services.ts per biome.
| memoryUsage: 0, | ||
| process: childProcess, | ||
| task, | ||
| }; | ||
|
|
||
| this.workers.set(workerId, worker); | ||
| this.taskToWorker.set(task.id, workerId); | ||
|
|
||
| const regResult = this.workerRepository.register({ | ||
| workerId, | ||
| taskId: task.id, | ||
| pid, | ||
| ownerPid: process.pid, | ||
| agent: agentProvider, | ||
| startedAt: Date.now(), | ||
| }); | ||
| if (!regResult.ok) { |
There was a problem hiding this comment.
Dual
Date.now() calls may produce mismatched startedAt timestamps
worker.startedAt and the DB-registered startedAt are sourced from two separate Date.now() calls. While the drift is usually a few microseconds (both calls are synchronous), the in-memory WorkerState and the persisted record could carry different timestamps — which matters if any cross-process coordination logic uses startedAt for ordering or deduplication.
The extraction into registerWorker creates a clean opportunity to fix this by reusing the worker's own timestamp:
| memoryUsage: 0, | |
| process: childProcess, | |
| task, | |
| }; | |
| this.workers.set(workerId, worker); | |
| this.taskToWorker.set(task.id, workerId); | |
| const regResult = this.workerRepository.register({ | |
| workerId, | |
| taskId: task.id, | |
| pid, | |
| ownerPid: process.pid, | |
| agent: agentProvider, | |
| startedAt: Date.now(), | |
| }); | |
| if (!regResult.ok) { | |
| const workerId = WorkerId(`worker-${pid}`); | |
| const worker: WorkerState = { | |
| id: workerId, | |
| taskId: task.id, | |
| pid, | |
| startedAt: Date.now(), | |
| cpuUsage: 0, | |
| memoryUsage: 0, | |
| process: childProcess, | |
| task, | |
| }; | |
| this.workers.set(workerId, worker); | |
| this.taskToWorker.set(task.id, workerId); | |
| const regResult = this.workerRepository.register({ | |
| workerId, | |
| taskId: task.id, | |
| pid, | |
| ownerPid: process.pid, | |
| agent: agentProvider, | |
| startedAt: worker.startedAt, | |
| }); |
Summary
Post-v0.6.0 tech debt cleanup. Closes 5 issues; 3 were already resolved, 2 required code changes.
registerWorker()fromspawn()— reduces method from ~99 to ~65 linesexitOnError/exitOnNullCLI helpers — eliminates 13 repeated guard blocks acrossstatus.ts,logs.ts,schedule.ts(net -41 lines)Also updated #31 tech debt backlog: marked 3 stale/fixed entries, updated open count from 14 → 4.
Closes #96, closes #97, closes #98, closes #99, closes #102
Test plan
npm run build— clean typechecknpm run test:implementations— 314 tests pass (covers tech-debt: EventDrivenWorkerPool.spawn() at 99 lines — extract post-spawn setup #98)npm run test:cli— 157 tests pass (covers Extract exitOnError CLI helper to reduce boilerplate #102)npx biome check— clean