Rename the recurring-jobs feature scheduler->cron to disambiguate from the legacy ScheduledRun system (/api/scheduled), which is left untouched: - ScheduledJob->CronJob, SchedulerService->CronService - /api/scheduler/jobs -> /api/cron/jobs; SSE scheduler:* -> cron:* - state keys cronJobs/cronJobRuns - files moved to src/cron/, cron-routes.ts, cron-port.ts, types/cron.ts - frontend cron-ui.js, #cronModal, menu "Cron" - docs moved to docs/cron-discovery.md + docs/cron-build-brief.md, README guides - new tests: cron-service.test.ts, cron-time.test.ts Green: tsc, lint, frontend-syntax, format, 30 cron + 9 legacy scheduled-runs tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
6.9 KiB
CRON_DISCOVERY.md
Phase 1 deliverable for the "Add Scheduling to Codeman" build brief. This documents the existing Codeman architecture and the smallest integration points for a cron. No session/tmux logic will be rebuilt — the new code is purely a trigger + persistence + history layer on top of the existing primitives.
Stack: aicodeman v1.2.1 — Fastify 5 backend, node-pty + tmux sessions,
vanilla-JS SPA frontend served as static assets, JSON file state store, zod
validation, ports-based dependency injection.
0. Critical finding: an existing ScheduledRun is NOT a cron
Codeman already has a ScheduledRun concept (/api/scheduled,
src/web/ports/infra-port.ts:14-26, src/web/server.ts:1480-1605). It is a
run-now, duration-bounded autonomous loop: given {prompt, workingDir, durationMinutes} it immediately spawns/kills throwaway sessions in a loop until
the duration elapses. It has no time-based triggering, recurrence
(once/interval/daily/weekly), enable/disable, next-run calculation, run history,
or persistence across restarts.
Therefore the brief's core (the calendar/cron trigger layer) does not exist
and must be built. The execution primitives it sits on top of do exist and
will be reused. To honor brief §16 ("do not rename existing core concepts"), the
new feature is named CronJob (with CronJobRun history
records), kept distinct from the existing ScheduledRun.
1. Where session creation happens
- Canonical create flow:
POST /api/sessions,src/web/routes/session-routes.ts:262-438.new Session({ workingDir, mode, ... })(src/session.ts:421-570)ctx.addSession(session)→ctx.setupSessionListeners(session)→ctx.persistSessionState(session)(all viaSessionPort).
SessionPortinterface:src/web/ports/session-port.ts:8-16.- Integration point: the cron service will mirror this exact sequence
(create → addSession → setupSessionListeners → start) via
SessionPort, not reimplement it.
2. Where agent/session types are defined
type SessionMode = 'claude' | 'shell' | 'opencode' | 'codex' | 'gemini'(src/types/session.ts:43-44).shellcovers the brief's "Terminal/custom".- CLI availability resolvers in
src/utils/{claude,codex,gemini,opencode}-cli-resolver.ts. - Integration point: the job's
agentTypereusesSessionModeverbatim.
3. Where input is sent into a session
- Raw / paste:
session.write(data)(src/session.ts:2243-2247) — direct PTY write. - Typed (recommended):
session.writeViaMux(data)(src/session.ts:2301-2311) — tmuxsend-keys, falls back to PTY. Submit requires trailing\r. - Integration point: prompt delivery uses
writeViaMux(typed) by default,write(paste) as the alternateinput_mode.
4. Where active sessions are listed
ctx.sessions: ReadonlyMap<string, Session>(SessionPort).- Filters:
Array.from(ctx.sessions.values()).filter(s => s.mode === X)and.isBusy()/.isIdle()(src/session-manager.ts:220-247). - Integration point: the §8 multi-session warning queries this map.
5. Where session kill/delete is handled
ctx.cleanupSession(sessionId, killMux?, reason?)(SessionPort; implsrc/web/server.ts:997-1152). Underlyingsession.stop(killMux)atsrc/session.ts:2498-2585.- The cron does not kill sessions it launches (the brief wants them visible in the normal session UI); cleanup stays user-driven.
6. How session state is stored / 7. Existing persistence
- JSON file store:
~/.codeman/state.json(+state-inner.jsonfor Ralph).StateStoreclasssrc/state-store.ts:71;AppStateinterfacesrc/types/app-state.ts:99-114. - Pattern: declare a field on
AppState, add typed get/set methods onStateStorethat mutate in-memory state and call the debouncedsave()(500ms debounce, atomic temp-file+rename,.bakbackup, circuit breaker). - Integration point: add
cronJobs?: Record<string, CronJob>andcronJobRuns?: Record<string, CronJobRun>toAppState, with matchingStateStoreaccessors. No new DB (brief §6 forbids Postgres/Redis).
8. Where backend routes live
- Route modules:
src/web/routes/*.ts; barrelsrc/web/routes/index.ts; registered inWebServer.setupRoutes()src/web/server.ts:858-876with a singlectxobject fromcreateRouteContext()(src/web/server.ts:553-613) that satisfies all port interfaces. - Validation: zod schemas in
src/web/schemas.ts, applied viaparseBody(Schema, req.body)(src/web/route-helpers.ts:101-111). - Errors:
createErrorResponse(ApiErrorCode.X, msg)/ApiResponse(src/types/api.ts), auto-mapped to HTTP status by apreSerializationhook (src/web/server.ts:644-659). - SSE:
ctx.broadcast(SseEvent.X, data)(EventPort,src/web/sse-events.ts); frontend mirror insrc/web/public/constants.js. - Integration point: new
cron-routes.tsregistered alongside the others; new zod schema; newSseEventconstants for job list/run changes.
9. Where frontend pages/components live
- Vanilla-JS SPA: single
src/web/public/index.html+ feature mixin files (Object.assign(CodemanApp.prototype, {...})). API viaapi-client.js(_apiJson/_apiPost/_apiDelete). Build = esbuild minify + content-hash, no bundler (scripts/build.mjs). - UI is panels/modals toggled by JS classes; forms use
.form-row/.modalconventions (styles.css). SSE handler map inapp.js. - Integration point: add a new
cron-ui.jsmixin + a panel/modal inindex.html+ nav entry, following the orchestrator/respawn panel pattern.
10. Background-loop pattern (for the due-checker)
- Established pattern:
this.cleanup.setInterval(fn, intervalMs, {description})inWebServer.start()(src/web/server.ts:~1942-1966), auto-disposed inWebServer.stop()viathis.cleanup.dispose()(src/web/server.ts:2336). RalphLoop (src/ralph-loop.ts:268-286) shows the self-rescheduling guard idiom. - Integration point: register a 30s cron tick via
cleanup.setInterval; no manual shutdown wiring needed.
Smallest integration points (summary)
| New piece | Reuses | Location |
|---|---|---|
CronJob / CronJobRun types |
— (new) | src/types/cron.ts |
| Persistence | StateStore / AppState |
src/types/app-state.ts, src/state-store.ts |
| Next-run time math | — (new, pure, unit-tested) | src/cron/cron-time.ts |
| Launch + send prompt | SessionPort (addSession/listeners/writeViaMux) |
src/cron/cron-service.ts |
| Background due loop | cleanup.setInterval pattern |
src/cron/cron-loop.ts |
| Routes + schema | route/ports/zod/SSE patterns | src/web/routes/cron-routes.ts, src/web/schemas.ts, src/web/sse-events.ts |
| UI | panel/modal/mixin conventions | src/web/public/cron-ui.js, index.html |
Nothing in the session, tmux, persistence, routing, or SSE subsystems is rewritten — the cron is additive and calls existing services.