Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/code/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The standalone command reads the database in read-only mode, so it is safe to ru

## What it shows

- **Run list**: every run with its status, task key or automation id, origin (tracker task, PR mention, or scheduled), agent harness, PR link, and duration. The task key links straight to the tracker ticket when the tracker's URL can be derived from your configuration; filter by status or origin (`origin=scheduled` isolates automation runs).
- **Run list**: every run with its status, task key or automation id, origin (tracker task, PR mention, scheduled, or estimate), agent harness, PR link, and duration. The task key links straight to the tracker ticket when the tracker's URL can be derived from your configuration; filter by status or origin (`origin=scheduled` isolates automation runs; `origin=estimate` isolates story-point sweeps).
- **Run detail**: the task key header (linked to its tracker ticket when possible) plus a snapshot of the original task description — captured when the run started and rendered as markdown — followed by a stage-by-stage timeline: the feasibility verdict, the implementation summary, each self-review iteration, each human change request and how it was handled, and the final outcome.
- **Stats**: runs per week, success and escalation rates, median run duration, and a per-harness breakdown over a selectable window (7, 30, or 90 days, or all time).
- **Logs**: the most recent worker log lines (timestamp, severity, message), filterable by level (`everything` / `warnings` / `errors`) with a search box over the loaded window. Lines that mention a task key link straight to that task's latest run in the Runs view.
Expand Down Expand Up @@ -104,7 +104,7 @@ The dashboard is backed by a small read-only JSON API you can use directly, for

| Endpoint | Returns |
| --------------------------- | --------------------------------------------------------------------- |
| `GET /api/runs` | Paginated run list (`limit`, `offset`, `status`, `origin`, `taskKey`); `origin=scheduled` is supported |
| `GET /api/runs` | Paginated run list (`limit`, `offset`, `status`, `origin`, `taskKey`); `origin=scheduled` and `origin=estimate` are supported |
| `GET /api/runs/:id` | One run with its stage timeline and retry metadata |
| `POST /api/runs/:id/retry` | Schedule a re-run of the task behind a failed/escalated/abandoned run (requires sign-in) |
| `GET /api/stats?window=30d` | Aggregate stats (`7d`, `30d`, `90d`, or `all`) |
Expand Down
147 changes: 35 additions & 112 deletions docs/code/story-points-estimation.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
---
title: "Story Points Estimation"
description: "Automatically estimate JIRA story points with AI using the --estimate flag"
description: "Schedule unattended AI story-point estimation via [[estimations]] in workspace.toml, or run one-shot --estimate"
section: "Server Automation"
order: 6
dateModified: 2026-08-26
dateModified: 2026-08-27
---

# Story Points Estimation

Use the `--estimate` flag to have your AI agent analyze JIRA tasks and automatically assign story point estimates. This is useful for backlog grooming, sprint planning, or keeping estimates up to date as tasks evolve.
Let the [workspace worker](./worker.md) estimate stories on a schedule with `[[estimations]]` in `workspace.toml`, or use the `--estimate` flag as a CLI one-shot. In both modes your AI agent analyzes tasks and automatically assigns story point estimates — useful for backlog grooming, sprint planning, or keeping estimates up to date as tasks evolve.

```toml
[[estimations]]
id = "weekday-groom"
enabled = true
cron = "0 9 * * 1-5"
query = "status = 'To Do' AND labels IN (NeedsEstimate)"
```

## What It Does

Expand All @@ -18,7 +26,9 @@ Use the `--estimate` flag to have your AI agent analyze JIRA tasks and automatic
- Sets the story points field directly in JIRA
- Posts a rich estimation comment with full context

## Usage
## Usage (CLI one-shot)

`--estimate` remains the interactive/one-shot path; scheduled sweeps run the same engine from the worker.

### Single Task

Expand Down Expand Up @@ -90,123 +100,36 @@ If your JIRA instance uses a custom field name, you can override it in `.devinte
}
```

## Automated Estimation

`--estimate` is still a CLI one-shot. The [worker](./worker.md) does not schedule estimation yet. Until it does, the unattended path is a systemd timer or crontab of `devintern --estimate --query`. Prefer that over inventing a second daemon; drain ready tickets with the worker, and keep this timer only for story points.

### systemd timers

A systemd job is a one-shot `.service` plus a `.timer` that triggers it. Below is the daily morning run; the weekly grooming and sprint-planning helpers follow the same shape with different `OnCalendar` expressions and JQL.

#### Daily estimation (weekday mornings)

Run every weekday at 9 AM to estimate new tasks.

`/etc/systemd/system/devintern-estimate-daily.service`:

```ini
[Unit]
Description=Daily estimation of NeedsEstimate tasks
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=devintern
WorkingDirectory=/path/to/your/project
ExecStart=/usr/local/bin/devintern --estimate \
--query 'project = PROJ AND status = "To Do" AND labels IN (NeedsEstimate)'
StandardOutput=journal
StandardError=journal
```

`/etc/systemd/system/devintern-estimate-daily.timer`:

```ini
[Unit]
Description=Run daily estimation at 9 AM on weekdays

[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00
Persistent=true

[Install]
WantedBy=timers.target
```

Enable and inspect:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now devintern-estimate-daily.timer
systemctl list-timers devintern-estimate-daily.timer
journalctl -u devintern-estimate-daily.service -f
```

#### Weekly backlog grooming

`OnCalendar` for Monday at 8 AM, JQL covers the last 7 days:

```ini
# devintern-estimate-weekly.timer
[Timer]
OnCalendar=Mon *-*-* 08:00:00
Persistent=true

[Install]
WantedBy=timers.target
```

```ini
# devintern-estimate-weekly.service, ExecStart line
ExecStart=/usr/local/bin/devintern --estimate \
--query 'project = PROJ AND status = "Backlog" AND updated >= -7d'
```

#### Sprint planning helper
---

Wednesday at 10 AM, fills in any remaining gaps before planning:
## Scheduled Estimation with the Worker

```ini
# devintern-estimate-sprint.timer
[Timer]
OnCalendar=Wed *-*-* 10:00:00
Persistent=true
The [workspace worker](./worker.md) runs unattended estimation for you via `[[estimations]]` in `workspace.toml`. No more cron of `--estimate`: the worker owns the timer, the durable schedule state, and serialization with all other agent work.

[Install]
WantedBy=timers.target
```
```toml
[[estimations]]
id = "weekday-groom"
enabled = true
cron = "0 9 * * 1-5"
query = "status = 'To Do' AND labels IN (NeedsEstimate)"

```ini
# devintern-estimate-sprint.service, ExecStart line
ExecStart=/usr/local/bin/devintern --estimate \
--query 'project = PROJ AND sprint in openSprints() AND "Story Points" is EMPTY'
[[estimations]]
id = "sprint-gaps"
enabled = true
cron = "0 10 * * 3"
query = "sprint in openSprints() AND \"Story Points\" is EMPTY"
```

### Cron
Each entry needs a unique `id`, boolean `enabled`, a non-empty `query`, and exactly one of `cron` or `interval`. There is no `prompt` and no `repo`: a due entry runs one-shot `devintern --estimate --query "<query>"`, which estimates — never implements, branches, or opens PRs. Estimating does not depend on `[defaults].task_query`; an omitted or empty `[[estimations]]` table simply means estimation is off.

If you're not on systemd, the same three schedules work as crontab entries:

```bash
# In crontab (run `crontab -e`)

# Daily at 9 AM, weekdays: estimate new tasks
0 9 * * 1-5 cd /path/to/your/project && devintern --estimate --query 'project = PROJ AND status = "To Do" AND labels IN (NeedsEstimate)' >> /tmp/devintern-estimate.log 2>&1

# Weekly on Monday at 8 AM: re-estimate recently updated backlog
0 8 * * 1 cd /path/to/your/project && devintern --estimate --query 'project = PROJ AND status = "Backlog" AND updated >= -7d' >> /tmp/devintern-estimate.log 2>&1

# Weekly on Wednesday at 10 AM: sprint planning helper
0 10 * * 3 cd /path/to/your/project && devintern --estimate --query 'project = PROJ AND sprint in openSprints() AND "Story Points" is EMPTY' >> /tmp/devintern-estimate.log 2>&1
```
Notes:

### Important Notes
- Works with Jira, Linear, Azure DevOps, Asana, and GitHub (comment-only). Trello/markdown workspaces fail at startup.
- Runs use the full skip/re-estimate behavior above (24h gate, update-in-place).
- Usage-limit aborts end the sweep cleanly; the next occurrence retries. Tickets already written stay done.
- Each sweep is visible in the [dashboard](./dashboard.md) under its own `estimate` origin.

- Set `WorkingDirectory` (systemd) or `cd` (cron) to your project directory so the correct `.devintern-code/.env` is loaded
- Use absolute paths to `devintern` and your agent binary, or pin `PATH` explicitly in the unit file
- For systemd, `journalctl -u <unit>` gives you logs; for cron, redirect stdout/stderr to a log file
- Test your JQL query manually before scheduling
- Tasks created less than 24 hours ago are automatically skipped, so frequent runs are safe
See [Worker Daemon → Scheduled story-point estimation](./worker.md#scheduled-story-point-estimation) for semantics and troubleshooting.

## Example Output

Expand Down
33 changes: 33 additions & 0 deletions docs/code/worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,39 @@ On shutdown the scheduler stops its timer, terminates active automation subproce
| A run failed and you need to know why | Open the dashboard's Logs tab to read recent worker output without a shell on the machine ([details](./dashboard.md)). |
| The dashboard Logs tab is empty | The daemon tees its output to `worker.stdout.log` / `worker.stderr.log` in the workspace home — check those files (or `journalctl --user -u devintern-worker`) and see [where the logs come from](./dashboard.md#where-the-logs-come-from). |

## Scheduled story-point estimation

`[[estimations]]` in `workspace.toml` runs unattended `--estimate` sweeps on a schedule. Entries use the same schedule grammar as `[[automations]]`, but the body is a **query**, not a prompt — there is no implementation, no branch, no worktree, no PR, and no `repo` key even in multi-repo workspaces:

```toml
[[estimations]]
id = "weekday-groom"
enabled = true
cron = "0 9 * * 1-5"
query = "status = 'To Do' AND labels IN (NeedsEstimate)"

[[estimations]]
id = "sprint-gaps"
enabled = true
cron = "0 10 * * 3"
query = "sprint in openSprints() AND \"Story Points\" is EMPTY"
```

Each entry needs a unique `id`, boolean `enabled`, non-empty `query`, and exactly one of `cron` or `interval`. Omitting the table (or leaving every entry disabled) changes nothing: estimation is simply off, and `[defaults].task_query` is never estimated as a side effect. The workspace tracker must support estimation (Jira, Linear, Azure DevOps, Asana, GitHub comment-only); Trello/markdown workspaces fail at startup with a clear error. `worker init` does not ask about estimations — add tables by hand.

When an entry comes due the worker runs one-shot `devintern --estimate --query "<query>"` from the workspace home. That path keeps all of the interactive behavior: tickets younger than 24 hours are skipped, already-estimated tickets are skipped unless the ticket changed since the estimate, changed tickets are re-estimated with the estimate comment updated in place, points are written to the tracker field, and usage-limit aborts exit cleanly so the next occurrence retries. Each sweep is recorded with its own `estimate` origin (plus the schedule id) in the [dashboard](./dashboard.md) — it never shows up as a scheduled implement run.

### Serialization

Agent usage limits are account-global. While scheduled estimation is configured, every agent run in the worker process — implement tasks, automation occurrences, PR reviews, conflict resolutions, and estimation sweeps — takes turns through one process-level gate, so at most one agent subprocess burns quota at a time.

Estimation shares the [automation scheduler](#schedule-semantics): durable cursors, leases, missed-occurrence coalescing, and overlap skipping live under `estimation:<id>` keys in `queue.db`.

```bash
# The CLI one-shot stays available for ad-hoc estimates:
devintern --estimate --query "project = PROJ AND status = 'To Do'"
```

## Polling mode

With `[defaults].task_query` in `workspace.toml`, the worker polls your tracker on an interval (`[defaults].poll_interval`, default 60 seconds) and runs every task that matches the query. The query uses the same language as batch `--query` runs for your tracker, so "ready" means whatever your query says, for example a status or label.
Expand Down
3 changes: 2 additions & 1 deletion docs/code/workspaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ prompt = "Review the frontend and clean up one source of recurring noise."
- Repo names must be unique and filesystem-safe; they become directory names under `repos/` and `worktrees/`.
- Rule criteria combine with AND; list values (`components`, `labels`) match when the task carries any of them. Comparisons are case-insensitive. `project` matches the task key prefix for `PROJ-123` style keys (Jira, Linear); trackers with numeric or opaque ids route via labels or components.
- `[[automations]]` uses the same schema as single-repo `.devintern-code/automations.toml`. An entry must name `repo` when the workspace has more than one repository. See [Worker Daemon → Recurring automations](./worker.md#recurring-automations) for prompt-writing guidance and schedule semantics.
- `[[estimations]]` schedules unattended story-point sweeps (tracker query + cron/interval, no `prompt`, no `repo`). The workspace tracker must support estimation. See [Worker Daemon → Scheduled story-point estimation](./worker.md#scheduled-story-point-estimation).

### Automatic conflict resolution: `auto` vs `scheduled` vs `disabled`

Expand Down Expand Up @@ -149,7 +150,7 @@ devintern worker # auto-detects ~/.devintern/workspace.toml
devintern worker --workspace /path/to/workspace.toml
```

The fleet query comes from `[defaults].task_query`. A workspace with automations can omit the query and run as an automation-only worker. Poll interval, per-task flags, and the embedded dashboard are also set in `workspace.toml` (`poll_interval`, `worker_task_args`, `[workspace].dashboard` / `dashboard_port`). Direct webhooks are an advanced repo-local service: run `devintern webhook serve` from that repository as a separate process. Workspace and automation configuration is loaded at startup; restart the worker after editing it. Schedule state and leases for automations live in the central workspace database.
The fleet query comes from `[defaults].task_query`. A workspace with automations or estimations can omit the query and run as a schedules-only worker. Poll interval, per-task flags, and the embedded dashboard are also set in `workspace.toml` (`poll_interval`, `worker_task_args`, `[workspace].dashboard` / `dashboard_port`). Direct webhooks are an advanced repo-local service: run `devintern webhook serve` from that repository as a separate process. Workspace, automation, and estimation configuration is loaded at startup; restart the worker after editing it. Schedule state and leases for automations and estimations live in the central workspace database.

`devintern worker init` can generate a user-level systemd unit on Linux or launchd agent on macOS. One service runs the whole workspace. For a hand-written Linux unit:

Expand Down
19 changes: 18 additions & 1 deletion packages/code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ function buildCliRunProps(tracker: string): Record<string, AnalyticsPropValue |

function isWorkerTaskProcess(): boolean {
const origin = process.env[RUN_ORIGIN_ENV];
return origin === "worker" || origin === "scheduled";
return origin === "worker" || origin === "scheduled" || origin === "estimate";
}

/** Finish the local run record and emit exactly one outcome event for worker tasks. */
Expand Down Expand Up @@ -2167,6 +2167,17 @@ async function main(): Promise<void> {

estimationResults.total++;

// Structured run record for this attempt (skips above are not
// attempts). Estimation is its own dashboard origin — never an
// implement run — and scheduled sweeps carry the schedule id.
const estimationScheduleId = process.env.DEVINTERN_AUTOMATION_ID;
beginRun({
origin: "estimate",
taskKey,
tracker: activeTrackerType,
...(estimationScheduleId ? { automationId: estimationScheduleId } : {}),
});

// Fetch comments and linked resources
const comments = await tracker.getComments(taskKey);
const linkedResources = tracker.extractLinkedResources(task);
Expand Down Expand Up @@ -2223,10 +2234,15 @@ async function main(): Promise<void> {
error: "Failed to parse estimation response",
});
}
await finishTaskRun(
result ? "succeeded" : "failed",
result ? undefined : "Failed to parse estimation response",
);
} catch (error) {
// Usage limit is account-global — abort the rest of the estimation
// batch and exit 0 so the scheduler retries next window.
if (error instanceof UsageLimitError) {
await finishTaskRun("deferred", error.message);
console.warn(`\n⏳ ${error.message}. Aborting estimation batch; will retry next run.`);
if (lockManager) {
lockManager.release();
Expand All @@ -2240,6 +2256,7 @@ async function main(): Promise<void> {
error: (error as Error).message,
});
console.error(`❌ Failed to estimate ${taskKey}: ${(error as Error).message}`);
await finishTaskRun("failed", (error as Error).message);
}
}

Expand Down
10 changes: 8 additions & 2 deletions packages/code/src/lib/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type AnalyticsEvent = "cli_run" | "worker_started" | "worker_task_run" |

export type WorkerTaskOutcome = "succeeded" | "failed" | "deferred" | "escalated" | "abandoned";

export type WorkerTaskTrigger = "task" | "scheduled";
export type WorkerTaskTrigger = "task" | "scheduled" | "estimate";
export type WorkerMode = "polling" | "relay" | "hybrid" | "scheduled";

/** Internal marker inherited only by task subprocesses launched by the worker. */
Expand Down Expand Up @@ -243,7 +243,13 @@ export function trackWorkerTaskRun(
): boolean {
const runOrigin = env[RUN_ORIGIN_ENV];
const workerTrigger: WorkerTaskTrigger | undefined =
runOrigin === "worker" ? "task" : runOrigin === "scheduled" ? "scheduled" : undefined;
runOrigin === "worker"
? "task"
: runOrigin === "scheduled"
? "scheduled"
: runOrigin === "estimate"
? "estimate"
: undefined;
if (!workerTrigger) return false;

void track("worker_task_run", {
Expand Down
Loading
Loading