Skip to content

Engine gaps: quiet hours, policy CRUD, type safety, structured logging #14

@jayzalowitz

Description

@jayzalowitz

Context

Four medium-priority gaps remain in the engine layer. Individually small, they collectively affect daily usability and code quality. Bundled here because each is 2-4 hours and they share no dependencies.

Claude Code estimate: ~1-2h

Current State (verified 2026-04-04)

Quiet hours

  • `User` type has `quietHoursStart?: string` and `quietHoursEnd?: string`
  • Settings API accepts and persists these fields
  • Not enforced: `PolicyEvaluator` never checks quiet hours before auto-execution

Policy CRUD

  • `action_policies` table exists in schema
  • `PolicyEvaluator` reads via `getAllPolicies()`
  • No API endpoints for CRUD
  • Users manage behavior through domain autonomy + escalation triggers, but cannot create custom action-level policies

Type safety

  • `apps/api/src/routes/ask.ts` line 102: `policyRepositoryAdapter as never`
  • Lines 105-106: `twinService as never` and `policyEvaluator as never`
  • Root cause: port interface method names don't match DB repository method names

Structured logging

  • All logging via `console.log/warn/error`
  • No JSON output, log levels, request IDs, or correlation

Proposed Change

1. Quiet hours enforcement

Add check in `PolicyEvaluator.evaluate()`: if current time is within quiet hours and outcome is auto-execute, escalate to approval with reason "Quiet hours active." Handle midnight wrap-around (22:00 → 07:00). Urgent escalations (safety, spend limit) still fire.

2. Policy CRUD API

GET/POST/PUT/DELETE at `/api/policies/:userId`. Settings page: "Advanced → Custom Policies" section. Not the primary flow.

3. Type safety cleanup

Create proper adapters in `ask.ts`, remove all `as never` casts. Grep entire codebase to verify zero remain.

4. Structured logging

Add `pino` to `@skytwin/core`. `createLogger(name)` factory. JSON output with timestamp, level, service, request ID. Replace all console calls. Human-readable in dev via `pino-pretty`. Desktop app logs to `~/Library/Logs/SkyTwin/`.

Acceptance Criteria

  1. User sets quiet hours 22:00–07:00 → action at 23:00 with `outcome: 'auto-execute'` → policy evaluator changes outcome to `'request-approval'` with reason containing "Quiet hours"
  2. User sets quiet hours 22:00–07:00 → action at 06:59 → escalated to approval (midnight wrap-around works)
  3. User sets quiet hours 22:00–07:00 → action at 07:01 → auto-executes normally (boundary correct)
  4. User sets quiet hours 22:00–07:00 → spend limit breach at 23:00 → notification still fires (urgent escalations bypass quiet hours)
  5. User with no quiet hours configured → no change in behavior (null-safe)
  6. `POST /api/policies/:userId` with valid policy → returns 201 → `GET /api/policies/:userId` includes new policy
  7. `PUT /api/policies/:userId/:policyId` with updated conditions → returns 200 → policy enforced on next decision
  8. `DELETE /api/policies/:userId/:policyId` → returns 204 → policy no longer returned by GET → no longer enforced
  9. `POST /api/policies/:userId` with missing required field `name` → returns 400 with field-level error
  10. Created policy with `actionType: 'email-send'` and `conditions: { requireApproval: true }` → next email-send action requires approval regardless of trust tier
  11. `grep -r "as never" apps/ packages/` returns 0 results
  12. API request to any endpoint → structured JSON log line emitted with: `timestamp`, `level`, `service: "api"`, `requestId` (UUID), `method`, `path`, `status`, `durationMs`
  13. Worker poll cycle → log lines include `userId` field for the user being polled
  14. Desktop app → logs written to `~/Library/Logs/SkyTwin/skytwin.log` in JSON format
  15. `NODE_ENV=development` → logs are human-readable (pino-pretty), not raw JSON
  16. All 432 existing tests pass
  17. PR passes `/review` before merge

Testing Plan

Layer What Count
Unit `isWithinQuietHours()` — normal range, midnight wrap, boundary at start/end, null config +5
Unit PolicyEvaluator with quiet hours active — auto-execute → approval +2
Unit PolicyEvaluator with quiet hours — urgent escalation still fires +1
Integration Policy CRUD lifecycle: create → read → update → enforce → delete → verify removed +4
Unit Logger factory: JSON format, request ID propagation, level filtering +3
Grep Zero `as never` casts across codebase +1

Effort Estimate

  • Quiet hours: ~20min
  • Policy CRUD: ~30min
  • Type safety: ~15min
  • Structured logging: ~30min

Total: ~1-2h Claude Code time

Files Reference

File Change
`packages/policy-engine/src/policy-evaluator.ts` Add quiet hours check
`packages/core/src/time-utils.ts` New: `isWithinQuietHours()`
`packages/core/src/logger.ts` New: pino logger factory
`apps/api/src/routes/policies.ts` New: CRUD endpoints
`apps/api/src/routes/ask.ts:102-106` Remove `as never` casts, create proper adapters
`apps/api/src/index.ts` Request ID middleware, mount policies route
`apps/api/src/middleware/request-logger.ts` New: structured request logging
`apps/worker/src/index.ts` Replace console with structured logger
`apps/web/public/js/pages/settings.js` "Custom Policies" advanced section

Out of Scope

  • Per-domain quiet hours (single global window sufficient for v0.4)
  • Policy templates / presets
  • Log aggregation / shipping to external service

Related


Working Context Protocol

During implementation, maintain two sources of truth to survive context compaction:

  1. Local context file: Write progress, decisions, and blockers to .context/issue-14-engine-gaps.md (gitignored). Update this file after each meaningful step. On compaction, re-read this file to restore state.
  2. GitHub issue: Post progress comments on #14 at key milestones (subtask complete, blocker hit, design decision made). Reference the issue URL in your conversation so it persists across compaction: Engine gaps: quiet hours, policy CRUD, type safety, structured logging #14

This ensures no quality loss across compaction events — the local file has granular state, the GitHub issue has durable history.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions