Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,93 @@ 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`; absolute path inside `workingDir` to a regular file, 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.
For recurring jobs with `autoClosePreviousSession` enabled (the default), `Run Now`
also closes the previous run's session before launching, even if it is still working.

### 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`.
`lastDueKey` is an opaque internal duplicate-launch guard, not a stable API format.

`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 subsequent
updates, but do not assume a terminal status will follow: if the session is closed
during the readiness wait or the server restarts before delivery, the run can remain
`session_started` indefinitely with `finishedAt: null`.
`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
`403 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.
See the [complete cron guide](cron-guide.md) for the `cron:runCreated` and
`cron:runUpdated` SSE events.

## Long-polling (agent wait)

Three calls block until something happens instead of answering immediately. They
Expand Down
4 changes: 2 additions & 2 deletions docs/cron-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ spin up a Claude (or shell / OpenCode / Codex / Antigravity / Gemini / Pi) sessi
feed it a prompt. Think "cron for agent sessions": _"every weekday at 3am, open a
Claude session in `~/proj` and tell it to update dependencies and open a PR."_

- **UI**: the **⏰ Cron** button in the header → the Cron Jobs modal (`#cronModal`).
- **UI**: the **⏰ Cron** button in the bottom toolbar → the Cron Jobs modal (`#cronModal`).
- **API**: `/api/cron/jobs*` and `/api/cron/runs`.
- **Code**: `src/cron/cron-service.ts`, `src/cron/cron-time.ts`, `src/cron/cron-input.ts`,
types in `src/types/cron.ts`, routes in `src/web/routes/cron-routes.ts`,
Expand All @@ -24,7 +24,7 @@ Claude session in `~/proj` and tell it to update dependencies and open a PR."_

### In the browser

1. Click **⏰ Cron** in the header.
1. Click **⏰ Cron** in the bottom toolbar.
2. Click **+ New Job**.
3. Fill in a **name**, pick an **agent type** and **working directory**, choose a
**prompt** (inline text or a file path), pick a **schedule**, and leave
Expand Down
22 changes: 15 additions & 7 deletions docs/wiki/Cron-Jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -96,11 +96,19 @@ 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.
If the session is closed during the readiness wait or the server restarts before
delivery, a run can remain `session_started` indefinitely with `finishedAt: null`;
clients must not poll forever waiting for a terminal status.

The schedule is advanced **before** the session launches, so a slow start cannot cause the
same job to re-trigger.
Expand Down
26 changes: 26 additions & 0 deletions docs/wiki/HTTP-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/The-Dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ Two extras depending on the device:
| Respawn | Session Options | [Keeping Agents Running](Keeping-Agents-Running) |
| Ralph | Session Options | [Autonomous Loops](Autonomous-Loops) |
| Orchestrator | Toolbar | [Autonomous Loops](Autonomous-Loops) |
| Cron | Header ⏰ (opt-in) | [Cron Jobs](Cron-Jobs) |
| Cron | Bottom toolbar ⏰ (opt-in) | [Cron Jobs](Cron-Jobs) |
| Subagents | Automatic while agents run | [Watching Agents Work](Watching-Agents-Work) |
| Ultracode | Header (opt-in) | [Watching Agents Work](Watching-Agents-Work) |
| File Viewer | Header | [Working With Files](Working-With-Files) |
Expand Down
56 changes: 56 additions & 0 deletions test/cron-docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/** @fileoverview Guards cron docs against drift from routes, schema, statuses, and UI entry points. */
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.replace(/\s+/g, ' ')).toContain("not whether the agent's task succeeded");
expect(guide).toContain('bottom toolbar');
expect(guide).toContain('App Settings → Header & Panels → Scheduling');
});

it('keeps linked pages consistent and explains non-terminal launch history', () => {
expect(read('docs/wiki/The-Dashboard.md')).toMatch(/\| Cron\s*\| Bottom toolbar/);
expect(read('docs/cron-guide.md')).not.toMatch(/Cron\*\* (?:button )?in the header/);
for (const text of [reference, guide]) {
const normalized = text.replace(/\s+/g, ' ');
expect(normalized).toContain('session is closed during the readiness wait');
expect(normalized).toContain('server restarts before delivery');
expect(normalized).toContain('`session_started` indefinitely with `finishedAt: null`');
}
expect(reference).toContain("also closes the previous run's session before launching");
});
});
Loading