PR state was written by two flows and the second one could not persist: the feedback fetch returned live PR data without saving it, and the browser patched its own store copy instead. Opening a PR's detail panel turned that tab purple while the kanban card and boot payload still read state=open, and a reload snapped it back.
The backend is now the only writer — the feedback read reconciles through SyncTaskPR and publishes github.task_pr.updated, so every surface converges and the state survives a reload. SyncTaskPR also refuses to move a row out of merged, since GitHub cannot un-merge a PR.
* feat(automations): reliable cron scheduling and webhook-driven push/CI triggers
Automations were "wounded": cron schedules mishandled timezones and some
never fired, and the GitHub push/CI trigger types were coming-soon stubs.
Cron (fix):
- The scheduler reduced every cron expression to a fixed interval derived
only from a */N step in the minute/hour field, so any pinned schedule
(0 9 * * *, 30 14 * * *, weekday ranges, day-of-month) never fired, and
the configured timezone was parsed but never used.
- Replace the interval approximation with robfig/cron/v3: shouldFire now
computes the true next fire time in the configured timezone (DST-aware,
defaulting to UTC for host-independence), anchored to last-evaluated or
creation time. Dead interval-parsing helpers and shorthand constants
removed.
GitHub push + CI triggers (feat):
- Implement push and CI-failure triggers end to end via the existing
per-workspace GitHub App webhook plumbing (#1810): the HMAC-verified
webhook service now parses push and completed check_run events, resolves
the installation to its workspaces, and publishes normalized bus events.
- A new automation subscriber matches enabled github_push / github_ci
triggers by workspace + repo + branch/conclusion/check-name filters and
fires them (deduped by commit SHA / check-run id). The old poll-based
stub evaluators are removed; the PR poll path is unchanged.
- Subscribe the App manifest to push/check_run and enable both trigger
types in the registry (CI defaults to the failure conclusion).
Docs:
- Update docs/public/feature-status.md and automation-and-mcp.md so the
cron and push/CI rows reflect shipped reality.
Tests: regression tests pin the previously-never-firing cron shapes,
timezone/DST behavior, and host-independence; backend tests cover the
webhook parse->publish and subscriber match/fire/dedup flows for both
push and CI-failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(automations): populate push.message and ci.url placeholders in fired trigger data
Address PR review blockers: the default push and CI automation prompts
reference {{push.message}} and {{ci.url}}, which the interpolator maps to
data["message"] / data["html_url"] — but the webhook subscriber never
populated those fields, so both always rendered empty.
- Parse head_commit.message (push) and check_run.html_url (CI) in the
GitHub webhook service and carry them on the published bus payloads.
- Include "message" / "html_url" in the fired trigger data so the
placeholders resolve; extend the webhook and subscriber tests to assert
both flow through end to end.
Non-blocking review cleanups:
- Replace the misleading reuse of matchesAuthors for CI conclusions/check
names with a clearly-named matchesFilterValue helper.
- Drop dead context.Background() line and its unused import from the cron
regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(automations): scope webhook triggers to active installs and per-branch dedup
Address CodeRabbit review on the push/CI webhook triggers:
- Security: resolveWorkspaceIDs now only returns connections whose status is
active, so a suspended or revoked GitHub App installation can no longer fire
push/CI automations.
- Correctness: include the branch in the push dedup key
(push:owner/name@branch@sha) so the same commit pushed to two matching
branches fires once per branch instead of being suppressed.
- Correctness: add an optional head-branch filter to github_ci triggers and
gate CI matching on it, matching the push trigger's branch-scoping.
- Stability: make the webhook subscriber's Start retryable — a subscription
failure rolls back partial subscriptions and leaves the subscriber
un-started so a later Start re-attempts, instead of permanently disabling
push/CI processing.
- Tests: cover multi-connection workspace resolution (dedup + inactive
filtering), per-branch push dedup, and CI branch filtering.
- Docs: note the CI branch filter and per-branch push dedup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(automations): cover webhook subscriber Start retry-after-failure path
Add TestGitHubWebhookSubscriberStartRetryableAfterFailure with a minimal
flaky bus whose second Subscribe fails, pinning the retry contract: a
subscription failure rolls back the partial subscription and leaves the
subscriber un-started, and a later Start re-attempts and succeeds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: sync workflows from a configured github repo
Per-workspace setting pointing at a GitHub repo directory of portable
workflow export files. A background poller fetches it on an interval and
creates, updates, or deletes the matching workflows; a force-sync endpoint
bypasses the content-hash check. Synced workflows carry source/source_path
provenance and coexist with manual ones; changes that cannot be applied
safely (steps or workflows that still hold tasks, ambiguous step names,
unparseable files) surface as warnings in the new settings section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): fill workflow sync form from a pasted github link
A new Repository link field parses plain repo URLs, SSH remotes, and
tree/blob links (which also carry branch and directory) into the
structured owner/repo/branch/path fields.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): simplify workflow sync form to link, branch, and interval
The repository link is now the only repo input: owner, repo, and
directory resolve from the pasted URL (echoed as a summary under the
field), and a stored config renders back into a canonical tree link via
buildGitHubRepoUrl. Branch and poll interval stay individually editable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): collapse workflow sync into a button, dialog, and status card
The Workflows settings page now shows only a GitHub Sync button by
default; the link/branch/interval form lives in a dialog that closes on
successful save or removal. Once configured, a compact status card
(mirroring the GitHub connection-status card) shows the synced repo,
branch, directory, last result, warnings, and a Sync now action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): move github sync button into the workflows page header
The GitHub Sync trigger now stacks under the Workspace settings button
on the top right, and the extra separator around the sync area is gone —
unconfigured workspaces show no sync chrome at all, configured ones show
just the status card above the workflow list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): move github sync button next to the workflow actions
GitHub Sync now sits in the Workflows section toolbar alongside
Export All, Import, and Add Workflow; the status card renders inside the
section above the workflow list. The toolbar moved to its own component
to keep the page under the file-size limit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): separate sync status card from the workflow list
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(web): align sync status card with the workflow list gutter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: repair local edits to synced workflows on periodic sync
Periodic syncs previously skipped applying when the repo content hash
was unchanged, so local UI edits to a synced workflow were only reverted
by a manual Sync now. Every sync now reconciles against the repo; the
applier compares each step against its definition and only writes (and
broadcasts) actual differences, so a no-drift sync stays silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: make github-synced workflows read-only
Workflows with source=github reject UI mutations: workflow rename,
description, agent-profile, and delete return 409 from the task-domain
handlers; step create/update/delete/reorder and template application are
guarded in the workflow controller. The sync applier writes through the
service/repo layer and stays exempt. The frontend disables the card's
inputs, save/delete, and the step pipeline editor for synced workflows,
with a tooltip pointing at the synced repository. Drift repair on sync
remains as the backstop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: optional workflow sync polling and repo-derived branch picker
A per-workspace auto-sync toggle (poll_enabled, default on) controls
the background polling loop; when off the workspace only syncs via Sync
now. The dialog's branch field becomes a select fed by the repository's
actual branches (same loader the task-create dialog uses), falling back
to free text until branches load. Existing dev databases gain the new
column via an idempotent ALTER.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: release synced workflows when the sync config is removed
Deleting a workspace's workflow sync configuration converts its
GitHub-sourced workflows back to manual ownership (source=manual), so
they become editable instead of being stranded read-only. Release runs
before the config row is deleted so a failure keeps the config for
retry. The settings page reloads after removal (and after syncs that
changed workflows) so cards unlock without a manual refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): drop branch selector and compact the auto-sync row
The pasted link already carries the branch (main by default), so the
dialog no longer needs a branch field — the resolved summary echoes it.
The auto-sync toggle is now one compact row with the interval inline and
a single short helper line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: address PR review feedback
Backend: fail-open read-only guard on workflow lookup errors (fixes MCP
step handler CI failures); poller Stop holds its mutex through drain;
per-workspace lock serializes syncs against config delete/replace;
rollback workflow creation when provenance stamping fails; idiomatic
duplicate-column migration via db.IsDuplicateColumnError; branch-name
validation and a 30-day interval cap; generic 500s with logged detail;
WS read-only rejections use CONFLICT with the real reason; e2e reset
aborts when sync-config cleanup fails; test hygiene (t.Cleanup,
SetMaxOpenConns).
Frontend: malformed percent escapes parse as invalid instead of
throwing; save disabled during initial load and for invalid intervals;
Remove disabled while saving; stale async config responses discarded;
delete-dialog confirms re-check read-only; focusable Synced badge;
explicit button types; failed polls labeled 'last attempt'; unique
warning list keys; test cleanup via afterEach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: guard workflow sync test fake against concurrent poller access
The poller invokes the fake applier from its own goroutine while the
synctest test asserts on call counts; -race caught the unguarded reads.
Also documents that the per-workspace lock is deliberately held across
the GitHub fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: coalesce ci auto-fix queue rounds
* fix: centralize ci auto-fix round cap
* fix: keep ci auto-fix state fresh
* fix: refine ci auto-fix round status
* fix: handle ci auto-fix exhaustion edge cases
* fix: reduce ci auto-fix dispatch complexity
* fix: refresh pending ci auto-fix before direct prompt
* fix: harden ci auto-fix queue replacement
---------
Co-authored-by: root <root@kandev.ts.net>
GitHub's /search/issues API returns merged PRs with state "closed" and a
non-empty pull_request.merged_at, so the my-github search list rendered them
with the red closed icon while the detail view (which checks merged_at) showed
the correct purple merged icon.
Plumb pull_request.merged_at through both the gh CLI and PAT search converters
and promote those PRs to the "merged" state.
* fix(github): handle UNIQUE collision in UpdatePRWatchBranchIfSearching
When a sibling PRWatch already owns the destination
(session_id, repository_id, branch) triple, the raw UPDATE tripped
the UNIQUE constraint and the change was logged as an error
("failed to update PR watch branch"). Wrap the update in a tx that
first checks for a sibling; if one exists, delete the source watch
(pr_number=0, no PR state to preserve) and cede branch ownership.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(github): close TOCTOU and cover sibling-has-PR collision case
Address PR review feedback on #1285:
- Catch SQLite "UNIQUE constraint failed" on the no-sibling UPDATE path
and treat it as the same collision case (Greptile P2 inline). A
concurrent CreatePRWatch that commits between our existence probe
and the UPDATE would otherwise re-trigger the bug this PR fixes.
- Switch the existence probe from `SELECT id` + `siblingID` dummy to
`SELECT 1` scanned into a typed `probe int` with an inline comment
(Claude inline readability nit).
- Extract `dropSourceAndCommit` helper so the probe-hit branch and the
TOCTOU fallback share one code path.
- Add `TestUpdatePRWatchBranchIfSearching_CollidesWithSiblingHasPR_DropsSource`
exercising the realistic race: sibling has already found its PR
(pr_number != 0) when source tries to migrate (Claude / Greptile P2 /
cubic P3 all flagged the gap). Asserts sibling row + PR number
survive untouched after source is dropped.
* test(github): cover missing-row no-op and reframe TOCTOU comment
Address Claude bot's follow-up suggestions on #1285:
- store.go: rewrite the UNIQUE-catch comment. The original wording
implied an in-process race, but SQLite's writer pool is
SetMaxOpenConns(1) so a concurrent in-process CreatePRWatch cannot
commit between probe and UPDATE. Reframe as defensive belt-and-
suspenders for an external SQLite writer / future pool changes.
- service_pr_watch_multi_branch_test.go: add
TestUpdatePRWatchBranchIfSearching_MissingRow_NoOp covering the
ErrNoRows -> tx.Commit() idempotency contract that the new
transaction code introduces. Locks the prior UPDATE-by-id semantics
of "0 rows affected, no error" for a nonexistent id.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(tasks): search tasks by PR number in command panel
Typing a PR number (e.g. 1243 or #1243) in the Cmd+K panel now surfaces
the task that PR is associated with, alongside the existing
title/description/repo search.
The github store gains a workspace-scoped ListTaskIDsByPRNumber lookup
over github_task_prs; the task service resolves it through an injected
PRTaskResolver interface (github service wired in cmd/kandev) and merges
PR matches into search results, deduped and respecting the
archived/ephemeral/config filters. Keeping the lookup at the service
layer avoids coupling the task repository to the github schema, so
existing search queries and tests are untouched.
* fix(tasks): scope PR-number search to page 1 and unfiltered queries
Address review feedback on PR-number search:
- Only augment results on the first page of an unscoped search. Running
on every page re-prepended the PR match and truncated a real result
off each subsequent page; running under a workflow/repository filter
leaked tasks that don't satisfy the filter.
- Add idx_github_task_prs_pr_number so the lookup is index-backed
instead of a full scan (pr_number is the 3rd column of the existing
UNIQUE constraint, unusable as a leading key).
- Log instead of silently dropping a task when GetTask fails in the
PR-match loop.
* test(github): dedup PR-lookup test setup into shared newTestStore
Add workspace_id to newTestStore's tasks table and drop the near-identical
newTestStoreWithWorkspaces helper, per review feedback.
* perf(tasks): batch-fetch PR-matched tasks instead of N+1 GetTask
Add a repository GetTasksByIDs and use it in augmentWithPRMatches so PR-number
search loads all matched tasks in one query instead of one GetTask per id,
addressing the N+1 noted in review. Fetch+filter is extracted into a
fetchPRMatchedTasks helper that preserves resolver order and dedup.