1b58a64fd8
* feat: add automation system with triggers, backend-driven metadata, and PR association Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(automations): execution_mode + flat /settings/automations Layered on top of #406's automation subsystem: - Add execution_mode to Automation (task | run, default task). Run-mode firings create an ephemeral kanban-hidden task tagged with origin=automation_run. The session pipeline still runs against it, but users surface them through the AutomationRun history instead of the kanban. Pragmatic shim while AutomationRun-as-true-session-owner is deferred to a follow-up. - Thread IsEphemeral + Origin through ReviewTaskRequest + reviewTaskCreatorAdapter so the orchestrator can create the hidden task in one call. - Run-mode always auto-starts (the user never sees the task to drag). - Flat /settings/automations route replaces the per-workspace sidebar entry. Picks the only workspace, shows a picker for many, or shows a CTA for zero workspaces. - Editor exposes execution mode + table shows it as a badge column. Spec at docs/specs/office/automations-settings.md documents the design, deferred items, and scenarios. * feat(automations): per-automation repository picker Adds a repository_id field on Automation so users can pin scheduled and webhook automations to a specific workspace repository instead of always falling through to "first repo". github_pr triggers ignore the field and keep using the PR's own repo — the editor disables the picker with a hint when a PR trigger is selected. Backend: - repository_id column folded into the canonical CREATE TABLE (no migration needed — PR #406 introduces the table). - RepositoryID threaded through model, DTOs, store INSERT/UPDATE, and service.CreateAutomation. - resolveAutomationRepository prefers the explicit ID, falls back to workspace-first on empty. Frontend: - repository_id on the Automation TS type + Create/Update payloads. - Repository SelectField in ConfigSection backed by useRepositories; "Auto" sentinel represents the empty/fallback choice. - Disabled with help-text for github_pr triggers. * feat(automations): repo picker shows discovered repos too Matches the task-creation dialog's UX: the editor's repository picker now lists registered workspace repos AND filesystem-discovered repos under the workspace's roots. Picking a discovered repo registers it with the workspace at save time (via createRepositoryAction), captures the new id, and writes it onto the automation. After the first save the picker selection is promoted from "discovered" to "registered" so re-saving doesn't re-register. Form state holds a RepositorySelection union (auto | registered | discovered) rather than a plain id string so the picker can carry the metadata needed to register. github_pr triggers still disable the picker — PR events always override any configured repo with the PR's own. * fix(automations): add idempotent ALTERs for in-branch columns The canonical CREATE TABLE covers fresh installs, but `CREATE TABLE IF NOT EXISTS` is a no-op for DBs already initialised from an earlier commit on this branch (PR #406's original schema). Without these ALTERs, dev DBs created before execution_mode + repository_id existed on the row hit "no such column" errors. Three idempotent ALTERs: task_title_template, execution_mode, repository_id. SQLite returns a duplicate-column error when the column already exists; we swallow it. * fix(automations): use generateUUID in non-secure contexts crypto.randomUUID is only defined in secure contexts (HTTPS or localhost). PR #406's editor used it directly, which throws "crypto. randomUUID is not a function" when the app is reached via a plain hostname like nova:58204. Switch to lib/utils.generateUUID, which falls back to a Math.random-based implementation when crypto isn't available. * fix(automations): polish run-mode lifecycle, validation, and editor UX Run-mode automations were partly broken end-to-end: - workflow_id was sent for ephemeral tasks (rejected by task service) and was required by the editor + automation service, so "Create Automation" was always disabled and silently failed - a permission_request hanging the agent left the run pinned at task_created forever; nothing reaped the ephemeral worktree - trigger config inputs (cron, branches, authors) did not re-sync with the loaded automation on page reload - runs row never linked back to the underlying task for task-mode automations, and the table header highlighted on hover Backend - skip workflow fields in CreateReviewTask / StartTask for run-mode - relax automation.Service.CreateAutomation: workflow only required for task-mode - auto-reject permission prompts on ephemeral automation tasks and mark the AutomationRun failed with the requested action - on agent completion / failure for ephemeral automation tasks, flip the run to succeeded/failed and remove the worktree + branch immediately (new RunStatusSucceeded + MarkRunSucceededByTaskID) - wire worktree.Manager into the orchestrator service Frontend - show Execution Mode first; hide Workflow / Workflow Step in run-mode - canSave no longer requires workflow in run-mode - surface save errors via toast instead of silent failure - sync trigger-config local input state with loaded prop value - runs table: clickable row → /tasks/<id> for task-mode runs, non-clickable in run-mode; suppress header hover * fix(automations): address PR review feedback Backend - automation/scheduler.go: parseCronFields returns an error for unsupported expressions instead of silently defaulting to hourly - automation/webhook.go: constant-time secret compare, header-only (no more ?secret= query param), reject with 409 when no enabled webhook trigger exists - automation/service.go: enforce max_concurrent_runs in FireTrigger via maybeSkipForConcurrencyCap (records a skipped run when capped) - cmd/kandev/e2e_reset.go: surface automation delete errors as 500 instead of swallowing them - orchestrator/event_handlers_git.go: auto-reject permission with cancelled=true so the message status is "rejected" and the session isn't flipped back to running - orchestrator/event_handlers_agent.go: finalizeAutomationRunIfEphemeral now runs at the top of handleAgentFailed, before the resume/recoverable early returns, so session-backed run-mode failures get cleaned up Frontend - state/slices/automations/automations-slice.ts: declare _get/_api so the call site's arity matches and CodeQL stops flagging ignored trailing arguments - automations/schedule-selector.tsx: clearing the input now propagates ""; cron is trimmed before validation+save - automations/triggers-section.tsx: switching to webhook mode deletes the existing scheduled/condition triggers so they don't keep firing hidden in the background - automations/trigger-configs/github-ci-config.tsx: useEffect re-sync for checkNames, matching the other trigger configs - hooks/domains/settings/use-automations.ts: per-workspace fetch via loadedWorkspaceRef + render-safe loadedWorkspaceId state; inFlightWorkspaceRef discards stale responses after workspace switch --------- Co-authored-by: Carlos Florêncio <carlosmflorencio@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kandev Agent <agent@kandev.dev>
111 lines
10 KiB
Markdown
111 lines
10 KiB
Markdown
---
|
|
status: draft
|
|
created: 2026-05-21
|
|
owner: jcfs
|
|
---
|
|
|
|
# Automations in Settings
|
|
|
|
## Why
|
|
|
|
Users want to schedule an agent to run a prompt on a cron (or on a GitHub PR event, or on a webhook) without first navigating to per-workspace settings, picking the right workspace, then drilling down into a workflow. They also want two execution flavors: **tracked work** that shows up on the kanban as a real task (current default), and **fire-and-forget runs** whose output is just informational — not kanban clutter.
|
|
|
|
The Automations feature, originating in PR #406, gives kandev a standalone trigger-based subsystem (cron, GitHub PR events, webhooks) that turns triggers into Tasks. This spec extends it with two changes: (1) a per-automation `execution_mode` choice between **task** (existing behavior) and **run** (new — creates an ephemeral kanban-hidden task that surfaces only through the AutomationRun row), and (2) a flat `/settings/automations` entry point that drops the per-workspace nesting from the sidebar.
|
|
|
|
## What
|
|
|
|
- Every automation has an `execution_mode` field — `task` (default) or `run`. The choice is per-automation, editable in the editor.
|
|
- Every automation has an optional `repository_id` field. When set, scheduled and webhook firings pin the task to that repository on its default branch. When empty, falls back to the workspace's first repository (legacy behavior). `github_pr` triggers always use the PR's own repository and ignore `repository_id`.
|
|
- The editor's repository picker matches the task-creation dialog's UX: lists both registered workspace repositories AND filesystem-discovered repositories under the workspace's roots. Picking a discovered repo registers it with the workspace at automation-save time (one round-trip via `createRepositoryAction`), then stores the resulting id on the automation. After the first save, the selection is promoted from `discovered` to `registered` so subsequent edits don't try to re-register.
|
|
- `execution_mode = task`: trigger fires → a normal kanban task is created (current PR #406 behavior). Task is visible on the kanban, commentable, reviewable, and has full lifecycle.
|
|
- `execution_mode = run`: trigger fires → an ephemeral task is created (`is_ephemeral = true`, `origin = "automation_run"`) so the existing session pipeline still launches an agent. The kanban hides ephemeral tasks. The AutomationRun row is the surfaced artifact; the linked task is plumbing only.
|
|
- Run-mode automations **auto-start** their agent regardless of the workflow step's `auto_start_agent` setting — the user never opens the task to drag it, so the trigger MUST be the start signal.
|
|
- The sidebar exposes a single top-level **Automations** entry pointing at `/settings/automations`. The per-workspace `Automations` sub-link is removed (PR #406 added it; this spec drops it).
|
|
- `/settings/automations` is a server-side router:
|
|
- 0 workspaces → empty state with "Create workspace" CTA.
|
|
- 1 workspace → redirect to `/settings/workspace/<id>/automations`.
|
|
- 2+ workspaces → workspace picker (grid of cards, click to enter).
|
|
- The automations table shows the execution mode as a badge column ("Task" / "Run") so the user can scan which automations clutter the kanban and which don't.
|
|
|
|
## Data model
|
|
|
|
Builds on PR #406's `internal/automation/` schema. Two columns added (folded into the canonical `CREATE TABLE` since PR #406 itself introduces the `automations` table — no in-branch migrations are needed):
|
|
|
|
```
|
|
automations.execution_mode TEXT NOT NULL DEFAULT 'task' -- 'task' | 'run'
|
|
automations.repository_id TEXT NOT NULL DEFAULT '' -- optional FK-by-id to repositories
|
|
```
|
|
|
|
The `tasks.is_ephemeral` and `tasks.origin` columns already exist (used by quick-chat). Run-mode automations set both at task-create time. New task origin constant `TaskOriginAutomationRun = "automation_run"` lives in `internal/task/models/models.go`.
|
|
|
|
`automation_runs.task_id` continues to reference the created task for both modes. Run mode just means that task is hidden.
|
|
|
|
## API surface
|
|
|
|
PR #406's WS-based API gets two new fields — `execution_mode` and `repository_id` — on:
|
|
|
|
- `automation.create` payload (input)
|
|
- `automation.update` payload (input)
|
|
- `automation.get` / `automation.list` responses (output)
|
|
|
|
No new endpoints. No HTTP routes change. Sidebar deep-links to `/settings/automations` (flat).
|
|
|
|
## State machine
|
|
|
|
Automation lifecycle unchanged. Run-mode and task-mode share the trigger → AutomationRun pipeline. The only branching is in `orchestrator/event_handlers_automation.go::handleAutomationTriggered`:
|
|
|
|
```
|
|
trigger fires
|
|
→ resolve repository
|
|
→ CreateReviewTask(IsEphemeral=mode==run, Origin=automation_run)
|
|
→ record AutomationRun (status=task_created, task_id set)
|
|
→ associate PR if github_pr trigger
|
|
→ if mode==run OR step.auto_start_agent: StartTask
|
|
```
|
|
|
|
## Permissions
|
|
|
|
Inherits PR #406's model (no per-action authorization gates). The flat `/settings/automations` page is reachable by anyone with workspace-list access, since it only lists workspaces and links into the per-workspace UI.
|
|
|
|
## Failure modes
|
|
|
|
| Dependency / invariant | Behavior |
|
|
|---|---|
|
|
| `listWorkspaces` fails on the flat page | Page renders the empty state (treating "couldn't load" as "no workspaces"). |
|
|
| Run-mode automation's task starts but agent fails | AutomationRun stays at `task_created` status; task error_message reflects the failure. User sees the failed AutomationRun row. |
|
|
| User manually drags a run-mode task on the kanban | Cannot happen — ephemeral tasks are hidden from the kanban. The "auto-start" rule fires once at trigger time; no manual recovery path. |
|
|
| Existing automation upgraded from pre-execution_mode version | Migration sets `execution_mode = 'task'` for all existing rows. UI shows them with "Task" badge. |
|
|
| User edits `execution_mode` from `task` to `run` on an enabled automation | Next firing uses the new mode. In-flight runs are unaffected. |
|
|
|
|
## Persistence guarantees
|
|
|
|
`automations.execution_mode` and `tasks.is_ephemeral` survive restart. Run-mode AutomationRuns and their hidden tasks persist normally. The kanban filter on `is_ephemeral` is applied at query time, not at write time — so re-marking a task non-ephemeral via direct DB update would reveal it.
|
|
|
|
## Scenarios
|
|
|
|
- **GIVEN** a user creates an automation with `execution_mode = "task"` and a cron trigger, **WHEN** the cron fires, **THEN** a normal kanban task appears with the rendered title; the user can click it, drag it, comment on it.
|
|
- **GIVEN** a user creates an automation with `execution_mode = "run"` and a cron trigger, **WHEN** the cron fires, **THEN** an ephemeral task is created (not visible on the kanban), the agent starts automatically, and the AutomationRun row in the automation's history shows the result.
|
|
- **GIVEN** a user opens `/settings/automations` in an install with one workspace, **WHEN** the page loads, **THEN** the browser redirects to `/settings/workspace/<id>/automations`.
|
|
- **GIVEN** a user opens `/settings/automations` in an install with three workspaces, **WHEN** the page loads, **THEN** a workspace picker is shown; clicking one navigates to its automations.
|
|
- **GIVEN** a user opens `/settings/automations` in a fresh install with zero workspaces, **WHEN** the page loads, **THEN** an empty-state card explains "create a workspace first" with a CTA.
|
|
- **GIVEN** a user toggles an existing task-mode automation to run mode in the editor, **WHEN** the next trigger fires, **THEN** the resulting task is hidden from the kanban; previously-created tasks (from task-mode firings) remain visible.
|
|
- **GIVEN** a run-mode automation triggered by a GitHub PR event, **WHEN** the trigger fires, **THEN** the PR is associated with the ephemeral task via `AssociatePRWithTask` exactly as in task mode.
|
|
- **GIVEN** a scheduled automation with `repository_id` set to a specific repo, **WHEN** the cron fires, **THEN** the resulting task is pinned to that repo's default branch — regardless of whether the workspace has other repositories.
|
|
- **GIVEN** a scheduled automation with `repository_id = ""` in a multi-repo workspace, **WHEN** the cron fires, **THEN** the task uses the workspace's first repository (legacy fallback) and a warning is logged.
|
|
- **GIVEN** an automation with `repository_id` set and a `github_pr` trigger, **WHEN** a PR event fires, **THEN** the task uses the PR's own repository, not the configured `repository_id` — the editor disables the picker for PR triggers with a hint.
|
|
- **GIVEN** a user picks a discovered (not-yet-registered) repository in the editor and clicks Save, **WHEN** the save flow runs, **THEN** the discovered repo is registered with the workspace first (`createRepositoryAction`), its new id is written onto the automation, and the picker selection is promoted to `registered` so re-saving doesn't duplicate the registration.
|
|
- **GIVEN** an upgrade from a pre-execution_mode kandev version, **WHEN** the user opens the editor for an existing automation, **THEN** the execution-mode selector defaults to "Task" (preserving previous behavior).
|
|
|
|
## Out of scope
|
|
|
|
- **AutomationRun-as-true-session-owner** (instead of ephemeral task). The cleaner model — make `task_sessions.task_id` nullable, add `task_sessions.automation_run_id`, route run-mode bypassing tasks entirely — was considered and explicitly deferred to a future PR. It touches ~50+ files in the orchestrator + session pipeline + WS layer + frontend state, which is out of scope here. The ephemeral-task path is the pragmatic shim.
|
|
- **Agent-type primary picker.** PR #406's editor still picks an `agent_profile_id` (a fully configured profile), not a raw agent type (`claude` / `codex` / `opencode`). Switching to agent-type-primary requires plumbing changes in the orchestrator (which expects a profile id). Deferred.
|
|
- **Auto-provisioned default workspace.** When no workspaces exist, the flat page shows a CTA; it does not auto-create one. Most installs already have a workspace (workspace setup is part of onboarding), so the CTA is sufficient for now.
|
|
- **Cross-workspace automation listing** on the flat page. Multi-workspace installs see a picker, not a merged list. Merging would require a new list-all endpoint and a workspace column in the table.
|
|
- A standalone AutomationRun detail page (showing session output for run-mode firings). Run-mode automations currently link to the linked task's detail page; since the task is hidden from the kanban it's reachable only by direct URL.
|
|
- Webhook execution-mode override (e.g. webhook payload setting `execution_mode` per call). The execution mode is automation-level, not per-firing.
|
|
|
|
## Open questions
|
|
|
|
- (none)
|