- Add batchedMap() to fs-helpers.ts: Promise.all-based worker pool capped
at BATCH_CONCURRENCY (10) concurrent operations. Individual item errors
become null — one bad file never aborts the entire batch.
- Replace unbounded Promise.all in loadCacheFromDisk() with batchedMap()
- Replace unbounded Promise.all in listArchivedTasks() with batchedMap()
- Add batch-reads-benchmark.test.ts: concurrency-cap proof, order
preservation, error isolation, corrupt/missing file tolerance, and a
50-file wall-clock benchmark (3.4× improvement on local tmpfs)
Closes#253
- auth: disable localhost bypass entirely in production mode instead of
just logging a warning — prevents misconfigured deployments from
allowing unauthenticated access
- broadcast-storage: wrap JSON.parse() calls for tags and readBy
frontmatter fields in try-catch, defaulting to empty arrays on parse
failure instead of crashing the route handler
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Service initialization (telemetry, policy, config, migrations) now
calls process.exit(1) on failure instead of silently continuing with
a partially broken server
- WebSocket server close gets a 3s timeout so stuck clients don't block
shutdown indefinitely
- Telemetry flush gets a 5s timeout so a stuck write queue doesn't
prevent shutdown
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- config-service: coalesce concurrent getConfig() calls into a single
disk read via pendingRead promise, preventing cache stampede under load
- activity-service: log warning when corrupted activity file is reset
instead of silently discarding data
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace per-subscribe ws.on('close') listeners with tracked emitter
references, preventing listener accumulation when clients re-subscribe
- Add message rate limiting (30 msgs / 10s window) to prevent DoS via
WebSocket message spam
- Clean up emitter listeners on close handler to prevent callbacks on
destroyed sockets
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- gateway-chat-client: add settled flag to prevent multiple resolve/reject
on the same promise from concurrent timeout, error, and close events
- file-lock: add rejection handler on previous.then() in timeout path so
a rejected predecessor doesn't cause an unhandled rejection
- telemetry-service: capture event reference at enqueue time instead of
shifting from queue at write time, preventing event loss under concurrency
- status-history-service: await async init before any public method runs,
preventing race conditions when logStatusChange is called before
loadLastEntry completes
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add explicit `algorithms: ['HS256']` to all `jwt.verify()` calls to
prevent algorithm confusion attacks (CVE-2015-9235). Without this,
an attacker could switch the algorithm header to exploit key type
mismatches and forge valid tokens.
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The requireDeliverableForDone field was used in task-service.ts and
the UI (TasksTab.tsx) but was missing from the Zod validation schema
in feature-settings-schema.ts. Due to .strict() mode on
TaskBehaviorSettingsSchema, PATCH /api/settings/features rejected
any payload containing this field with a 400 error.
Fix: Add requireDeliverableForDone: z.boolean().optional() to
TaskBehaviorSettingsSchema after autoSaveDelayMs.
Also add tests verifying the field is accepted (true and false) and
that unknown fields are still rejected by strict mode.
Reimplements #130. Original contribution by @TylonHH.
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
* feat: global system health status bar (#185)
* fix: export system-health types from shared barrel
---------
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Add optional 'card' field (Record<string, unknown>) to SquadMessage and
SquadMessageInput types, allowing callers to attach Adaptive Card v1.5
JSON payloads to squad chat messages.
Changes:
- shared: Add card? to SquadMessage and SquadMessageInput interfaces
- routes/chat: Add card to zod validation schema and passthrough
- chat-service: Accept and spread card into squad message object
- squad-webhook: Include card in webhook payload type and forwarding
The card field flows through the full pipeline: API validation → storage
→ API response → WebSocket broadcast → webhook forwarding. Cards are
transient (not serialized to markdown logs) and intended for real-time
delivery to Teams via Adaptive Card attachments.
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
getRunMetrics() returns successRate as 0-1 ratio but getOperationsSignal()
treated it as 0-100 percentage. This caused the banner to show '1% success
rate' when all runs succeeded, and incorrectly flagged operations as critical.
Multiply by 100 and round before threshold comparison and display.
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
- Add RunMode type and QaGateState interface to shared task.types.ts
- Add runMode and qaGate optional fields to Task and UpdateTaskInput interfaces
- Mirror changes in shared/src/types/task.types.d.ts (used by web bundler)
- Add RunModeGateSection.tsx component (was untracked, causing web build failure)
- Add qa-gate.test.ts and dependency-cycle.test.ts (untracked test files)
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
- checkForCycle now accepts a direction parameter ('depends_on' | 'blocks')
so DFS only traverses edges of the same relationship type being validated.
Previously, mixing both types produced false positives: e.g. C depends_on D
and D blocks E is a valid DAG, but the old DFS would traverse C→D→E through
mixed edge types and incorrectly report a cycle when adding E depends_on C.
- Deep-copy task dependency objects before mutation so the in-memory cache is
never corrupted by pre-commit edge additions, which caused the final race-
condition check to mis-detect cycles on valid graphs.
- Fix blocks cycle detection direction: when adding A blocks B, the check
should start from B and follow blocks edges to see if A is reachable,
matching the same semantics as depends_on cycle detection.
- Add dependency-cycle.test.ts with 7 targeted test cases including the
specific false-positive scenario from issue #188.
Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Complete the TODO at hook-service.ts:153 — when a hook config has
`notify: true`, create a notification via NotificationService for
the lifecycle event (created, started, blocked, completed, archived).
Follows the same non-blocking pattern as fireWebhook and fireSquadChat:
errors are logged but never propagate to the caller.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Aggregate system, agent, and operations health signals into a single
status bar displayed below the header. The bar shows one of five states
(stable/reviewing/drifting/elevated/alert) with color-coded indicators
and expands on click to show per-signal details.
Backend: GET /api/v1/system/health aggregates storage/disk/memory checks,
agent registry stats, and 24h run metrics into a unified response.
Frontend: SystemHealthBar component with useSystemHealth hook polling
via @tanstack/react-query (30s connected, 60s disconnected).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Comment add/edit/delete operations update tasks via taskService but
don't notify WebSocket clients, causing stale UI for other connected
users. They only see comment changes after a full page refresh.
Add broadcastTaskChange('updated', taskId) calls to all three comment
endpoints (POST, PATCH, DELETE) matching the pattern used in the main
task routes (tasks.ts lines 564, 711, 773).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Docker users mapping to non-standard ports (e.g., -p 3099:3001) were
getting CORS blocked because buildDefaultDevOrigins() only generated
origins for ports 5173 and 3000.
Two changes:
1. CORS origin callback now allows any localhost/127.0.0.1 origin in
dev mode (NODE_ENV !== 'production'), mirroring the WebSocket origin
validator in auth.ts.
2. buildDefaultDevOrigins() now includes the server's own PORT in the
default origins list.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace all 9 production `as any` casts and ~35 unsafe `as string`
casts across 12 files with proper type narrowing.
Changes:
- Add server/src/lib/query-helpers.ts with qStr, qStrD, qNum, qNumD,
and paramStr utilities for safe Express 5 query/param extraction
- telemetry.ts: use discriminated union narrowing for run.completed
durationMs instead of (eventInput as any).durationMs
- telemetry-service.ts: use intersection type cast instead of as any
for durationMs capping
- docs.ts: replace (req.params as any).path with paramStr(); replace
as string query casts with qStr/qStrD
- dashboard-metrics.ts: remove unnecessary as any on run.started agent
(discriminated union already narrows correctly)
- config-service.ts: narrow as any to as Record<string,unknown>
- transition-hooks.ts: validate toStatus against TaskStatus enum
instead of casting as any
- activity.ts, summary.ts, status-history.ts, digest.ts,
error-learning.ts, task-observations.ts: replace all as string
query param casts with type-safe helpers
Runtime behavior unchanged. All 1347 existing tests still pass.
tsc --noEmit: 0 errors (before and after).
Add batched WebSocket broadcasting to improve performance with
many connected clients.
Changes:
- New broadcastToClients() helper function
- Batches client.send() calls in groups of 50
- Uses setImmediate() between batches to yield event loop
- Preserves synchronous behavior for small client counts (<50)
Applied to all broadcast functions:
- broadcastTaskChange()
- broadcastChatMessage()
- broadcastSquadMessage()
- broadcastTelemetryEvent()
- broadcastNewMessage()
- broadcastWorkflowStatus()
Performance impact:
- Prevents main thread blocking with 100+ clients
- No impact on latency for typical deployments (<50 clients)
- Maintains message ordering within each client
Risk: Low - backward compatible, fallback to sync for small counts
Co-authored-by: BradGroux <super.seth.vos@gmail.com>
* fix(stability): complete Zod 4 API migration
BREAKING CHANGE: Migrated from Zod 3 to Zod 4 API patterns
Changes:
- Replace ZodError.errors with ZodError.issues (Zod 4 API)
- Update z.record(valueSchema) to z.record(z.string(), valueSchema)
- Fix env.ts schema defaults to use correct types (numbers/booleans)
- Replace required_error with message in Zod schemas
This resolves 50+ TypeScript compilation errors that were blocking
CI/CD and potentially causing runtime issues.
Fixes: type checking errors in server and web packages
Risk: Low - straightforward API migration with full test coverage
* fix(zod4): use string defaults for transform/pipe schemas
Zod v4 changed .default() to require the input type (string) rather
than the output type. Fixed PORT, VERITAS_AUTH_ENABLED,
VERITAS_AUTH_LOCALHOST_BYPASS, CSP_REPORT_ONLY, and RATE_LIMIT_MAX
to pass string defaults to their respective portSchema / booleanString
/ positiveIntString coercing schemas.
---------
Co-authored-by: BradGroux <super.seth.vos@gmail.com>
Express 5 uses path-to-regexp v8+ which requires named wildcards.
Bare '*' patterns are no longer valid.
Fixes#150
Co-authored-by: Brad Groux <bradgroux@Brads-Mac-mini.local>
- New middleware: external-api-key.ts
- Requires X-API-Key header for non-localhost requests
- Protects tunnel endpoint (vk-api.ops.digitalmeld.cloud)
- Localhost requests bypass key check for dev convenience
- Key stored in 1Password and VK_API_KEY env var