diff --git a/docs/api-reference.md b/docs/api-reference.md index 47c22e0b..810c321d 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -87,6 +87,85 @@ the HTTP status. Adding a new error code is non-breaking; removing or renaming one is a major change. +## Cron jobs + +Saved jobs and their launch history are separate from the legacy `/api/scheduled` +duration-bounded loops. Use `/api/v1/cron/...` in external clients; `/api/cron/...` +is the unversioned alias. These routes use the response envelope above; the table +lists the value inside `data` on success. + +| Method | Path | Request body | Response `data` | +| --- | --- | --- | --- | +| GET | `/api/v1/cron/jobs` | None | `CronJob[]` | +| POST | `/api/v1/cron/jobs` | Full job definition below | `{ job: CronJob }` | +| GET | `/api/v1/cron/jobs/:id` | None | `CronJob` | +| PUT | `/api/v1/cron/jobs/:id` | Partial job definition | `{ job: CronJob }` | +| DELETE | `/api/v1/cron/jobs/:id` | None | `{}` | +| PUT | `/api/v1/cron/jobs/:id/enabled` | `{ enabled: boolean }` | `{ job: CronJob }` | +| POST | `/api/v1/cron/jobs/:id/run` | None | `{ run: CronJobRun, activeAgents: number }` | +| GET | `/api/v1/cron/jobs/:id/runs` | None | `CronJobRun[]` | +| GET | `/api/v1/cron/runs` | None | `CronJobRun[]` | + +### Job request fields + +The create body requires `name`, `agentType`, `workingDir`, `promptMode`, +`inputMode`, `scheduleType`, `enabled`, and `concurrencyPolicy`. Additional fields +are required according to the selected prompt and schedule: + +| Field | Type / validation | +| --- | --- | +| `name` | String, 1–200 characters | +| `agentType` | A supported session mode (including `shell`) | +| `workingDir` | Existing, allowed working-directory path | +| `launchCommand` | Optional single-line string, at most 2000 characters; for shell jobs | +| `promptMode` | `inline_text` or `prompt_file_path` | +| `promptText` | Required for `inline_text`; nonempty single-line string, at most 100000 characters | +| `promptFilePath` | Required for `prompt_file_path`; regular file confined to `workingDir`, at most 1 MiB, read when the job fires | +| `inputMode` | `paste` or `typed` | +| `scheduleType` | `once`, `interval`, `daily`, or `weekly` | +| `runAt` | Required for `once`; positive integer Unix timestamp in milliseconds | +| `intervalMinutes` | Required for `interval`; integer from 1 to 525600 | +| `dailyTime` | Required for `daily`; `HH:MM` in server-local time | +| `weeklyDays` | Required for `weekly`; 1–7 weekday integers, 0 (Sunday) through 6 (Saturday) | +| `weeklyTime` | Required for `weekly`; `HH:MM` in server-local time | +| `enabled` | Boolean | +| `concurrencyPolicy` | `warn_only` or `skip_if_same_agent_running`; scheduled runs only | +| `autoClosePreviousSession` | Optional boolean, default `true`; ignored for `once` | +| `notes` | Optional string, at most 2000 characters | + +`PUT /jobs/:id` accepts any subset of these fields, then validates the merged job. +When changing `promptMode` or `scheduleType`, supply the fields the new mode needs. +`Run Now` works even when the job is disabled, bypasses the scheduled concurrency +policy, and does not change the schedule. `activeAgents` counts live sessions of +the same agent type, excluding sessions created by this job. + +### Job and run response fields + +`CronJob` contains the request fields plus server-maintained `id`, optional +`owner` (multi-user mode), `createdAt`, `updatedAt`, `lastRunAt`, `nextRunAt`, +`lastStatus`, `lastDueKey`, and optional `completedOnce`. Times are Unix +milliseconds; `lastRunAt`, `nextRunAt`, `lastStatus`, and `lastDueKey` can be `null`. + +`CronJobRun` contains `id`, `cronJobId`, nullable `sessionId` and `sessionName`, +`startedAt`, nullable `finishedAt`, `status`, optional `errorMessage`, +`triggerType` (`scheduled` or `manual_run_now`), and nullable `createdSessionUrl`. +Run times are also Unix milliseconds. Status is one of `created`, +`session_started`, `prompt_sent`, `failed`, or `skipped`. + +Prompt delivery continues asynchronously after session launch, so `Run Now` can +return `session_started` before the prompt is sent. Read run history for the later +status. `finishedAt` refers to the launch/prompt-delivery attempt, **not completion +of the agent's task**; `prompt_sent` does not prove that the task succeeded. + +In multi-user mode, list/history endpoints filter to accessible jobs. An unknown +or inaccessible job returns `NOT_FOUND`. Job creation and updates can return +`FORBIDDEN` for a working directory outside the owner's workspace or a shell / +launch-command job without the required privilege grant. Invalid definitions or +working directories return `INVALID_INPUT`; launch/delivery failures are recorded +on the run, so inspect its `status` and `errorMessage` even after an HTTP success. + +See [Cron Jobs](wiki/Cron-Jobs.md) for the UI, scheduling, and prompt-file rules. + ## Long-polling (agent wait) Three calls block until something happens instead of answering immediately. They diff --git a/docs/wiki/Cron-Jobs.md b/docs/wiki/Cron-Jobs.md index 699c89ee..c71c03c4 100644 --- a/docs/wiki/Cron-Jobs.md +++ b/docs/wiki/Cron-Jobs.md @@ -4,8 +4,8 @@ Saved, named jobs that start a session and send it a prompt on a schedule. Cron sessions: *every weekday at 03:00, open a Claude session in `~/proj` and tell it to update dependencies and open a PR.* -The ⏰ **Cron** header button is opt-in. Turn it on in -**App Settings → Header & Panels**. +The ⏰ **Cron** button in the bottom toolbar is opt-in. Turn it on under +**App Settings → Header & Panels → Scheduling**. ## Creating a job @@ -96,11 +96,16 @@ The skip policy has the details you would want it to have: Every fire is recorded per job, with a status: -| Status | Meaning | -| --------- | -------------------------------------------------------------------- | -| `created` | The run started and a session was created. | -| `skipped` | The concurrency policy blocked it. Not counted as a run. | -| `failed` | The prompt could not be resolved, or the working directory was gone. | +| Status | Meaning | +| ----------------- | ----------------------------------------------------------------- | +| `created` | The run record was created; the session has not started yet. | +| `session_started` | The session started; prompt delivery is still pending. | +| `prompt_sent` | The prompt was sent to the session. | +| `skipped` | The concurrency policy blocked it. Not counted as a run. | +| `failed` | Prompt resolution, session launch, or prompt delivery failed. | + +History records whether the session started and the prompt was delivered, **not whether +the agent's task succeeded**. `prompt_sent` is not a task-completion signal. The schedule is advanced **before** the session launches, so a slow start cannot cause the same job to re-trigger. diff --git a/docs/wiki/HTTP-API.md b/docs/wiki/HTTP-API.md index 7cd94f11..b4fb3cae 100644 --- a/docs/wiki/HTTP-API.md +++ b/docs/wiki/HTTP-API.md @@ -92,6 +92,32 @@ Roughly 235 handlers across 26 route modules. By domain: Each route module documents its own endpoints in its file header. +## Cron jobs + +Saved scheduled jobs are distinct from the legacy `/api/scheduled` loops. Their +versioned endpoints are: + +| Method | Path | Purpose | +| --- | --- | --- | +| GET | `/api/v1/cron/jobs` | List jobs | +| POST | `/api/v1/cron/jobs` | Create a job | +| GET | `/api/v1/cron/jobs/:id` | Read a job | +| PUT | `/api/v1/cron/jobs/:id` | Update a job (partial body) | +| DELETE | `/api/v1/cron/jobs/:id` | Delete a job | +| PUT | `/api/v1/cron/jobs/:id/enabled` | Enable/disable with `{ enabled: boolean }` | +| POST | `/api/v1/cron/jobs/:id/run` | Run now, without changing the schedule | +| GET | `/api/v1/cron/jobs/:id/runs` | Read a job's run history | +| GET | `/api/v1/cron/runs` | Read all accessible run history | + +Responses use the envelope above: job lists/history have arrays in `data`, a +single-job GET has the job itself, create/update/enable have `{ job }`, delete has +`{}`, and Run Now has `{ run, activeAgents }`. Prompt delivery is asynchronous; +`session_started` and `prompt_sent` describe launch/delivery, not task success. + +The [cron API reference](https://github.com/Ark0N/Codeman/blob/master/docs/api-reference.md#cron-jobs) +lists all request and response fields, validation, and ownership restrictions. +[Creating a job](Cron-Jobs) covers the UI and schedule semantics. + ## Long-polling instead of polling Three calls block until something happens, so an agent driving Codeman from a shell can wait diff --git a/test/cron-docs.test.ts b/test/cron-docs.test.ts new file mode 100644 index 00000000..8c154805 --- /dev/null +++ b/test/cron-docs.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { CronJobSchema } from '../src/web/schemas.js'; + +const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); +const reference = read('docs/api-reference.md'); +const wiki = read('docs/wiki/HTTP-API.md'); +const guide = read('docs/wiki/Cron-Jobs.md'); + +const cronSection = (text: string) => text.split('## Cron jobs\n')[1]?.split('\n## ')[0] ?? ''; + +describe('cron documentation', () => { + it('documents every cron route in both API references', () => { + const routes = [...read('src/web/routes/cron-routes.ts').matchAll(/app\.(get|post|put|delete)\('([^']+)'/g)]; + expect(routes).toHaveLength(9); + for (const [, method, path] of routes) { + const row = `| ${method.toUpperCase()} | \`${path.replace('/api/', '/api/v1/')}\` |`; + expect(cronSection(reference)).toContain(row); + expect(cronSection(wiki)).toContain(row); + } + }); + + it('documents every accepted job field and validates the guide example', () => { + for (const field of Object.keys(CronJobSchema.shape)) { + expect(cronSection(reference)).toContain(`| \`${field}\` |`); + } + const example = guide.match(/-d '(\{[\s\S]*?\})'/)?.[1]; + expect(example).toBeDefined(); + expect(CronJobSchema.safeParse(JSON.parse(example!)).success).toBe(true); + }); + + it('documents all run statuses without treating prompt delivery as task success', () => { + const statusType = read('src/types/cron.ts').match(/export type CronJobRunStatus = ([^;]+);/)?.[1]; + expect(statusType).toBeDefined(); + for (const [, status] of statusType!.matchAll(/'([^']+)'/g)) { + expect(guide).toContain(`| \`${status}\``); + expect(cronSection(reference)).toContain(`\`${status}\``); + } + expect(guide).toContain("not whether\nthe agent's task succeeded"); + expect(guide).toContain('bottom toolbar'); + expect(guide).toContain('App Settings → Header & Panels → Scheduling'); + }); +});