All notable changes to Aexy, documented.
A local-first macOS menu-bar app that captures lightweight semantic signals (frontmost app, window title, file/git context, dev/browser context, idle state) and uploads them as append-only, idempotent event batches. A downstream Temporal/LLM pipeline enriches, attributes, and narrates the activity so time tracking happens with no manual entry.
aexy-tracker-mac/, Swift): durable local buffer, batched idempotent upload, OAuth device-code onboarding, Keychain-persisted config, and best-effort nil-safe collectors. Events are removed from the buffer only after the server confirms them./tracker/*): device enrollment, partial-success batch ingest (idempotent on event_id), heartbeat/config pull, sync high-water mark, and evidence presign. Sliding-window rate limiting (fail-open) and a 30d-past/5m-future timestamp guard. category/attribution are server-derived only — never accepted from the client.TimeEntry rows that show in the existing tracking module. Fire-and-forget per-batch dispatch (time-bucketed workflow_id coalescing) plus a 5-min safety-net sweep.WorkLog upsert), and deterministic insight signals (context switching, meeting load, after-hours, focus fragmentation) surfaced as deduped in-app notifications./tracker/qa, /tracker/timesheet): individual-scoped natural-language Q&A over one's own journals + inferred time, and a day-grouped timesheet view with confidence badges. New /tracking/tracker UI page + useTrackerTimesheet hook.TaskSelect fed by GET /tracker/candidate-tasks), or dismiss it, via PATCH /tracker/timesheet/entries/{id} and a new attribution_status column. Dismissed entries drop out of totals. Page fully localized (tracking.tracker namespace, en/hi); Q&A now follows the selected date range and the date picker no longer shifts a day in non-UTC zones.GET /auth/device/login?provider=&port=, captures the developer JWT on a 127.0.0.1 loopback listener (RFC 8252), exchanges it for a long-lived aexy_… API token (POST /developers/me/api-tokens), and enrolls — no env vars or manual code entry. Replaces the dead device-code default that 404'd.docs/aexy-tracker.md (feature + macOS client + sign-in) and docs/api/tracker-ingest.md (ingest + device-login contract), linked into the handbook nav; code references repointed to them.OAUTH_EXTRA_REDIRECT_HOSTS, or a 127.0.0.1/localhost loopback (native apps). All provider login/connect entry points reject a disallowed redirect_url with 400, and every callback funnels through one guarded chokepoint, closing a token-exfiltration vector where an attacker-supplied redirect_url could capture a victim's token.FOR UPDATE SKIP LOCKED) and is backstopped by a partial unique index on inferred time_entries dedupe keys, so the per-batch dispatch and the periodic sweep can't double-attribute the same events into duplicate time entries.confidence values instead of crashing (and Temporal-retrying) the whole activity.end date (added the missing upper logged_at bound).accepted + duplicates + rejected reconciles to events sent; insight runs no longer overcount notifications suppressed by recipient preferences.1…600s range.Cross-project move (0.8.34) silently re-resolved the new task's
status to the destination's first "open" status. For sibling boards
that's fine; for cross-board moves (Product → Tech) the user
usually has a specific column in mind and the default was wrong.
MoveToProjectModal now fetches the destination project's status
set via the existing useTaskStatuses hook once a target is
picked, and renders a "Status on destination board" dropdown. The
default selection follows: same slug on the target → same name
(case-insensitive) → first active status by position. The picked
slug rides through as target_status_slug on both the single and
bulk move requests; the backend (SprintTaskService.move_to_project)
validates it against TaskConfigService.get_statuses_for_project
before any write, raising invalid_target_status (400) on
mismatch. Bulk move applies one status to every cloned task.
_clone_task_to_project now accepts an override_status_slug and
short-circuits the open-status resolver when supplied. Subtasks
under cascade still resolve their own open status — the picker is
parent-only, which matches the existing "subtasks inherit the
destination's defaults" semantics.
SprintTask.is_archived and the unarchive endpoint have existed
since the early sprint module, but no UI ever surfaced archived
rows. Once a task was archived (manually or as part of a cross-
project move), it disappeared.
Both /sprints/[projectId]/board and the workspace All-Tasks tab
get an Active | Archived segmented toggle (URL-synced via
?view=archived so reloads and link-shares round-trip). In
archived view:
TaskTableView — archived rows don'tbelong in status columns, and the table is the right surface for
a flat list. The Board/Table layout toggle, Sprints/Status
view-mode toggle, Add Task, Columns shortcut, Import button, and
priority/labels/epics filters are all hidden (search, assignee,
project, sprint stay). On the board page this is driven by a new
minimal flag on the existing FilterBar component.
the workspace tab gains an "Restore selected" entry that fires
parallel unarchives.
include_archived; both endpoints now also accept archived_only. list_project_tasks
was hard-coded to is_archived = false — that's been generalized
to the same flag pair. archived_only is strict regardless of
include_archived.
New useUnarchiveTask hook wraps projectTasksApi.unarchive and
reuses invalidateTaskCaches so the active view re-fetches
correctly when a row is restored.
POST /analytics/workload was crashing with
AttributeError: 'WorkloadRequest' object has no attribute 'days'
because the handler read request.days but the schema didn't
declare the field. The frontend has been sending days: 30 since
that endpoint shipped. Added days: int = 30 to the schema.
Cross-project moves (shipped in 0.8.34) already created a
task_dependencies row linking the new task back to the source —
but nothing in the UI rendered that linkage. Anyone opening either
side of the move saw a context-free task.
SprintTaskService.move_to_project now prepends a one-line
"Moved from <KEY> — <title>" breadcrumb to the new task's
description and a matching "Moved to" line on the source. The
breadcrumb is written into both description (plain text) and
description_json (a ProseMirror paragraph with a link mark
pointing at /sprints/<team>/board?task=<id>) so every surface
that renders descriptions shows it without any extra UI plumbing.
Cascade subtasks each get their own pair of breadcrumbs pointing
at the corresponding clone — the parent's pointer alone wouldn't
reach the children.
The existing task_dependencies row is still recorded as the
structured source of truth for any future banner work.
The flat "Apps" section in the docs sidebar (0.8.35) is replaced
with a sidebar that mirrors the main app sidebar's grouping —
Engineering / People / Business / AI / Compliance — plus a
"Recent" strip at the top tracking the user's last-visited apps.
Implementation:
recentAppsStore (Zustand + localStorage, cap 8) records each app visit. Mounted once in app/(app)/layout.tsx via
useRecentApps() so visits from any surface count.
NotionSidebar reads the main sidebar's GROUPED_LAYOUT, applies the same persona filter (useSidebarPersona) and
app-access filter (useAppAccess) the main sidebar uses, and
renders each section collapsed by default to keep the docs
surface focused.
sidebar IS the knowledge view; re-listing it would be
tautological). Docs and Drive are filtered out of the Recent
strip for the same reason.
SidebarAppGroup component renders apps with sub-items(Tracking → Standups/Blockers/Time, etc.) as expandable rows
inside the section, matching the main sidebar's depth.
Reported: "after typing the doc refreshes and the cursor becomes
deselected".
Root cause was on the page, not the editor. /docs/[documentId]/page.tsx
was passing isLoading={isUpdating} to DocumentEditor, where
isUpdating is the mutation-pending flag from useDocument's
updateContent mutation. DocumentEditor returns its loading skeleton
when isLoading is true — so every debounced autosave kicked off by
typing flipped isUpdating to true, the editor was replaced by the
skeleton, then isUpdating flipped back to false and the editor was
remounted — fresh TipTap instance, fresh selection, cursor lost.
Removed the prop. The page-level initial-load guard (above the
component) still shows a skeleton on first fetch; once the document
is loaded the editor stays mounted, and the in-editor "Saving… / Saved"
indicator reflects save state without tearing anything down.
0.8.35 gated BubbleMenu on editorMode === "rich" thinking the
crash was a mode-switch race. The user kept hitting the same
removeChild error while selecting text in rich mode — the gate
fixed the switch path but not the steady-state path. Re-diagnosis:
@tiptap/react's BubbleMenu wraps Tippy.js.document.body, outside theReact tree.
selectionchange causes BubbleMenu to remount its Tippyinstance, moving DOM nodes between body and the editor.
no longer owns it → `NotFoundError: Failed to execute 'removeChild'
on 'Node'` in the commit phase.
This is a known incompatibility between @tiptap/react's BubbleMenu
and React 18+ concurrent reconciliation
(ueberdosis/tiptap#3580, #2658).
Removed the BubbleMenu entirely. The top EditorToolbar already
exposes Bold / Italic / Underline / Code, so the affordance isn't
lost — only the floating bubble. If we want the bubble UX back, the
replacement should use @floating-ui/react (in-tree positioning)
rather than Tippy.
Two docs surface fixes.
The main app sidebar is hidden on /docs/* routes, so the docs sidebar
(NotionSidebar) was the only navigation chrome — but it had no path
to other modules. Users had to back out via browser nav or memorize
URLs to jump to Sprints, CRM, etc.
Added a collapsed-by-default "Apps" section at the bottom of the docs
sidebar (above the divider before "Add space"). It reuses
useAppAccess(workspaceId, developerId) to list only the apps the
current user can access, with each row linking to that app's
baseRoute from APP_CATALOG. Same access logic as the main sidebar
— no new permissions surface.
Reported: selecting text in /docs/[id] would intermittently throw
`NotFoundError: Failed to execute 'removeChild' on 'Node': The node to
be removed is not a child of this node` in the React commit phase.
Root cause: DocumentEditor's BubbleMenu was rendered when
editor && !readOnly, regardless of editorMode. In markdown mode
the EditorContent is replaced by a <textarea>, but the BubbleMenu
(and its Tippy.js portal) stayed mounted. Any subsequent selectionchange
would race React reconciliation — Tippy holds DOM references that React
no longer owns, the next reposition tries to removeChild a detached
node, and the commit phase throws.
Fix: gate BubbleMenu on editorMode === "rich" so it tears down
cleanly when the user switches modes. One-line conditional change in
frontend/src/components/docs/DocumentEditor.tsx.
New: cross-project task move (fork + link). A task can now be moved to
another project in the same workspace; a fresh task is created in the
destination, linked back to the source as a "duplicates" dependency,
and the source is either archived or marked done at the operator's
choice.
Moving the row in place would orphan the source's history, sprint
membership, comments, and attachments — and task_key is workspace-
scoped but tasks reference sprint/epic/story IDs that don't translate
across projects. A new task in the destination plus a task_dependencies
link preserves provenance while letting the destination start fresh.
source_action, subtask_strategy, actor_id) and a bulk_move_to_project`
variant that returns per-task results (one failure doesn't abort the
batch). See plan mutable-herding-flute.md for the full contract.
api/project_tasks.py: - POST /teams/{team_id}/tasks/{task_id}/move-to-project
- POST /teams/{team_id}/tasks/bulk-move-to-project
cross_workspace_move, same_project_move, target_project_not_found,
task_already_archived, task_has_subtasks,
source_task_not_found, invalid_source_action,
invalid_subtask_strategy.
- block (default, safest) — reject the move if subtasks exist.
- cascade — clone every active subtask into the destination under
the new parent; archive each original subtask.
- orphan — leave subtasks in place; their parent_task_id still
points at the archived/done source.
- archive — is_archived=True on the source.
- mark_done — set the source's status to its project's first
semantics="done" slug (workspace fallback, then canonical "done")
and set completed_at = now() if null.
member of the target project; otherwise cleared. Sprint, started_at,
completed_at, cycle/lead time, epic, story, and parent_task_id are
intentionally not copied — see the plan for the rationale.
moved_to_project on the source (carries new task's id/key and the chosen strategies in activity_metadata)
and created_from_move on the new task (carries source's id/key).
task_dependencies with dependency_type="duplicates" is the link mechanism.
MoveToProjectModal (components/planning/MoveToProjectModal.tsx)used by both single-task and bulk entry points. Project picker excludes
the source project and any archived project. Subtask-strategy radio
shows only on single-task moves when the task has subtasks.
useTaskMove hook (hooks/useTaskMove.ts) wrapping both the single and bulk mutations, with invalidateTaskCaches integration and
friendly toast messages mapped from each stable error code.
EditTaskModal sidebar (project board) gains a "Move to project…"button above "Archive Task".
button next to the existing "Move to Sprint" dropdown.
backend/tests/unit/test_task_move_to_project.py:happy path, mark-done variant, cross-workspace reject, same-project
reject, archived-source reject, subtask block / cascade / orphan,
assignee membership rule, sprint+timing fields not copied,
activity logged on both tasks, bulk-move continues on per-task
failure.
Follow-up sweep on the 0.8.32 status work — two production bugs and the
missing admin surface for editing categories themselves.
PATCH /teams/{id}/tasks/{id} was rejecting any non-canonical slug
with a Pydantic literal_error:
`
Input should be 'backlog', 'todo', 'in_progress', 'review' or 'done'
`
Root cause: TaskStatus was still a Literal[...] at the schema
layer, defeating the whole point of project-scoped custom statuses
from 0.8.32. Two-part fix:
TaskStatus = str in both backend/src/aexy/schemas/sprint.py and frontend/src/lib/api.ts. Any slug parses; validity is decided at
write time, not parse time.
SprintTaskService.validate_status_slug(task, slug) checks the slug exists in the task's scope (workspace_task_statuses rows for
the project OR workspace defaults). On miss → 400 unknown_status.
Wired into both update_task and update_task_status, on both
PATCH endpoints (/teams/.../tasks/... and
/sprints/.../tasks/...).
The canonical five seed slugs (backlog, todo, in_progress,
review, done) are accepted unconditionally so workspaces that
pre-date the status table aren't bricked by tasks carrying slugs
without matching rows.
Production was showing two columns titled On Hold on a kanban — the
admin had typed the name twice and create_status had silently
deduplicated only the *slug* (storing on_hold and on_hold_1).
Both rendered because the column title comes from name, not slug.
create_status and update_status now share an _assert_name_unique
helper that rejects case-insensitive name collisions within a scope
(workspace + project): error code status_name_exists, HTTP 400.
This prevents the future occurrence but does not clean up existing
duplicate rows in production data — admins need to delete one of the
duplicates via the new admin UI (below).
/settings/projects/{projectId}/statuses gains a "Categories" section
above the existing statuses list:
CategoryModal — create / edit a category with label, semantics(Open / Active / Done / Cancelled), and color. Slug is auto-derived
from the label on create and locked on edit (existing statuses
reference it as a string).
SortableCategoryItem — compact row with color swatch, semanticsbadge, edit/delete menu.
category) and server-side (category_in_use error, HTTP 400).
backend/tests/unit/test_task_status_validation.py (new, 4 tests) —canonical slug accepted, project-scoped custom slug accepted,
unknown slug rejected, slug scoped to a different project rejected.
backend/tests/unit/test_status_categories.py (+1) — test_create_status_rejects_duplicate_display_name pins the
case-insensitive name uniqueness check.
Two threads landing together:
1. DB-driven status categories. The category on each task status
was previously locked to three Literal values (todo,
in_progress, done). It's now a free-form slug validated
against a new workspace_status_categories table that ships six
canonical buckets per workspace (backlog, todo, in_progress,
in_review, done, cancelled) and is open to admin additions.
2. Project-scoped statuses actually reach the board. The
useTaskStatuses(workspaceId, projectId) hook + endpoint existed
since 0.8.29, but both the project board (sprints/[id]/board)
and the workspace All-Tasks tab were silently rendering hardcoded
5-status arrays. They now call the hook and render whichever
statuses the project (or workspace fallback) defines.
3. Board ↔ Table layout toggle. The orphaned Settings2 button
in the board toolbar is replaced with a LayoutGrid | Table2
pill; the workspace All-Tasks tab gains the same toggle. Layout
is persisted per scope via the new useTasksLayout hook.
backend/scripts/migrate_status_categories.sql creates workspace_status_categories and seeds the six canonical buckets
for every existing workspace. The unique index uses
COALESCE(project_id::text, '') so workspace defaults and project
overrides occupy separate uniqueness buckets, matching the pattern
already in use for workspace_task_statuses.
semantics field (one of open, active, done, cancelled). All business logic that needs to branch on
completion (burndown, velocity) should read semantics — slugs
are user-facing and renameable.
StatusCategory in backend/src/aexy/schemas/sprint.py becomes str; CategorySemantics is the new Literal. The frontend
mirror in lib/api.ts matches.
TaskConfigService: get_categories, get_categories_for_project,
create_category, update_category, delete_category,
reorder_categories, seed_default_categories.
/workspaces/{id}/status-categories with the same ?project_id= scope filter as /task-statuses.
create_status / update_status validate the category slugagainst the workspace's category set (with project fallback) and
raise TaskValidationError("unknown_category") on miss.
Workspaces created before the categories table existed get
lazy-seeded on first write so legacy data never trips.
StatusModal accepts a categories prop instead of a hardcodedarray. Each cell renders the category color, label, and a small
semantics chip; the title attribute carries the burndown hint.
Both consumers (project statuses page + workspace task-config
page) wire useStatusCategories and thread it through.
frontend/src/app/(app)/sprints/[projectId]/board/page.tsx calls useTaskStatuses(workspaceId, projectId) and renders status
columns from the resolved set (project rows or workspace
fallback). The hardcoded five-column STATUS_CONFIG is kept only
as a label/color fallback for the canonical slugs.
WorkspaceTasksTab.tsx does the same when exactly one project isfiltered in; otherwise it falls back to workspace defaults.
useProjectBoard.tasksByStatus and useWorkspaceTasks.tasksByStatus are now Record<string, _>
instead of Record<TaskStatus, _> so custom slugs bucket correctly.
frontend/src/hooks/useTasksLayout.ts — localStorage-backed "board" | "table" preference, scoped per surface
(board:<projectId> for each project, workspaceTasks for the
All-Tasks tab).
frontend/src/components/planning/TaskTableView.tsx — shareddense table view used by both pages. Columns: Key, Title, Status
(with the project-scoped color dot), Priority, Assignee, Sprint,
Pts, Updated. Sticky header, hover row, bulk-select column,
row-click opens the same detail surface as the kanban cards.
Settings2 button for a segmented Board/Table pill; WorkspaceTasksTab adds the same
pill in its toolbar alongside the existing project-statuses link.
tests/unit/test_status_categories.py (7 tests)covers: canonical seed, fallback resolver, project override,
unknown-category rejection on create + update, lazy-seed for
legacy workspaces, and refusal to delete a category in use.
- src/test/useTasksLayout.test.ts — persistence, hydration,
malformed-value guard, scope isolation.
- src/test/StatusModal.test.tsx — dynamic categories rendering,
submit payload uses the selected slug, empty-state hint.
e2e/tasks-view-toggle.spec.ts — a custom project status (design_review) surfaces as a kanban column on
the board; the Board ↔ Table toggle swaps content and persists
across reload via the scoped localStorage key.
CREATE TABLE IF NOT EXISTS + ON CONFLICT DO NOTHING for the
seed.
migration also retags the seeded "Backlog" status from
category=todo → backlog and "In Review" from in_progress →
in_review for workspaces that hadn't renamed those rows.
test_task_config_project_scope.py continue to pass against the
updated seed (it already used the in_review slug).
Moves the status admin to its semantic home: project-scoped statuses
now live at /settings/projects/<id>/statuses next to General /
Permissions / Repositories, instead of the workspace settings page
with a ?project= query param. The workspace task-config page keeps
its workspace-defaults mode; the project-scoped UI moves out.
frontend/src/app/(app)/settings/projects/[projectId]/statuses/page.tsxhosts the project status admin in the same shell as General /
Permissions: matching breadcrumb, project header chip, tab nav
including the new Statuses link.
useTaskStatuses(workspaceId, projectId), the DeleteStatusModal, and the auto-fork backend from 0.8.29-0.8.30 —
no new service or API.
position as the workspace settings page's version. Rows render
read-only with a Workspace default chip until the fork happens.
SortableStatusItem to frontend/src/components/settings/SortableStatusItem.tsx — the row component used by both task-config/page.tsx and the
new project statuses page. Includes the readOnly mode introduced
in 0.8.30.
StatusModal (the add/edit form) to frontend/src/components/settings/StatusModal.tsx. Both pages
import it; the workspace page's inline copy is gone.
/settings/projects/[projectId] and .../permissions) pages grow a Statuses tab link. Repositories sub-page keeps its
back-button layout untouched.
Columns deep link on the project board (/sprints/[projectId]/board) now points at
/settings/projects/<id>/statuses instead of
/settings/task-config?tab=statuses&project=<id>.
only renders when the user has filtered to a single project.
/settings/task-config keeps its existing project picker for now;it still works but the project deep links no longer point at it.
Once usage shifts to the new route the project-mode dropdown there
can be retired.
useProject(workspaceId, projectId) was already exporting isLoading — no hook changes needed for this PR.
Finishes the project-scoped statuses UX: tasks no longer get orphaned
when a column is deleted; the project board has a direct entry point
into status editing; fallback projects render their inherited columns
as visually read-only; and adding a project status from fallback now
snapshots the workspace defaults first so the project doesn't lose
its inherited columns.
TaskConfigService.delete_status(status_id, migrate_to_status_id=None)now optionally rewrites every task pointing at the source status
(sprint_tasks.status_id and the legacy status slug column) to
the chosen target before the soft delete. Validation refuses a
cross-workspace target, refuses a project-scoped target for a
workspace-default delete (tasks come from across the workspace),
refuses a different project's target for a project-scoped delete,
and refuses self-target.
GET /api/v1/workspaces/{ws}/task-statuses/{id}/usage returns { count } — powers the delete modal's "N tasks use this status"
copy.
DELETE /api/v1/workspaces/{ws}/task-statuses/{id} now accepts a ?migrate_to=<uuid> query param.
frontend/src/components/settings/DeleteStatusModal.tsx replaces the previous confirm() dialog. Renders the usage count, requires
a target status when count > 0, defaults the target to a same-
category sibling for sensible fallback, and surfaces the backend's
stable error codes inline.
create_status(project_id=...) for a project that's currently onfallback now clones the workspace defaults into that project before
inserting the new row. Without this, the resolver would flip from
"5 inherited statuses" to "1 manually-added status" the moment an
admin clicked Add Status from a per-project view — silent column
loss.
frontend/src/app/(app)/sprints/[projectId]/board/page.tsx gets a"Columns" link in the toolbar (next to Add Task) that deep-links
to /settings/task-config?tab=statuses&project=<projectId>.
frontend/src/components/planning/WorkspaceTasksTab.tsx shows thesame link in the All-Tasks header when filtered to a single project.
task-config/page.tsx reads ?project=<uuid> from the URL andpreselects the scope dropdown so the deep links land where they
promise.
SortableStatusItem gains a readOnly prop. When the page is in per-project mode and the project is in fallback (isUsingWorkspaceFallback),
rows render with a Workspace default chip and the drag-handle /
edit / delete affordances hide. The single primary action becomes
the existing "Customize for this project" CTA.
test_task_config_project_scope.py: - count_tasks_using_status returns the count.
- delete_status with a target rewrites both status_id and the
legacy status slug on every affected task.
- delete_status without a target leaves tasks pointing at the
now-inactive row (legacy slug still renders the card).
- Cross-workspace / cross-project migration targets are rejected
with migration_target_other_workspace / migration_target_other_project.
- create_status(project_id=...) on a fallback project copies the
workspace defaults in before adding.
exactly one row" was updated to match the new auto-snapshot
behavior; the invariant it now expresses is "the resolver returns
project-scoped rows once any exist", which is what the codebase
actually relies on.
Project statuses are now genuinely isolated from workspace edits. The
0.8.28 release introduced project-scoped statuses with a workspace
fallback; this release closes the gap where a fallback project would
still see workspace renames, deletions, and reorders flow through.
TaskConfigService._snapshot_fallback_projects(workspace_id)finds every project in the workspace that has no project-scoped
status row of its own and runs clone_workspace_statuses_to_project
for each, capturing the current workspace defaults.
update_status and delete_status now invoke the snapshot beforeapplying the change when the target row is a workspace default
(project_id IS NULL). Editing a project-scoped row is a no-op for
the snapshot — those projects already own their statuses.
reorder_statuses invokes the snapshot when any of the reorderedIDs is a workspace default; reordering changes a project's visual
workflow and counts as destructive for the same reason as a rename.
create_status (workspace) is intentionally not wrapped — addinga new status is additive, so fallback projects pick it up via the
resolver without being auto-forked into snowflakes.
db.commit is the last step in update_task_status / delete_task_status /
reorder_task_statuses), so a partial failure rolls back cleanly.
test_task_config_project_scope.py:- Workspace rename snapshots the fallback project (project keeps
the old name).
- Workspace add does not snapshot (project stays in fallback
and resolves the new status via the workspace defaults).
- Workspace delete snapshots the fallback project (project keeps
the deleted status as an active project override).
- Workspace reorder snapshots the fallback project (project keeps
the original order).
- Workspace edit with a mixed project set leaves the already-
customized project untouched and only forks the fallback one.
This release is backend-only. The discoverability work proposed
alongside this (kanban-header drawer, `/sprints/[projectId]/settings/
statuses` route, delete-with-task-migration modal, read-only
"Workspace default" preview, "reset to workspace defaults" undo)
will land in a follow-up PR. Operators editing statuses today still
use /settings/task-config with the project picker.
Workspace All-Tasks gains inline create, statuses become per-project
(with a workspace fallback), and the kanban picks up a round of
Linear-style polish. Backend tests now run against SQLite without
the previous ARRAY/JSONB schema-compile blocker.
WorkspaceTasksTab (/sprints?tab=tasks) was read-only. Adds a hover-only + button per column, a Trello-style dashed "+ New
task" row at the bottom of every column (Enter to submit, Esc to
cancel, refocus on success for rapid entry), and a global "+ Add
task" button in the filter bar.
AddWorkspaceTaskModal (components/planning/AddWorkspaceTaskModal.tsx)— compact, keyboard-first form with Project, Sprint, Status,
Priority, Assignee, Story points, dates, and Estimate. Status
renders as a locked chip when the modal is opened from a column,
so the new card lands in the column the user clicked.
POST /api/v1/workspaces/{ws_id}/tasks endpoint (api/workspace_tasks.py) backed by SprintTaskService.add_workspace_task.
Resolves team_id from project_teams, validates that the sprint
(if any) belongs to that team, and rejects a status_id that
belongs to a different project (returns one of the stable error
codes project_has_no_team / sprint_not_in_project /
status_belongs_to_other_project so the frontend can branch on
the detail string).
localStorage so successivequick-adds land on the same project without re-picking.
migrate_project_task_statuses.sql: adds a nullable project_id UUID column to workspace_task_statuses and replaces
the workspace+slug unique constraint with a scoped expression
index (workspace_id, COALESCE(project_id, ''), slug). Existing
rows keep project_id = NULL and continue to act as workspace
defaults; rows with project_id set are project overrides.
TaskConfigService.get_statuses_for_project(workspace_id, project_id)returns the project's own status rows when any exist, falling
back to workspace defaults otherwise. This is the single helper
the column UI, task-create validation, and the status admin API
all share.
clone_workspace_statuses_to_project service helper + POST /workspaces/{ws}/projects/{p}/task-statuses/clone-from-workspace
endpoint — idempotent fork-the-defaults action that powers the
new "Customize for this project" CTA on the Statuses settings
page.
GET /workspaces/{ws}/task-statuses now accepts ?project_id=<uuid>; POST /task-statuses accepts project_id
in the body. Response schema gains a project_id field.
useTaskStatuses(workspaceId, projectId?) switches scope, exposes cloneFromWorkspace and an
isUsingWorkspaceFallback flag for the CTA.
settings/task-config) gets a project picker; inper-project mode and using fallback statuses, an info banner
offers the one-click clone.
backend/scripts/backfill_project_task_statuses.py — operator CLIthat clones workspace defaults into existing projects. Flags
--workspace-id, --project-id, --all, --dry-run. Idempotent
(skips projects that already have overrides). The non-migrate*.sql
filename keeps it out of the migration runner so it only runs
when invoked explicitly.
selected via shift-click / per-card checkbox): bulk "Move to…"
status change plus Clear.
?q=, ?assignee=, ?priority=, ?team=, ?sprint= round-trip so refresh / back-button / link-sharing
reproduces the view.
n opens the new-task modal; / focuses thesearch input.
count stay visible while scrolling long lists.
column-shaped placeholders with staggered card animation delays.
md (full-width,no max-height) instead of forcing a horizontal scroll on phones.
page rendered "No tasks found" and hid the columns — making the
new inline quick-add unreachable. The full empty-state now only
appears when filters are active and matched nothing.
core/database.py registers SQLite dialect shims via @compiles(... "sqlite") for ARRAY → JSON, JSONB → JSON,
INET → VARCHAR(45). Models declared with PG-only types now
compile under sqlite+aiosqlite:///:memory: so the test suite
reaches the test bodies instead of failing in
Base.metadata.create_all(). 401 previously-blocked tests now
run; remaining failures are pre-existing fixture issues
unrelated to this PR.
'::jsonb' casts from four server_default literals in models/dashboard.py and models/crm.py so SQLite accepts the
DDL. PostgreSQL still parses the bare '[]' / '{}' defaults
into JSONB.
setupTaskBoardMocks now sets the aexy_authed presence cookie via page.context().addCookies(),
preventing the middleware from bouncing every spec to / and on
to /onboarding. Unblocks task-card-drag,
task-create-attachments, task-link-clickable,
task-over-estimate, task-attachment-ai-tags, and
task-overdue-badge in addition to the two new
workspace-tasks-create specs.
backend/tests/unit/test_task_config_project_scope.py — 5 unittests covering fallback to workspace defaults, project override
preference, no cross-workspace leak, clone copy fidelity, and
clone idempotency.
backend/tests/integration/test_workspace_tasks_api.py — 5 APItests covering the happy path, cross-project status rejection,
project-without-team rejection, status-list fallback, and clone
idempotency.
frontend/e2e/workspace-tasks-create.spec.ts — Playwright specexercising the inline quick-add row (asserts the wire shape:
title, project_id, status) and the global "Add task" modal.
lib/api.ts: new workspaceTasksApi.create(), taskConfigApi.getStatuses({ projectId }), and
taskConfigApi.cloneToProject().
addTask, newTaskPlaceholder, refreshed dropTasksHere copy in both en and hi.
Part B follow-ups: close the three loops Part B's commit message
flagged as "deferred". All three streams of AI-generated content
now route through the proposed-edits queue, the doc owner gets a
notification each time a proposal lands, and the stale-conflict
view exposes a Regenerate action to refresh against the current
base.
DocumentSyncService.regenerate_document and process_queue were referenced by the Temporal regenerate_document /
process_document_sync_queue activities but didn't exist on the
service — the whole sync regen path was dead. Implemented both,
routing through ProposedEditsService.create_proposal with
source=code_change_sync.
_trigger_real_time_sync no longer marks the doc pending_regeneration and forgets about it — it generates fresh
docs and creates a proposal via a new shared _generate_and_propose
helper.
POST /workspaces/{ws}/documents/{doc_id}/suggest-improvements/apply. Takes a suggestion_summary query string (copy/pasted from the
improvements[].suggestion field returned by the existing
suggest-improvements endpoint), runs it through
DocumentGenerationService.update_documentation, and lands the
result as a pending proposal with source=suggest_improvements.
The legacy GET-style suggest-improvements keeps its
"return-suggestions-list" contract; the new endpoint is the
"apply this one" action.
ProposedEditSource lifecycle now fires a DocumentNotification to the document's created_by_id with the new AI_PROPOSAL
type. Self-notifications (proposer == owner, e.g. owner-triggered
manual regenerate) are suppressed. Best-effort: if the doc has no
created_by_id, the notification step is a no-op (legacy fixture
safety).
DocumentNotificationType.AI_PROPOSAL enum value (backend/src/aexy/models/documentation.py).
ProposedEditReview gets a new optional onRegenerate prop. Whenthe proposal is stale AND a handler is wired, the merge-conflict
view renders a third action between Reject and "Apply anyway":
Regenerate.
ProposedEditsBanner wires this to a new regenerate mutation that calls documentApi.generate(workspaceId, documentId) — the
new proposal supersedes the stale one server-side via
create_proposal's supersede sweep, so we just invalidate the
query cache afterwards.
asserts this).
test_proposed_edits_service.py extended with TestNotificationOnCreate (3 specs): notification fired for
owner, no self-notification, no notification when owner is
missing.
docs-proposed-edits.spec.ts extended with twospecs: stale conflict renders Regenerate + clicking it calls
POST /generate; non-stale proposals don't show the button.
Total docs E2E: 29 specs, ~60 s.
Bumped both backend/pyproject.toml and frontend/package.json
to 0.8.27.
Part B of the AI documentation initiative: the **proposed-edits
review queue**. AI-generated content no longer overwrites
document.content directly — it lands in a pending queue the user
approves or rejects through a banner above the editor.
(backend/scripts/migrate_document_proposed_edits.sql). Columns:
`id, document_id, source, proposed_content (jsonb),
base_content_sha, diff_summary (jsonb), status, proposed_by_id,
proposed_at, reviewed_by_id, reviewed_at, reason`. Indexed on
(document_id, status) for the banner's hot read path and on
(document_id, base_content_sha) for stale-detection lookups.
aexy.models.documentation + ProposedEditSource and
ProposedEditStatus enums. Wired into models/__init__.py's
__all__.
ProposedEditCreate, ProposedEditResponse (carries computed is_stale), ProposedEditReject.
backend/src/aexy/services/proposed_edits_service.py) - create_proposal snapshots the current content_sha if the
caller didn't supply one, then auto-supersedes prior pending
proposals on the same document. The new row is flushed before
the supersede UPDATE runs, so the new proposal's id can be
referenced in the supersede reason without a null-id race.
- approve routes through DocumentService.update_document
which creates a DocumentVersion automatically — every approved
proposal lands as a versioned change.
- reject records an optional human-readable reason.
- is_stale compares the proposal's base_content_sha against
the document's current SHA; rows without a base are never
flagged (legacy / migration safety).
- compute_content_sha is key-order invariant (sort_keys=True)
so JS round-trips that re-serialize equivalent content don't
spuriously trigger the stale badge.
changed: now creates a pending proposed_edit instead of writing
to document.content. Legacy overwrite behaviour is preserved
behind ?apply=true for scripted / migration callers.
list pending (default), or ?status=approved|rejected|superseded|all.
transitions; bumps the version chain via DocumentService.
pending proposals exist. Groups by source (regenerate,
code_change_sync, suggest_improvements, manual_ai_edit)
with distinct icons/labels per group. Click a proposal to expand
the review inline.
- Summary (default): sections added / removed / headings
changed, scannable, no scrolling.
- Unified: full JSON view in a scroll container.
- Side-by-side: current vs proposed columns.
Approve / Reject actions live in the footer; Reject opens an
inline reason input. When proposal.is_stale is true, the
banner shows the merge-conflict UX and the Approve button copy
flips to "Apply anyway".
editor. The component self-hides when there are no pending
proposals — no layout shift on docs that don't have AI edits.
rejectProposedEdit}** added to lib/api.ts plus ProposedEdit`,
ProposedEditSource, ProposedEditStatus types.
tests/unit/test_proposed_edits_service.py — 10 unit tests covering compute_content_sha invariants
(deterministic, key-order invariant, None == {}), create_proposal
(SHA snapshotting, flush-before-supersede ordering, string-source
acceptance), and is_stale (no-base / matching / diverged).
e2e/docs-proposed-edits.spec.ts — 5 specs coveringbanner rendering, all three diff modes (summary / unified /
side-by-side toggle), approve flow, reject-with-reason flow, and
the stale conflict UX.
Full backend unit suite for docs: 10 specs pass.
Full docs E2E: 27 specs, ~58 s.
Run python scripts/run_migrations.py (the new
migrate_document_proposed_edits.sql is the only pending change).
No backfill needed — proposals only land going forward, legacy
generate callers that pass ?apply=true keep working unchanged.
Part A of the AI documentation testing initiative: TDD coverage for
autogenerate flows + the autoupdate plumbing. The audit had flagged
that the entire docs-AI surface had zero tests; this commit closes
that with 11 specs and surfaces three bugs along the way, two of
which are fixed in the same change.
Part B (proposed_edits model + approval UX) lands separately.
(backend/src/aexy/services/document_sync_service.py:68).
Line referenced PlanTier.TEAM.value but the enum has no TEAM
member. Every free-tier or pro-tier-without-realtime developer
hit AttributeError when get_sync_type_for_developer was called.
Fixed to PlanTier.ENTERPRISE.value, matching the convention used
in api/knowledge_graph.py, api/notifications.py,
api/app_access.py. Caught by test_document_sync_service.py.
DocumentGenerationService.suggest_improvements claims to return
`{quality_score, improvements[], missing_sections[],
overall_assessment}` but was returning generic code-analysis JSON
(languages, frameworks, code_quality, summary) because:
1. lmstudio_provider._build_analysis_prompts had no branch for
AnalysisType.DOC_* types — they fell through to
CODE_ANALYSIS_PROMPT, dropping the service's custom prompt.
Fixed by adding a DOC_* branch that honours
request.context["system_prompt"] + uses the pre-formatted
request.content verbatim.
2. The service's json.loads(result.raw_response) blew up on
markdown-fenced LLM output. Extracted _parse_llm_json helper
that strips `json fences before parsing. Applied to all four
raw_response parse sites in the service.
3. Tightened DOC_IMPROVEMENT_SYSTEM_PROMPT to say "Respond ONLY
with valid JSON … No preamble, no analysis, no markdown fences".
4. Bumped lmstudio_config max_tokens in the AI test conftest
from 2048 → 8192 so Qwen "thinking" models don't run out of
budget before producing JSON.
Caught by test_suggest_improvements.py::test_returns_documented_contract_shape.
frontend/src/components/docs/SyncStatusPanel.tsx).221 LOC of pending-changes UI implemented but never mounted in
any page. Wired into app/(app)/docs/[documentId]/page.tsx:
uses useDocumentCodeLinks to compute the pending count, renders
above the editor when the doc has any code links, exposes a
manual-sync button that calls documentApi.generate. Caught while
writing the FE pending-banner spec.
| File | What it covers |
| --- | --- |
| backend/tests/ai/services/test_document_generation_paste.py | generate_from_code returns TipTap doc shape with heading + paragraph + matching identifier (real LLM) |
| backend/tests/ai/services/test_document_generation_repo.py | generate_from_repository forwards to GitHubService correctly; missing file raises ValueError (mocked GH, real LLM) |
| backend/tests/ai/services/test_document_regenerate_from_link.py | The orchestration the {doc_id}/generate endpoint runs: load doc, load links, generate, write content back, flip generation_status, clear has_pending_changes |
| backend/tests/ai/services/test_suggest_improvements.py | Contract shape (quality_score, improvements[], missing_sections[], overall_assessment); locks in the fix for the schema drift above |
| backend/tests/unit/test_document_sync_service.py | Plan-tier routing in get_sync_type_for_developer: REAL_TIME / DAILY_BATCH / MANUAL for premium / pro+enterprise / free; the previously-dead enterprise branch now reaches DAILY_BATCH |
| File | What it covers |
| --- | --- |
| docs-autogenerate-paste.spec.ts | Full live flow: paste TS function, click Generate, real LLM round-trip, lands on new doc with editor visible |
| docs-autogenerate-repo.spec.ts | From Repository tab opens; either repo list or empty state renders; Generate disabled in empty state |
| docs-autogenerate-repo-full.spec.ts | End-to-end repo orchestration with mocked repo/branch/contents APIs; user picks repo → root dir → click Generate → mocked content lands as a new doc |
| docs-pending-changes-banner.spec.ts | SyncStatusPanel renders pending count + manual-sync label when a code-link is dirty (mocked code-links, live doc) |
| (orphan SyncStatusPanel finding informs this) | — |
pytest, pytest-asyncio, pytest-cov, aiosqlite into the aexy-backend image (they weren't there before, blocking any
attempt to run the backend test suite via docker exec).
Docs UI/UX follow-up sweep: the five items the 0.8.23 commit
deliberately left as "out of cluster scope" — visual gradient
heroes, ring-spinner duplication, Drive IA confusion, hardcoded
colour refs, mobile responsiveness on Drive/Files/Knowledge-Graph.
5 new E2E specs lock the changes in (18 total docs E2E specs now,
~32 s full pass).
from-primary-500/20 to-purple-500/20 rounded-2xl icon hero in two places** (DocsLayoutClient.tsx, page.tsx)
with a typography-first treatment: small tracked eyebrow label,
semibold tracking-tight headline, one line of supporting copy.
The audit called this gradient pattern the strongest "AI-slop"
tell in the surface — docs-no-gradient-hero.spec.ts regression-
guards both heroes.
and auto-generate documentation from your code" to an inviting
"What do you want to write today?" with shorter supporting copy.
xs|sm|md|lg size variants, role="status", data-testid="aexy-spinner", and an sr-only
label. Replaces four near-identical inline implementations:
DocsLayoutClient.tsx:81 (lg), [documentId]/page.tsx:45 (md),
CollaborativeEditor.tsx:319 (sm), TemplateSelector.tsx:140 (xs).
inline pattern accumulated four variants of the same idea across
the surface.
SidebarNavigation.tsx pointing at /docs/drive. Drive was previously reachable only by URL.
drive.page.title in messages/en/drive.json + messages/hi/drive.json) with a new
subtitle: "Workspace files, task attachments, and compliance
documents — separate from your written docs." Makes the relationship
to docs explicit.
docs-drive-ia.spec.ts asserts the sidebar Files link is present,click lands on /docs/drive, and the renamed heading + subtitle
render correctly.
destructive/success states replaced with semantic tokens:
text-red-{300,400} → text-destructive, bg-red-50 dark:bg-red-900/20
→ bg-destructive/10, text-emerald-400 (saved indicator) → text-success.
Touched: DocumentItem.tsx (Delete menu item), [documentId]/page.tsx
(error state), CodeLinksDisplay.tsx, CodeLinkPanel.tsx,
CreateSpaceModal.tsx, GenerationPanel.tsx (error+success banners),
DocumentEditor.tsx (Saved indicator). 15 refs collapsed.
CollaborationAwareness.tsx (dead code), VersionHistoryPanel.tsx (diff visualization where red specifically
means "removed"), SyncStatusPanel/GitHubSyncPanel (domain-specific
status palettes), and DocumentItem.tsx's yellow favorite-star.
/docs/drive, /docs/files, and /docs/knowledge-graph. All three render usable content on
mobile after the Cluster 1 fixes (Drive already had lg:flex-row
+ lg:w-56 responsive utilities; KnowledgeGraph paywall is
naturally vertically-flowed; /docs/files redirects to /docs/drive).
docs-mobile-sub-routes.spec.ts locks in the regression: eachroute's primary content is visible at 390 px and the CTAs/headings
don't overflow the viewport.
5 new E2E specs (frontend/e2e/docs-*.spec.ts):
docs-no-gradient-hero — regression guarddocs-drive-ia — sidebar link + renamed heading + subtitledocs-mobile-sub-routes — 3 routes × 390 px content reachabilityTotal docs E2E: 18 specs, ~32 s full pass.
In-app docs UX bug-fix sweep across three clusters (shell, editor,
a11y), TDD against 13 new E2E specs. Captures every fix in a failing-
then-passing test so the regressions can't sneak back. Cmd+K now
actually searches docs, mobile is no longer unusable, the editor
gets a real reading measure plus bullets + a floating BubbleMenu,
and the sidebar exposes tree semantics to assistive tech.
Cmd+K in /docs opens the doc-scoped SearchModal, not the global CommandPalette.** Two keydown listeners on document were
racing — the app-shell global was mounted earlier and won. The docs
layout now installs its listener in capture phase and calls
stopImmediatePropagation(), so the global never sees the event
on docs routes. (DocsLayoutClient.tsx)
w-60 flex-shrink-0 was eating ~62 % of a 390 px viewport. Sidebar
now slides off-screen via -translate-x-full md:translate-x-0,
with a data-testid="docs-mobile-menu-trigger" hamburger in a new
mobile top bar (pl-14 so it doesn't collide with the app-shell's
fixed-position trigger) and a backdrop that closes on tap.
Drawer auto-closes on route change.
NotionSidebar.tsx now opens the existing ConfirmDialog from
components/ui/confirm-dialog.tsx with tone="danger" and a
"Delete" primary action. The native browser dialog (which broke
visual consistency with the dark theme) is gone.
"Manage Space" were console.log("…") TODOs surfaced as live
affordances. NotionSidebar no longer passes the onDuplicate /
onManageSpace props, so DocumentItem's existing
{onDuplicate && (…)} guards collapse the rows. Real handlers
can be wired later without changing markup.
The bare prefix matched the [documentId] catch-all with
documentId="files" and loaded forever. A new
app/(app)/docs/files/page.tsx redirects to /docs/drive.
prose ... max-w-none (which ran ~140cpl on 1440 px viewports) replaced with
prose ... max-w-3xl mx-auto (~672 px / ~65 cpl). Editor
spec asserts ≤ 900 px at 1440 desktop.
(DocumentEditor.tsx:181)
reset was stripping bullets off bare <ul>/<ol> inside the
ProseMirror because typography-plugin prose-ul: modifiers
weren't resolving in the cascade. Switched to arbitrary-variant
utilities (`[&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal
[&_ol]:pl-6 [&_li]:my-1`) which carry enough specificity.
staying open across three intermediate actions. Added a
scoped keydown listener while the picker is mounted; on
Escape it sets showEmojiPicker(false).
autoSave is on by defaultwith a 1 s debounce; the duplicate Save button created
"is autosave actually working?" doubt. Drop the onSave prop
passed to EditorToolbar — the {onSave && (…)} guard already
collapses the row. handleManualSave callback also removed.
BubbleMenu only existed in CollaborativeEditor.tsx, which is
hard-disabled by collaborationEnabled = false. DocumentEditor
now mounts its own BubbleMenu with Bold/Italic/Underline/Code
controls. data-testid="docs-bubble-menu" lives on an inner
wrapper because @tiptap/react@2.27.1 BubbleMenu only forwards
className to the rendered div (verified by reading
node_modules/@tiptap/react/dist/index.cjs).
role="dialog" + aria-modal="true" + aria-label="Search documents" on the
modal root. Screen-reader users can now identify the overlay.
gets role="tree" + aria-label="Documents". Each
DocumentItem row gets role="treeitem" + aria-selected
(driven by isSelected) + aria-expanded when it has children.
Active document is aria-selected="true".
13 new E2E specs under frontend/e2e/docs-*.spec.ts, all live-
backend, no LLM (use backendOnlyReady + setupAiLiveAuth).
Spec-first per cluster: write specs → run them red → implement
fixes → run them green. Files:
docs-cmdk-doc-search, docs-mobile-sidebar (×2), docs-styled-confirm-dialog, docs-todo-menu-items-hidden,
docs-files-route-redirect
docs-editor-reading-measure, docs-editor-list-bullets, docs-editor-emoji-picker-escape, docs-editor-no-save-button,
docs-editor-bubble-menu
docs-a11y-search-modal, docs-a11y-doc-treeFull suite passes in ~22 s.
AI/automation E2E coverage expansion: the workflow builder now has a
schema-driven test fixture, 35 new Playwright specs across nodes,
triggers, actions, templates and end-to-end runs, plus tighter
assertions on the live-LLM tests so the suite actually catches
provider drift and prompt regressions instead of greenlighting them.
WorkflowNodeType in backend/src/aexy/schemas/workflow.py; the
canvas's JoinNode was already wired up but the schema literal
was missing, so nodes: [..., { type: "join" }] round-trips
through validation now instead of being silently coerced.
data-testid="palette-category-${kind}",
palette-subtype-${kind}-${value} on every entry, plus
data-testid="node-config-panel" + role="dialog" on the config
drawer. One helper change updates every spec instead of 200.
A bare row gave no visual hint that clicking does anything;
drag-first UX stays primary, the icon is subtle by design.
backend's validate_workflow rejects email actions without
email_body, so the "follow-up sequence" and "welcome sequence"
templates were silently failing the save with HTTP 400 and the
user saw an empty canvas after "saving" (automationTemplates.ts).
NodeConfigPanel writes action fields flat (data.email_body,
data.duration_value) and the backend reads them flat too;
nesting under data.config meant the validator never saw the
required fields. No remaining node.data.config.* readers
anywhere in the frontend.
trigger/action registry to
frontend/e2e/fixtures/automation-schema.generated.json. The
per-subtype specs (ai-automation-triggers-*,
ai-automation-actions-*) parametrise from this fixture so
adding a new trigger on the backend forces a matching test entry.
docker exec aexy-backend .... `npm run schema:automation:check`
is the CI drift gate. Both now precheck that aexy-backend is
running and exit with a clear message ("Start it with:
docker-compose up -d backend") instead of leaving devs to parse
a raw docker exec error.
1. Per-node CRUD (`ai-automation-node-{trigger,action,
condition,wait,agent,branch,join}.spec.ts`) — palette add,
config-panel render, click-to-select, delete.
2. Per-subtype parametrised loops —
ai-automation-{triggers,actions}-{module}.spec.ts covering
every trigger and action in every module's registry, all
driven by the generated fixture above.
3. End-to-end — canvas-wire (6-node save/reload
round-trip), templates (every gallery template lands a
usable graph), generate-workflow-per-module (LLM generator
across all 10 modules), run-agent and end-to-end
(record-created trigger → seeded LLM agent → workspace
state mutation, with marker-envelope assertions).
— openCanvas, addNodeFromPalette, canvasNodes,
openNodeConfig, connectNodes, saveWorkflow,
fetchWorkflow, deleteAutomation. Roughly 35 specs share one
contract; testid drift breaks one helper, not the whole suite.
ai-automation-run-agent and ai-automation-end-to-end now pass a per-test
echo_token in trigger_data and instruct the agent (via its
system prompt) to wrap it in a literal [ECHO:<token>]
envelope. The envelope shape can't appear from stub providers,
cached responses, or a passthrough copy of input data — only
from an LLM that actually read and reshaped the payload.
generate-workflow-per-module now hard-fails on unknown trigger_type.** A console.warn previously demoted LLM
hallucinations like record.modified (instead of
record.updated) to log noise nobody reads — exactly the
prompt-regression class this spec exists to catch. Now an
unknown trigger fails the test with the known-trigger list in
the failure message.
run-agent workflow status check tightened from ["completed", "running", "failed"] to strictly "completed".**
dry_run=true is synchronous so anything else means the
executor bailed before producing the node_results we go on to
assert against.
splits the LM Studio probe out of aiLiveReady. Structural
tests that don't invoke any LLM (canvas wiring, palette
interaction, save round-trip) no longer skip the entire spec
file when LM Studio happens to be down.
setupAiLiveAuth now sets the aexy_authed=1 cookie beforethe first navigation.** Middleware redirects every protected
route to /?next=... when the cookie is missing — and the
cookie is normally set client-side by useAuth AFTER mount, so
without this fix the very first goto bounced through the login
page and dropped any query params we'd set.
PLAYWRIGHT_BASE_URL instead of hard-coding http://localhost:3000, so the suite can run
against a non-default host (CI runner, remote box).
LMSTUDIO_BASE_URL=${LMSTUDIO_BASE_URL:-http://host.docker.internal:1234/v1}
into the backend. localhost inside the container was the
container, not the host — the agent action couldn't reach the
developer's LM Studio during E2E and dev runs.
AI surface hardening: the /automations canvas no longer crashes on
LLM-generated workflows, the agent provider list catches up with the
backend, the frontend dev container has the headroom to run the new
live AI E2E suite, and a small layout bug in the workflow generator
is fixed before it ships.
POST /automations/generate-workflow response had no position
on its nodes, so ReactFlow crashed the /automations canvas and
bounced the user to the route's error boundary. Backend now
assigns {x, y} to every generated node via a one-shot
auto-layout pass before responding
(backend/src/aexy/services/workflow_generator.py).
fan-in graphs (A→B→C→D plus A→D) now place the merge node at
the depth of the longer path, with descendants cascading correctly.
The earlier BFS variant settled the merge node at the shallower
depth if the short edge was walked first. Five new unit tests in
tests/unit/test_workflow_generator.py pin the contract: every
node gets a position, linear chains cascade right, the diamond
case settles on longest-path depth, existing positions are
preserved, and cycles render rather than crash.
backend has accepted "deepseek" and "lmstudio" as
AgentCreate.llm_provider values for a while; the frontend
selector only knew the four originals, so any agent created with
one of the new providers crashed the agent detail page when
LLMConfigDisplay tried PROVIDERS[provider].models.find(...).
Selector now lists DeepSeek (Chat + Reasoner) and LM Studio
(Qwen 3.5 9B), and LLMConfigDisplay falls back to a generic
render for any future unknown provider rather than throwing.
(frontend/src/components/agents/shared/LLMProviderSelector.tsx)
lazy compilation across /agents, /automations, /chat,
/compliance, … in quick succession was exhausting the default
Node heap and getting SIGKILL'd by Docker. Frontend service now
sets NODE_OPTIONS=--max-old-space-size=6144 (6 GiB V8 heap)
with a matching mem_limit: 7g so Docker doesn't kill the
process before V8 has a chance to GC (docker-compose.yml).
surface (agent chat + conversation create + prompt preview +
test run, /ask, workflow generation, automation test run, code
analysis, developer insights, email draft, file
metadata/sidecar, file search, hiring re-evaluate, learning
path, review-cycle generate) against the live stack — real
frontend, real backend, real LM Studio. Mocked AI responses
defeat the point of this tier; the existing *.spec.ts files
cover UI-only behaviour.
like the backend tests/ai/ suite.
frontend/e2e/fixtures/ai-env.ts (env +LM Studio probe + auth bootstrap) and
frontend/e2e/fixtures/ai-helpers.ts (seeders, long-timeout
response waiters, fatal-error collectors).
AI_E2E_LLM_WAIT_MS).A spec that times out is signalling that the model is genuinely
slow, not flaky — don't lower it. See the new
"AI E2E tests" section in CLAUDE.md for setup.
/reviews surface UX overhaul, prod-bug fixes, and a tighter
contract between the frontend and the manager-review backend. One
hard 422 (manager Save Draft) is fixed via a backend schema relax
+ matching client change; the rest is i18n parity, draft-hydration
correctness, and accessibility nits.
overall_rating: 0 as a sentinel against ManagerReviewSubmission
which is Field(ge=1, le=5) — every draft save before the manager
had settled on a rating was rejected. overall_rating is now
Optional[float] on the submission schema (the hard constraint
stays on FinalReviewData where it actually matters), the service
preserves any prior rating when None is passed, and the client
drops the ?? 0 fallback. Three new regression tests pin the
contract: null accepted, missing accepted, finalize still rejects
out of range (backend/tests/unit/test_reviews_prod_bugs.py).
hydration useEffect on /reviews/manage only wrote
discardedIds when the new workspace key had data; switching to a
workspace with no entry kept the previous team's discard list in
state. Now always resets (manage/page.tsx).
— manager review composer, self-review form, peer decline reason —
used a boolean hydratedRef that stayed true across client-side
nav, so visiting a second review/request id never hydrated its
draft. Now keyed by id (hydratedKeyRef === currentKey), with an
explicit reset when the new id has no stored draft.
advance on /reviews/cycles used to surface failures as a toast
that sat hidden behind the open ConfirmDialog; the detail page
rendered an inline red block inside the dialog. The list page now
uses the same inline block — same place users see the failure
matches the action that produced it.
en and hi (paritypreserved at 550 keys each). Sweep covers: cycles list ConfirmDialog
+ toasts + status filter + breadcrumb + error panel; goal complete
dialog; manage status filter; manage detail "Back to Reviews" +
"Invite Peer Reviewers"; peer-requests error title.
cycle, etc.) in English per the project convention.
/reviews/cycles/[cycleId] now has an explicit aria-label alongside title= — screen readers don't
reliably announce title, and the trigger needed a stable
accessible name.
next-env.d.ts and tsconfig.tsbuildinfo are now gitignored — the former is rewritten by Next between dev (.next/dev/...) and
prod (.next/...) builds, the latter is per-machine.
UX overhaul of the agents + automations surface, plus a four-week
accessibility sweep across the workspace shell. Nineteen commits since
0.8.01 consolidate three workstreams: a unified Operations IA, an
inbox triage rewrite, and a long polish tail that migrates the last raw
modals/drawers off ad-hoc divs onto Radix Dialog / Sheet primitives.
Closes with four follow-ups from the PR #148 review.
/operations, new). Single entry for agents *and* automations — replaces the two separate /agents and
/automations landings, which the audit flagged as the #1 user
confusion ("am I building an agent, or wiring a workflow?"). New
frontend/src/app/(app)/operations/page.tsx (534 lines) plus
sidebar layout updates and messages/{en,hi}/operations.json
translations.
/agents/[id]/inbox). Multi-select withshift-click range, bulk-action toolbar (approve / dismiss / mark
read), and full keyboard navigation (j/k row movement,
x = toggle-select, enter = open). Inbox detail polish adds five
follow-up wins (HTML email rendering via DOMPurify, sender chip,
read-state indicator, optimistic toggles, skeleton during refetch).
/agents/[id]/edit). Replaces the prior single hasChanges boolean — each of the seven
tabs (General / LLM / Tools / Behavior / Prompts / Escalation /
Email) reports its own dirty bit so users switching tabs see which
sections still have pending edits. Help text and the
system-agent-locks-non-LLM-tabs disable are part of the same pass.
frontend/src/hooks/useRouteGuard.ts, new). Anchor-click intercept + beforeunload for unsaved-changes
prompting; companion requestConfirm(href) API for programmatic
navigations (toolbar shortcuts, form-success redirects). Wired into
the edit page; ready for reuse on automation builder and CRM detail
forms.
agent detail page so executions and inbox counts refresh without a
manual reload. Pauses on hidden tabs (default RQ behavior); no extra
socket plumbing.
frontend/src/components/automations/TemplateGallery.tsx and
frontend/src/lib/automationTemplates.ts — the automation /new
page now opens to a curated gallery (standup digest, blocker
escalation, sprint kickoff, etc.) instead of a blank canvas.
<div role="dialog">surfaces (delete-agent confirm, email-disable confirm, multi-select
bulk confirm, automation-version pick) to components/ui/dialog.tsx
(Radix DialogPrimitive — focus trap, escape, restored focus on
close). Drawers (workflow Test Results, Execution History, Version
History) moved to components/ui/sheet.tsx. New
components/ui/confirm-dialog.tsx for the destructive-action pattern.
MessageBubble with safe link handling, aria-live="polite" execution-status region in
workflow nodes, prefers-reduced-motion respected on the chat
thinking-indicator and the workflow canvas pan/zoom transitions.
icon-only button across agents/automations/inbox; focus-visible
outlines added to all interactive surfaces; light-theme contrast
bumps on placeholder text and disabled-state buttons.
read/unread now flip instantly with rollback on error; inbox shows
skeleton rows during the first fetch instead of an empty state.
date helpers; replaced ~20 ad-hoc Intl.DateTimeFormat callsites.
next-intl ICU patterns so the Hindi locale gets correct plural
forms without per-callsite branching.
automations, inbox, insights, operations namespaces (full parity between locales).
tab carries its own dirtyByTab[id] so the tab strip can dot-mark
which sections have unsaved edits. Form-init effect skips re-sync
when the user has local changes (UX-EDT-021) — a refetch from
background polling or another mutation won't clobber in-flight
typing.
inflight tagging into a shared OAuthInflightTagger component;
callback page no longer touches localStorage directly.
AUTH_REQUIRED_PREFIXES matched /docs/ but not bare /docs, leaving the docs root unprotected by the auth
gate. Now matches both, consistent with every other entry in the
list.
_load_template_for_workspace helper. update_member_access and apply_template_to_member had
inlined the identical "template belongs to this workspace (or is a
system template)" check; both now call the helper.
new URL(anchor.href, ...) intry/catch. A page with a malformed anchor href would have thrown
inside the captured click handler.
the react-hooks/exhaustive-deps suppression: hasChanges and
name are read inside the form-init effect but intentionally
excluded from deps to avoid re-syncing the form mid-edit.
/agents/[id]/chat/...). New AgentService.stream_message emits tokens, tool-call markers,
and citations as Server-Sent Events; the frontend useAgentChatStream
hook wires them into the message bubble incrementally with an
optimistic placeholder, mid-stream stop, and a token-cost meter.
Migration migrate_agent_message_streaming.sql adds the supporting
columns on agent_messages (stream state, token deltas, citations).
class gained a stream() co-routine alongside the existing
request/response shape; the service routes streaming-capable agents
through it and falls back to a single-shot completion for the rest.
the cited tool-call output; renders even after the stream completes.
parent_message_id, so the detail pane renders the full back-and-
forth (incoming → agent reply → reply-to-reply, etc.) instead of a
flat list. New test_inbox_thread_chain.py (316 lines) pins the
resolver against forked threads and missing parents.
/new page cannow seed a workflow from a natural-language description. New
services/workflow_generator.py calls the LLM, validates the
produced node graph, and hands it to the existing builder. Wired
into TemplateGallery as a "Describe your workflow" entry.
api/email_webhooks.py (Postmark's MessageStream field was being dropped on rebound
events, breaking attribution for unarchived items).
GET /agents/defaults) returns the systemprompt / tools / behavior defaults for a given agent type so the
wizard and edit page render preview state without hardcoding.
Backed by useAgentDefaults on the frontend.
{{variable}} payload through the system prompt and renders the
result inline so users see what the agent will actually see at
runtime.
agent_drafts table (migrate_agent_drafts.sql), AgentDraftService,
GET/PUT/DELETE /agents/drafts endpoints, and the useAgentDraft
hook. Replaces the localStorage-only draft that vanished on
cross-device switches; drafts auto-restore on wizard re-entry and
garbage-collect on completion.
Sentry when NEXT_PUBLIC_SENTRY_DSN is set, falls back to a
structured console log otherwise. ModuleError.tsx boundary now
reports through it instead of swallowing. 156-line test suite covers
both branches.
Save button (aria-busy during inflight, error-region announcement
on failure), email-cancel resets the form to persisted values
instead of leaving stale local edits, NodeConfigPanel layout fix.
- reportError.test.ts (156 lines) — Sentry / console branches.
- useAgentDraft.test.tsx (326 lines), useAgentChatStream.test.tsx
(430 lines) — hook lifecycle, abort, error paths.
- test_agent_stream_message.py (510 lines) — five SSE flows
including mid-stream cancellation and tool-call interleaving.
- test_agent_draft_service.py (226 lines) — CRUD + workspace-
scope assertions.
- test_workflow_generator.py (233 lines) — graph validation +
LLM error fallback.
- test_agent_cost_estimation.py (125 lines), test_agent_preview_prompt.py
(340 lines), test_inbox_thread_chain.py (316 lines),
test_inbox_unarchive.py (193 lines),
test_email_webhook_parse.py (117 lines).
Post-merge audit of the streaming-chat + agent-runtime branch surfaced
one Critical cross-workspace gap on the new SSE endpoint plus a
cluster of Highs around partial state, citation XSS, and an SSE chunk-
buffering blind spot. Fixed in place; tests added for each.
conversations/{cid}/messages/stream` now calls
_assert_agent_in_workspace and rejects conversations whose
workspace_id doesn't match the URL. Previously the endpoint only
checked conversation.agent_id == agent_id, so a developer in
workspace A who knew a foreign workspace's (agent_id, conversation_id)
pair could stream user messages into that foreign conversation.
in a single transaction so a flush failure can't strand a user
message without a paired execution row. Inbox thread forward walk
now queries only the new frontier per round (was O(n²) on long
threads); capped at 50 rounds matching the backward walk. Workflow
generator caps generated graphs at 100 nodes / 200 edges so a runaway
LLM response can't spawn thousands of canvas nodes.
save_draft now uses attributes.flag_modified(...) to force the JSONB UPDATE (previously
relied on assigning a new dict, which worked but was fragile under
in-place mutation). Documented the pattern on the model field.
back to plain text for non-http(s) schemes, blocking
javascript: / data: URL XSS at the source. Live token meter +
per-message meter + "Sources" + "Processing…" + generate-prompt
placeholder all flow through useTranslations (messages/en/agents.json,
messages/hi/agents.json, messages/{en,hi}/automations.json). Per-
message meter stacks under the timestamp on narrow screens. Optimistic
message ids use crypto.randomUUID() instead of Date.now() so two
sends in the same millisecond can't collide React keys.
useAgentChatStream awaits refetchQueries then clears pending in the same tick (was
invalidateQueries + 80 ms setTimeout, which caused a one-paint
flicker when the refetch resolved fast). useAgentDraft tracks a
save-sequence + mountedRef so a slow in-flight save can't overwrite
newer state and unmount races don't trigger React's "set state on
unmounted component" warning. Inbox thread strip drives selection
through a state callback instead of document.querySelector(...).click().
cases to test_agent_cost_estimation.py (the longest-prefix-wins
sort would silently bill the wrong rate if reversed). Added a
useAgentChatStream test that tears a frame across two stream
chunks (mid-JSON + across \n\n) to lock in the buffer-reassembly
behavior. 77 backend + 82 frontend tests passing.
Post-review hardening of the 0.8.0 workspace-scope authz pass. Four
parallel reviewers audited the branch and flagged five Criticals plus
several Mediums that were missed in the original sweep; this release
closes all of them.
_filter_task_ids_to_workspace ended with a stray return sprint
(undefined name), so bulk_assign_tasks, bulk_update_status, and
bulk_move_tasks raised NameError for every in-workspace call
instead of authorizing them. Removed the dead return.
api/reviews.py — submit/finalize routes missed caller-identity checks**. submit_self_review, submit_manager_review, and
finalize_review accepted any authenticated caller. Added
_require_reviewee (caller must equal review.developer_id) and
_require_review_manager_or_admin (caller must equal
review.manager_id or hold workspace admin); both return 404 to
avoid existence oracles.
api/dependencies.py — story/task dependency mutations had no workspace scope**. update_story_dependency, delete_story_dependency,
resolve_story_dependency and the three task-dependency twins
loaded by id with db.get() and mutated without any tenancy check.
Added _load_story_dependency_authorized and
_load_task_dependency_authorized helpers that resolve the
dependent resource's workspace, assert active membership, and 404
on mismatch. Wired into all six routes.
api/email_webhooks.py — SES SNS Notification path skipped signature verification** (WS-082). Only the TopicArn was checked
against the allowlist; the field is attacker-controlled in the body,
so anyone who knew or guessed an allow-listed ARN could POST forged
Bounce/Complaint events. Added verify_sns_message_signature that
builds the canonical AWS SNS string-to-sign, validates
SigningCertURL against the AWS SNS host pattern (no SSRF), fetches
the cert (cached by URL), and RSA-verifies the message envelope.
Supports SignatureVersion 1 (SHA-1) and 2 (SHA-256).
services/email_webhook_verify.py — no replay window on SendGrid /Mailgun verifiers** (WS-082). A captured signed payload could be
replayed indefinitely. Added a 300s skew check on both providers,
matching the mailagent internal-auth middleware.
services/github_task_sync_service.py — cross-workspace [slug:task-key] auto-link** (WS-083). _find_aexy_task resolved by
workspace slug alone, so a malicious PR body in repo X (owned by
workspace A) containing [victim-workspace:42] could create a
TaskGitHubLink row pointing at workspace B's task. The lookup now
requires the resolved task's workspace to have actively adopted the
mentioning repo (WorkspaceRepository.is_active).
(WS-084). submit_standup, create_work_log, log_time, and
report_blocker accepted task_id/sprint_id/team_id from the
request body without scoping; the row was stamped with the caller's
first team's workspace. Replaced with _resolve_tracking_workspace
which derives the workspace from the supplied refs (in
task → sprint → team priority), rejects bodies that mix refs across
workspaces, and asserts the caller is an active member of the
resolved workspace.
api/developer_insights.py — non-admins received author_email PII** (WS-085). list_developer_commits returned the raw email
field for every active workspace member. Added _is_workspace_admin
helper that gates the field on owner/admin role; non-admins receive
null.
(WS-086). When the shared secret was missing, the middleware silently
passed every request through to handlers. Mailagent now raises
RuntimeError at boot when environment in {production, staging}
and the secret is empty; dev/test continue to pass through with the
existing warning.
The OAuth callback hung onto ?token=… in the address bar until the
next navigation. Now scrubbed via history.replaceState before any
token use, mirroring the /p/[publicSlug] flow.
/p/[publicSlug]/page.tsx — public-slug login didn't sync the presence cookie**. The page wrote token to localStorage but skipped
setAuthPresenceCookie(), reintroducing the redirect-loop class that
5895c1da had fixed for the landing page. Cookie now set inline.
Secure attribute on HTTPS so the flag isn't sent in cleartext if a
proxy ever downgrades the connection.
AnalyticsDetailsModal.tsx — external commit links missing noopener**. rel="noreferrer" only; added noopener for explicit
tabnabbing defense (modern browsers imply it, but the codebase
convention is to set both).
rule that all user-facing strings in new components must use
useTranslations(), the modal's ~30 hardcoded English strings (tab
labels, table headers, loading/empty states, etc.) are now driven
by the new insights.details namespace in messages/en +
messages/hi. The same pass i18n'd three new strings in
insights/page.tsx (Sources / Profile / Show inactive / "still
loading" toast).
the story- and task-dependency loader helpers: active member passes,
cross-workspace caller gets 404, missing id gets 404, removed-status
member is rejected.
tests (attacker cert URL rejected, valid sig accepted, tampered
payload rejected, dev-mode short-circuit) plus replay-window tests
for SendGrid and Mailgun. Refreshed the Mailgun happy-path fixtures
to use current timestamps.
_adopt_repo fixture that wires Repository + WorkspaceRepository for the test
workspace; new test_cross_workspace_slug_injection_is_blocked
exercising the WS-083 fix, plus test_shared_adoption_still_links
pinning that shared-repo adoption still resolves correctly to the
workspace whose slug was used.
Code review cleanup of work that originated on the long-running
agent-upgrade branch (compliance/tracking/automation/assessment
modules). Three reviewers audited the code as it currently sits on
main; this release fixes the verified Critical and High findings.
GET /channels, POST /channels, PATCH /channels/{config_id}, DELETE /channels/{config_id}
now verify the caller is a member of the target workspace (viewer for
read, member for write). Without it, an authenticated user in
workspace A could enumerate, create, edit, or delete channel configs
in workspace B.
GET /standups/team/{team_id} now fetches the team and asserts
workspace membership; GET /standups/summary/{sprint_id} does the
same via the sprint's team. Previously any authed user could read any
team or sprint's standup aggregate by guessing IDs.
GET /logs/task/{task_id} and GET /time/task/{task_id} now fetch the task and verify the
caller is a member of the task's workspace before returning logs or
time entries.
PATCH /blockers/{id}/resolve and PATCH /blockers/{id}/escalate now require workspace
membership (member role) before allowing state transitions.
Previously any authed user could resolve or escalate any blocker by
guessing its UUID.
team_id, the endpoint was returning blockers across all
workspaces. It now scopes the query to workspaces the caller is a
member of (WorkspaceService.list_user_workspaces); if team_id
is supplied, it verifies workspace membership for that team first.
api/assessments.py — workspace-scope authz across all authedendpoints**. Added two helpers:
- _assert_workspace_access(db, organization_id, developer_id, role)
for endpoints that take an organization_id directly
(POST /, GET /, GET /organization/{id}/metrics).
- _assert_assessment_access(db, assessment_id, developer_id, role)
that fetches the assessment and asserts workspace membership,
returning the loaded Assessment.
Applied to: create_assessment, list_assessments, get_assessment,
update_assessment, delete_assessment, clone_assessment,
get_wizard_status, all five step/N endpoints, list_topics,
suggest_topics, list_questions, create_question, update_question,
delete_question, generate_questions, list_candidates,
add_candidate, import_candidates, remove_candidate,
resend_candidate_invite, get_email_template, update_email_template,
pre_publish_check, publish_assessment, get_assessment_metrics,
get_organization_metrics, reevaluate_candidate,
get_candidate_details. Public-token endpoints
(/public/{public_token}/*) are out of scope (intentionally
unauthenticated). Previously any authed developer could read or mutate
assessments in any organization by guessing UUIDs.
(backend/src/aexy/api/tracking.py). The per-member developer fetch
loop was issuing one SELECT Developer WHERE id = ? per team member;
it now batch-loads all developers in a single IN query and indexes
by id.
timeout**. temporal/dispatch.py ACTIVITY_CONFIG now declares:
check_missed_standups, check_time_entry_thresholds,
check_stale_blockers, detect_blocker_patterns,
check_time_anomalies, check_standup_participation,
check_approaching_due_assignments, check_overdue_assignments,
check_expiring_certifications, check_expired_certifications,
check_bulk_compliance_rates — each with STANDARD_RETRY and a
10-minute timeout to accommodate scheduled detection activities that
loop over active workspaces.
backend/src/aexy/api/tracking.py: from typing import Any and
from aexy.services.automation_service import dispatch_automation_event
(dispatch is routed through services/tracking_events.py helpers).
WorkspaceService is now imported at module scope.
standup.streak and training.bulk_overdue — need product/design input on thresholds
before implementing.
NodePalette.tsx and the reminder/trackingpages — separate, larger effort that needs translator coordination.
tracking_events.py, tracking_compliance_config.py, compliance_service.py,
hiring_intelligence.py, assessment_service.py.
Replace manual GitHub issue/PR linking with mention-based auto-linking
via [workspace-slug:task-key] in PR or issue title/body.
api/webhooks.py routes issues events (opened/reopened/edited/closed) through
GitHubTaskSyncService.process_issue, which parses the issue title +
body for [slug:key] mentions and upserts a TaskGitHubLink row per
match with is_auto_linked=True. Works from any repo — the slug
resolves against Workspace.slug, the number against the
workspace-wide task_key.
pull_request.edited/synchronize and issues.edited, auto-links whose mention is no longer present in the
fresh body are deleted. Manual edits to the GitHub source are now the
way to add or remove links.
(task_id, repo, number), its cached github_issue_title/state/url
refresh when fresher values arrive (issue renamed on GitHub →
link metadata updates).
[slug:task_key]inline help so users know what to paste into a PR/issue body.
api/sprint_tasks.py and api/project_tasks.py:
POST /github-links/pull-requests and POST /github-links/issues.
GET /github/pull-requests, GET /github/issues,
GET /{task_id}/github-links/issue-repositories (both scopes).
board/page.tsx — the PR + issue search dropdowns, the manual owner/repo#123 entry, and ~300 lines
of supporting state/queries/mutations.
linkPullRequest, linkGitHubIssue, searchPullRequests, searchGitHubIssues, and
getGitHubIssueRepositoryContext from lib/api.ts (sprint and team
scopes). getTaskGitHubLinks and unlinkGitHubLink retained.
tests/unit/test_github_issue_auto_link.py — process_issue createsone auto-linked row per mention, case-insensitive slug match,
hyphens in slug, edit-then-remove drops the stale row, edit refreshes
cached title/state, closed/reopened refresh state without
pruning (only edited is allowed to remove mentions).
Fix duplicate developer rows in team insights, plus auto-hide
zero-contribution members.
compute_team_distribution now takes a member_ids list distinct from the activity-expanded developer_ids, so
_build_developer_alias_map can actually map ghost ids onto their
canonical workspace-member rows. The prior code passed the same
list as both args, which made the NOT IN filter exclude the
ghosts we wanted to bridge — producing two rows for "Ritesh
Biswas" (active vs ghost-with-personal-email) on the team insights
endpoint.
GitHubConnection:
1. Pull Commit.author_github_login (most-frequent value per
developer) and use it as the github login key.
2. Parse <id>+<login>@users.noreply.github.com out of the
developer's email. Together these collapse the two Mobashir
ghost rows that shared the same GitHub login but were never
linked to a Connection row.
_rollup_by_identity never sees a ghost+canonical pair — fewer
reliances on the identity_key tie-breaker.
compute_team_distribution(..., hide_zero_contribution=False)optionally filters out members whose four counters (commits, PRs
merged, lines changed, reviews given) are all zero in the window.
GET /workspaces/{id}/insights/team?include_inactive=false (default) — applies the filter. ?include_inactive=true restores
the full roster.
(insights/page.tsx) wired through useTeamInsights and the
generated getTeamInsights client.
author-github-login collapse, and zero-contribution filter.
GitHubConnection norany name/email overlap with their ghost rows cannot be linked
automatically. The three "Mobashir" rows in the original example
collapse from 3 → 2 (two ghosts merge), but the active member
mobashir.r@bimaplan.co stays separate until either an admin
links their GitHub login, or a manual "merge identities" action
is added.
Post-review hardening for the 0.7.82-0.7.88 workspace-scope leak audit.
The fixes were correct but a code review surfaced residual fail-open
edges and missing test coverage; this release closes those.
(services/email_webhook_verify.py). A new
webhooks_require_signing setting (default True) replaces the
prior behavior where each provider returned True when its env var
was missing. SES, SendGrid, Mailgun, and Postmark all reject events
outright when the required key isn't configured. Local development
can flip the flag off to fall back to the old accept-with-warning
behavior; production must keep the default.
mailagent/middleware.py:44). _is_public_path previously OR'd in path.startswith(p) (no
trailing slash), so /healthcheck-evil could skip HMAC auth on the
way to a route named with a public-prefix prefix. Tightened to
exact-match OR startswith(p + "/").
(frontend/src/lib/oauth.ts). The 0.7.85 implementation only
listened on mousedown, breaking OAuth login for keyboard users
(Tab + Enter on a focused login link) and any JS-driven navigation
(window.location.assign("/auth/github/login")). Now also installs
a capture-phase keydown listener and patches
window.location.{assign,replace} + the href setter so the
inflight marker is set on every navigation vector.
(api/booking/public.py). The 0.7.86 fix only guarded the workspace
lookup endpoint; the teams/team-by-id/event-type/slots endpoints
inherit the same throttle now via router-level Depends.
frontend/next.config.js). Negative-lookahead now anchored to embed/ so /embedded-* paths
still receive X-Frame-Options: DENY and
frame-ancestors 'none' instead of falling through both rules.
core/workspace_auth.py — centralizes the assert_active_member(db, workspace_id, developer_id) and
assert_resource_in_workspace(db, model, id, workspace_id)
helpers used across the 0.7.82-0.7.88 fixes. Call sites in
app_access.py and manager_learning.py switched to the helpers;
remaining inline copies will migrate opportunistically.
- backend/tests/unit/test_email_webhook_verify.py — pins the
fail-closed default for all four providers and the SubscribeURL
SSRF guard.
- backend/tests/unit/test_workspace_auth.py — pins membership
checks (active vs pending/suspended/removed) and the
resource-in-workspace mismatch case.
- mailagent/tests/test_internal_auth_middleware.py — pins the
public-path matcher against prefix-bypass paths and the HMAC
sign/verify wire-format round-trip between backend and mailagent.
- frontend/src/test/oauth.test.ts — pins safeInternalPath
against open-redirect inputs and round-trips
stashPostLoginRedirect.
/?next=... is now consumed. frontend/src/app/page.tsx stashes the (validated) next path in
sessionStorage for the OAuth flow, and useSetToken honours it
after onboarding completes. Open-redirect protection enforced by
safeInternalPath.
Closes the last 9 suspect rows in the workspace-scope leak tracker.
Five close as fixed with concrete patches; four close as verified-ok
or covered by prior fixes. Tracker is now zero open across every
severity.
update_member_access and apply_template_to_member (api/app_access.py) now verify the
target developer_id is an active WorkspaceMember of the route's
workspace, and that the applied_template_id belongs to that
workspace (or is a system template with workspace_id NULL).
create_learning_goal (api/manager_learning.py) verifies data.developer_id is an
active WorkspaceMember of current_workspace_id before stamping
a goal. Approval/budget routes follow the existing-goal chain so
they inherit the same scope.
ReportBuilderService.list_reports no longer surfaces is_public=True reports cross-tenant in the
default listing. Public reports now require an explicit matching
organization_id filter to appear. The reports route doesn't
pass organization_id today, so the default listing returns the
caller's own reports only.
get_developer_team (api/tracking.py) now accepts an optional workspace_id and
constrains the team join via Team.workspace_id. Existing call
sites keep historical "first team found" semantics; workspace-
prefixed routes can opt in.
(sprint analytics), WS-018 (public renderers), WS-019 (learning
services) closed as verified-ok or covered by prior fixes
(WS-009, WS-039, WS-041, WS-051, WS-055, WS-060, WS-061, WS-066,
WS-067, WS-068, WS-074). Each row now records the evidence used to
close it.
Closes the seven Medium/Low confirmed rows in the workspace-scope
leak tracker (WS-013, WS-065, WS-069, WS-070, WS-075, WS-082, WS-083).
LeaveRequestService._find_approver now joins Team and constrains
Team.workspace_id == workspace_id, so a developer's team lead in
another workspace can no longer become the approver on this
workspace's leave requests.
_check_roadmap_rate_limit(Redis sliding window: 10 creates / 50 votes per developer per
hour) on public_projects.create_roadmap_request and
vote_roadmap_request. Caps the spam vector while keeping the
public roadmap open to any authenticated developer.
/u/{token} now serves aconfirmation page on GET and only mutates subscriber state on POST.
Email prefetchers and link-checkers no longer trigger unsubscribes
while mail clients implementing RFC 8058's List-Unsubscribe-Post
still work.
_record_click_event resolves the ?r=<recipient_id> query parameter and drops the attribution
if recipient.campaign_id != link.campaign_id. The click is still
recorded at the link level; only the forged per-recipient
attribution is rejected.
_enforce_webhook_rate_limit (Redis sliding window) applied to /webhooks/github (600 per IP
per minute) and /webhooks/automations/{id}/trigger (60 per
automation per minute). Caps Temporal workflow / LLM token spam.
/webhooks/automations/{id}/trigger now records source_ip via
the shared get_client_ip helper instead of request.client.host,
so the captured IP honours X-Forwarded-For behind a load
balancer.
queryClient.clear() before the isResolved && !isAuthenticated redirect fires, eliminating
the brief window during a cross-tab logout where ghost-cached
React Query workspace data could be visible. The workspace-scoped
providers (ChatWebSocketProvider, WorkspaceSearchPalette,
FloatingChatWidget) were already gated on
isResolved && isAuthenticated.
Closes the remaining High rows in the workspace-scope leak tracker
(WS-060, WS-061, WS-067, WS-068) plus seven related Medium/Low rows on
the public/embed surface. Tracker now has zero open Critical or High
items.
booking/public.py) — get_workspace_teams, get_team_info, and the booking confirmation
response no longer leak member emails. Only id/name/avatar_url
is exposed. A new Redis-backed per-IP rate limit (30/min) gates
GET /public/book/{workspace_slug} to make slug enumeration costly.
Closes WS-060, WS-064.
public_projects.py) — added _project_team_ids helper. Backlog, board, stories, goals, roadmap,
sprints, and timeline endpoints now intersect with ProjectTeam /
GoalProject so a public project never leaks data from the other
projects in the same workspace. _fetch_sprints_with_stats accepts
a team_ids parameter; all callers now pass it. No schema migration
required. Closes WS-061.
booking/calendars.py) — start_oauth signs settings.frontend_url into state instead of the request Origin
header. Callback always redirects to settings.frontend_url,
ignoring any legacy signed value. Open-redirect via OAuth state is
closed. Closes WS-063.
booking/webhooks.py) — added _require_workspace_admin helper applied to every route
(list/create/get/secret/update/delete/test). An authenticated user
from workspace A can no longer read/modify webhooks (or their HMAC
secrets) for workspace B. Closes WS-062.
public_tables.py, models/crm.py) — added TableShareLink.allowed_origins column
(migration `backend/scripts/migrate_table_share_link_allowed_origins.
sql) plus _origin_matches helper. Every /public/tables/{token}*`
route now rejects requests whose Origin header isn't in the link's
allowlist (NULL/empty preserves legacy behaviour). Closes WS-066,
WS-074.
assessment_take.py) — get_assessment_by_public_token_or_id no longer accepts the
assessment UUID as a fallback for the public token; only
public_token matches. Candidate creation in start_assessment
goes through a Redis sliding-window rate limit
(_check_candidate_create_rate_limit): 5 candidates per IP per hour
and 50 per assessment per hour. Email-verification flow remains
backlog. Closes WS-067 fully and WS-068 partial.
booking/booking_service.py) — respond_to_rsvp is nowsingle-shot: refuses to process an attendee that already has
responded_at set, and rotates response_token after the first
use. A leaked email link can no longer be replayed to flip the
response later. Closes WS-076.
Closes the remaining four Critical and most of the High rows in the
workspace-scope leak tracker: frontend OAuth + framing hardening,
mailagent isolation, automation webhook signing, and per-provider email
webhook signature verification.
{id}/trigger now requires X-Aexy-Signature: sha256=<hex>` over the
raw body, verified with a per-automation HMAC secret derived as
HMAC(settings.secret_key, "automation:" + automation_id). Lets us
ship signature verification without a webhook_secret column
migration on CRMAutomation; the UI surfaces this derived value as
the automation's webhook secret. record_id is now constrained to
CRMRecord.workspace_id == automation.workspace_id before loading.
services/email_webhook_verify.py implements:
- SendGrid: ECDSA over timestamp + body against the configured
public key (X-Twilio-Email-Event-Webhook-Signature).
- Mailgun: HMAC over timestamp + token with the signing key.
- Postmark: HTTP Basic Auth against the configured user:pass.
- SES (via SNS): topic-ARN allowlist plus a hostname check on the
SNS SubscribeURL that restricts auto-confirmation to
sns.<region>.amazonaws.com (fixes the prior blind-SSRF).
Each provider handler now resolves the workspace from the
signature-verified sender via SendingDomain.domain lookup first,
and only falls back to the legacy message_id lookup when no
matching sending domain exists. New settings:
sendgrid_webhook_public_key, mailgun_webhook_signing_key,
postmark_webhook_basic_auth, ses_sns_topic_arn_allowlist.
mailagent/middleware.py InternalAuthMiddleware requires
`X-Mailagent-Signature: HMAC-SHA256(internal_secret, timestamp + "." +
body)` on every non-public route with a ±5min replay window. The
Aexy backend's mailagent_client._request signs every outbound call
when settings.mailagent_signing_secret is configured. CORS now
only mounts when cors_allowed_origins is set (default empty —
server-to-server only), and allow_credentials is False. `/send/
email validates from_address.domain` against the verified
mailagent_domains catalog and strips arbitrary headers down to a
whitelist of threading/unsubscribe ones. Per-workspace
EmailProvider isolation (full WS-079) is parked as a backlog
item — the unauthenticated-access vector is now closed.
/auth/callback now calls consumeOAuthInflight() and rejects the URL token (redirects to
/?error=oauth_state_missing) when the marker isn't present. A new
document-level OAuthInflightTagger (mounted in providers.tsx)
watches mousedown events for any <a href> matching
/auth/<provider>/(login|connect|connect-crm) and sets the marker
just before navigation. Catches the inline anchor login buttons in
app/page.tsx and LandingHeader.tsx without modifying every
callsite. The matching /p/[publicSlug] handler (WS-071) is
refactored to use the same shared lib/oauth.ts helper.
middleware.ts now redirects auth-required path prefixes to /?next=<path> when the
aexy_authed presence cookie is absent. The cookie is mirrored from
localStorage["token"] by useAuth on mount and at
setToken/logout. The JWT itself remains in localStorage and is
still validated by the API; the cookie just prevents the SSR app
shell from leaking placeholders to logged-out users.
next.config.js now configures headers(): X-Frame-Options: DENY + CSP
frame-ancestors 'none' everywhere except /embed/* (which gets
frame-ancestors * until per-link origin allowlisting moves to the
API side under WS-074). Also adds `Referrer-Policy:
strict-origin-when-cross-origin and X-Content-Type-Options:
nosniff` site-wide.
Closes 24 High and Medium ID-forgery rows in the workspace-scope leak
tracker (WS-010..014, WS-027..041, WS-044..047, WS-050..052, WS-054).
Each fix follows the same shape: load the referenced resource by id and
assert its workspace_id matches the route's workspace before delegating
to the service.
crm.py) — note CRUD and per-record activity list now verify CRMRecord.workspace_id == workspace_id
before exposing sub-resources. Stops `POST /workspaces/A/crm/records/
<B_record_id>/notes`. Closes WS-027, WS-028.
tables.py, forms.py) — list_fields now 404s on cross-workspace tables; delete_field and
reorder_fields verify form-in-workspace and field-in-form before
mutating. Closes WS-029, WS-030.
agents.py, agent_policies.py, automation_agents.py) — added _assert_agent_in_workspace helper
applied to all inbox actions (get/reply/escalate/archive/process),
routing-rule delete, agent-policy create, and automation-agent
trigger config. Routing-rule delete additionally verifies the rule
belongs to the agent. Closes WS-031..034.
cross-resource link operation now verifies the target shares the
workspace: link_project, link_epic, add_tasks_to_epic,
add_tasks_to_story, add_sprint_to_release,
add_stories_to_release, and sprint-task bulk_assign/status/move.
Sprint-task bulk_move also requires the target sprint to share
the workspace. The get_sprint_and_check_permission helper now
returns the sprint object so call-sites can scope queries to it.
Closes WS-035..039.
oncall.py) — verify_workspace_access now accepts team_id and asserts Team.workspace_id == workspace_id. All call
sites updated. Closes WS-040.
sprints.py) — list_sprints and get_active_sprint verify Team.workspace_id == workspace_id.
Closes WS-041.
team_calendar.py) — three GET endpoints now require workspace viewer-role membership and (when team_id is
supplied) verify the team's workspace. Closes WS-010.
tracking.py) — get_team_tracking_dashboard now resolves the team's workspace and
requires caller viewer-role before reading standups/blockers/time
logs. Closes WS-011.
dependencies.py) — added _require_member_ofhelper. Caller must be a member of the dependent story/task's
workspace before creating or listing dependencies. Also fixed a
pre-existing session.add(...) NameError on both `create_story_
dependency and create_task_dependency`. Closes WS-012.
chat_service.py, chat.py) — update_message and delete_message now accept workspace_id and constrain the lookup
via a ChatChannel.workspace_id join. A sender who is a member of
multiple workspaces can no longer edit a message in workspace B by
hitting workspace A's route. Closes WS-014.
leave.py) — added generic _assert_resource_in_workspace helper. Applied to update/delete of
LeaveType (admin-only), LeavePolicy (admin-only), Holiday
(admin-only), and leave-request approve/reject/cancel/withdraw.
get_developer_balance requires admin and verifies target is a
workspace member; get_team_balances verifies Team.workspace_id.
Closes WS-044..047.
google_integration.py) — link_email_to_record now verifies the CRM record belongs to the
caller's workspace before inserting the link. Closes WS-050.
entity_activity.py) — added _entity_model mapping plus _assert_entity_in_workspace helper
applied to both create_activity and add_comment. Validates the
10 most common workspace-scoped entity types (task/story/epic/
release/goal/crm_record/project/sprint/form/leave_request);
remaining types continue to be stamped pending follow-up. Closes
WS-051 (partial — see helper note).
reminders.py) — control-owner update/delete and domain-team-mapping delete now verify the target's workspace_id
matches the route. Closes WS-052.
planning_poker.py) — get_poker_session_state and the WebSocket entrypoint now resolve
the sprint and require viewer-role membership of
sprint.workspace_id. WebSocket rejects with 4003/4004 on miss.
Closes WS-054.
Continues the workspace-scope leak audit by closing four more Critical
rows from the tracker: three legacy unauthenticated APIs and the GitHub
webhook fail-open.
/analytics/*) — every endpoint now binds current_user_id (was discarded as _) and runs each request's
developer_ids (or path developer_id) through a
_require_developers_visible check that requires every target to
share an active workspace with the caller. Rejects (403) the whole
request rather than silently dropping invisible developers. Closes
WS-007.
/hiring/* section 1) — added get_current_developer to every route in the unauth section
(team-gaps, bus-factor, roadmap-skills, requirements list/create/get
/jd/rubric/scorecard/status). Helpers _resolve_team_workspace_or_403,
_require_developers_visible, _require_requirement_workspace_member
enforce workspace membership for the supplied team_id /
organization_id / requirement_id. JD generation, rubric
generation, requirement create/status update now require workspace
admin role. Closes WS-008.
/learning/*) — all 16 endpoints requireauthentication. Personal endpoints (list paths, generate path,
stretch tasks) require the caller to be the target developer or
hold admin role in a workspace the developer is a member of.
Path-scoped endpoints (get/regenerate/progress/milestones/activities
/recommended courses) use _require_path_access to resolve owner
via the path itself. Pause/resume/abandon are owner-only.
Team-scoped overview and recommendations require active membership
in the team's workspace. Closes WS-009.
/webhooks/github) — fail-closed when a webhook secret is configured: the X-Hub-Signature-256 header is now
mandatory (401 if missing) and verified. When no secret is
configured the route returns 503 unless settings.debug is True;
prevents an empty/typoed env-var from turning ingestion into an
open endpoint. Closes WS-059.
This release closes nine Critical authentication-bypass issues uncovered
by a platform-wide workspace-scope leak audit. A third pass added 28 new
tracker rows (WS-056..WS-083) covering the frontend, public/embed
surfaces, mailagent, and webhook ingress, with one same-day fix applied
to a frontend session-hijack vector.
/notifications/*) now binds the developer identity to the JWT via Depends(get_current_developer_id) on every
one of its 19 endpoints. The previous developer_id: str = Query(...)
parameter (used as authentication by every list/preference/push/admin
route) is removed. Closes WS-042.
/slack/*) — every admin-surface route nowrequires authentication and verifies the caller is an active
owner/admin of the integration's workspace via a shared
require_integration_admin helper. OAuth /install and /connect
derive the installer id from the current user, not a query
parameter. The signed webhook routes (/commands, /events,
/interactions) and the OAuth /callback remain public as
intended. Closes WS-043.
/reviews/*) — the entire surface (~28 endpointscovering cycles, individual reviews, work goals, peer requests,
contribution summaries) now requires Depends(get_current_developer)
and enforces resource-appropriate authorization: cycle CRUD requires
workspace admin; individual-review reads require reviewee / manager
/ peer-reviewer / workspace-admin; goal edits require ownership; peer
request actions require the actual party. Closes WS-021 through
WS-026.
/predictions/*) now binds current_user_id (was discarded as _) and requires the caller to share an
active workspace with the target developer at admin role for
attrition / burnout / trajectory / insights endpoints. Team-health
POST verifies admin permission in the supplied team_id's
workspace, or falls back to per-developer visibility. Closes WS-048.
/p/[publicSlug]) no longer silently writes a URL ?token= query parameter into localStorage["token"].
Token consumption now requires a one-shot oauthInflight
sessionStorage marker set by the page's own OAuth login button
immediately before navigating to the provider. Without that marker
the token is stripped from the URL and discarded. Closes WS-071; the
residual /auth/callback variant is tracked as WS-071b.
docs/workspace-scope-leak-tracker.md with 28 new findings(WS-056..WS-083) covering: cross-workspace CRMRecord pumping through
the unauthenticated automation webhook (WS-056), every email
provider webhook lacking signature verification (WS-057), an SSRF
in the SES SubscribeURL auto-confirm flow (WS-058), GitHub
webhook fail-open when no secret configured (WS-059), public
project endpoints returning entire workspace's data rather than
project-scoped data (WS-061), assessment public-token bypass
(WS-067), Candidate fan-out without verification (WS-068),
mailagent's zero-auth admin surface (WS-077), and cross-tenant
event injection through message_id lookup (WS-081). Each existing
fixed row was relabelled with file:line evidence pointing at the
patch.
This release hardens analytics authorization, scopes repository insights
strictly to adopted workspace repos, and adds an evidence drill-down on
the team insights page.
AnalyticsDetailsModal on the team insights page withSummary / Sources / Commits tabs surfacing the rows behind each
aggregate. A workspace-admin-only Raw tab exposes the underlying
JSON for debugging.
commits_synced, prs_synced, reviews_synced to theworkspace repository response, overlayed from the adopter's
DeveloperRepository row so the catalog and analytics agree on sync
state during the sync-pipeline migration.
against the workspace's adopted-repo allow-list, so a member's
personal or open-source contributions no longer leak into team-level
insights.
Removed and suspended members keep their historical attribution but
cannot keep calling analytics endpoints.
by WorkspaceRepository.workspace_id, making the cross-workspace
guarantee a query invariant instead of relying on data invariants.
/intelligence/team/{workspace_id}endpoints (burnout, expertise, collaboration, collaboration graph,
complexity, technology) that previously returned data when the caller
was not a workspace member.
author emails are not exposed to non-admin viewers.
membership. A teammate marked as "left" keeps their historical
attribution but can no longer read workspace notification settings,
AI code insights, role-gated resources via is_owner, billing
fallback workspaces, or per-app permission paths. Affects
notifications.py, code_insights.py, workspace_service.is_owner,
billing.py workspace selection, and app_access_service member
lookup (which protects four downstream config callsites).
NameError in the project PR search endpoint where the teamvariable was bound in the wrong function.
This release improves developer identity handling in insights and adds
soft member offboarding for workspaces.
contributors into canonical workspace members after safe dry-run review.
left and restore them later without deleting membership history.
email, GitHub login, avatar, identity key, and membership status.
compute per-member averages from the rolled-up contributor set.
search across identity fields, and hides past or external contributors
behind explicit toggles.
below active teammates.
This release improves the employee-facing review experience and reuses
the peer-reviewer invitation flow across manager and self-nomination
surfaces.
/reviews/my-reviews/[reviewId] so employees can open their ownreview, submit self-review notes, nominate peer reviewers when allowed,
track peer-review request status, and acknowledge completed manager
reviews.
InvitePeerReviewersModal that supports both managerassignment and employee self-nomination modes while preventing duplicate
active reviewer invites.
review cycle detail page when the current user is enrolled in the
active cycle.
review component.
actionable review surface instead of the admin-oriented cycle view.
This release resolves frontend TypeScript drift across app surfaces and
centralizes repeated marketing-page icon tuple types.
cards so AI Company OS, AI Agents, CRM, and GTM Intelligence pages can
reuse one typed tuple shape.
workspaces, plans, reviews, OKRs, campaigns, tables, agents, GTM,
planning poker, chat, and analytics payloads.
signatures, cloneElement icon typing, and fixture annotations so
TypeScript can validate without local casts.
aligned sprint backlog deletion with the existing archive task action.
onboarding, sprint, GTM, insights, e2e fixtures, and marketing pages.
This release improves performance review workflows with peer-review
detail pages, manager assignment tools, phase controls, and automated
deadline reminders.
decline, and submit focused feedback from a notification link.
with templates and delivery helpers.
reminders, plus a migration to track sent reminders per cycle.
actions with refreshed table/menu behavior.
cycle opens.
This release makes AI token usage visible and billable at the workspace
level, and adds raw commit detail behind developer insights.
overage cost tracking, and an idempotent migration for the new workspace
usage columns.
GET /workspaces/{workspace_id}/llm-usage so any workspacemember can inspect current AI token consumption and reset timing.
show the underlying synced commits behind aggregate metrics.
every workspace that has adopted the analyzed repository.
to a workspace, while preserving legacy developer counters as fallback.
This release tightens the AI insights experience after the initial
code-insights rollout, with better contributor-claim flows, more resilient
LLM execution, and clearer loading states.
reclaim orphaned GitHub commit, PR, and review activity without leaving
the context where missing activity is visible.
health panels stable while AI snapshots load.
commits, PRs, and reviews.
attribution, not only email-null contributor rows.
rate-limit waits so Temporal activities are less likely to burn retries
during LLM concurrency spikes.
connection failures fast.
states and contributor-claim entry points.
AI code insights now run across GitHub commits, pull requests, reviews,
and sprint task links, with workspace controls for enabling analysis and
new UI surfaces for reading the results.
similar-PR, reviewer-suggestion, task-PR alignment, and snapshot
retrieval workflows.
developer digests, repository health summaries, active PR refreshes,
task-to-PR alignment, and performance-review summaries.
embeddings, AI settings, and migration scripts for the new storage
columns and snapshot tables.
messages, and cards/panels for AI summaries in developer, repository,
review, sprint board, and settings pages.
organization/settings area.
supports branch-aware commit collection, and fans out AI analysis after
repository sync.
activity into the authenticated GitHub developer profile.
normal commits.
Tasks now have a copyable per-workspace identifier and a short
shareable link. Format is [{workspace_slug}:{task_key}] (e.g.
[aexy:42]); the bracketed form doubles as an auto-link token in
GitHub PR/issue titles. The kanban task card surfaces two icon-only
copy actions on hover — full link / full identifier shown on hover,
copied on click.
A new monotonic per-workspace counter assigns a task_key to every
new task. Combined with workspace.slug it forms the displayed
identifier [slug:N], rendered as a subtle monospace prefix on the
kanban card title and used as the body of two new copy actions in
the card's hover quick-actions bar. Existing tasks are backfilled
in created_at order per workspace.
sprint_tasks.task_key (int, unique per workspace) and workspaces.next_task_key (counter). Migration
migrate_task_keys.sql adds them, backfills existing tasks, and
seeds each workspace counter to MAX(task_key) + 1.
before_insert event on SprintTask — one UPDATE ... RETURNING consumes the next key
and serializes concurrent inserts. Covers all task-creation paths
(manual, GitHub import, Jira, Linear, workflows, templates,
planning poker) without touching their call sites.
SprintTaskResponse exposes task_key, workspace_slug, identifier, and public_url so the frontend can render and
copy without recomposing the string.
A short URL at /t/{workspace_slug}/{task_key} resolves to the
sprint kanban for the task, with the task drawer auto-opened.
GET /api/v1/tasks/by-key/{slug}/{key}returns the task UUID plus the sprint and project IDs needed to
build the redirect. Auth-gated on workspace membership.
frontend/src/app/(app)/t/[workspaceSlug]/[taskKey]/page.tsx calls the resolver and router.replaces to
/sprints/{project_id}/{sprint_id}?task={uuid} (or the project
backlog when the task has no sprint).
?task=<uuid> on mount, opens thetask drawer for that task, and strips the param so refresh
doesn't re-open it.
The task reference parser learns a new pattern for the native
[workspace-slug:N] form. When a PR or issue is ingested with that
bracket in its title, GitHubTaskSyncService resolves the matching
task by (workspace.slug, task_key) and creates a TaskGitHubLink
with is_auto_linked=True.
AEXY_BRACKETED_PATTERN regex \[([a-z0-9][a-z0-9-]*):(\d+)\] in task_reference_parser.py,
exposed as TaskReferenceSource.AEXY. Distinct from the existing
[PROJ-123] Jira/Linear pattern (the colon separator avoids the
collision).
(/webhooks/github) for both PRs and commits — no behavior change
for past PRs that didn't use this format, future ones link
automatically.
TaskCardPremium's hover quick-actions bar: Link2 copies the public URL, Hash copies the identifier.
Full string in the title= tooltip; Sonner toast on click.
[slug:N] prefix on the card title so theidentifier is visible at a glance without hovering.
Project-level (sprint-less) tasks reach feature parity with sprint
tasks. Backlog tasks can now carry attachments, attach GitHub PRs and
issues, accept comments, and surface a full activity history; several
silently-dropped fields on create/update across both routes are
plugged; the History tab now logs every meaningful task mutation
including archives, sprint moves, and planning-poker estimates; and
repository connection moves from per-developer to workspace-scoped.
Repositories are connected at the workspace level now, with
projects picking subsets. New tables workspace_repositories (the
workspace's adopted catalog) and team_repositories (the project's
selection) replace DeveloperRepository.is_enabled as the source of
truth for "which repos are tracked here." Migration
migrate_workspace_team_repositories.sql backfills both from
existing per-developer enables so nothing in scope today disappears.
GET/POST/DELETE /workspaces/{id}/repositories (admin), GET/POST/DELETE /teams/{id}/repositories, plus
POST /workspaces/{id}/repositories/{wr_id}/reclaim for the
former-member adoption flow.
WorkspaceRepositoryService exposes the adopt / unadopt /reclaim / link-team / unlink-team / pick_installation_developer
surface; the canonical sync state (sync_status, last_sync_at,
webhook bookkeeping, incremental cursors) lives on
workspace_repositories since sync is workspace-owned now.
LimitsService.can_adopt_repository(workspace_id) counts active
rows against the workspace's effective plan and gates the adopt
endpoint. Removes the per-developer counter from the gating path
(still used as a display-only roll-up on the limits widget).
search/import, the auto-sync Temporal scheduler, developer
insights, sync-status. Per-developer enable/disable endpoints
are removed; the column DeveloperRepository.is_enabled stays
as a discovery cache and gets cleaned up in a follow-up.
/settings/projects/{projectId}/repositories for picking which
workspace repos a project tracks.
/settings/repositories lists workspace_repositories whose
adopter is no longer an active workspace member, with a one-click
"Reclaim" action that re-binds the row to the active member who
clicked it (or any active member with reach as a fallback).
WorkspaceRepository.sync_status='no_credentials' is set
automatically when the auto-sync scheduler can't get a token,
surfacing the same banner.
handleRepoToggle on /settings/repositories to call workspaceRepositoriesApi.adopt / unadopt instead of
the removed per-developer endpoints; existing UI keeps working,
the toggle now adopts into the current workspace.
Sprint-less project tasks had attachment upload gated behind a "Move
this task into a sprint to upload attachments" banner because the
only attachment routes lived under /sprints/{sprint_id}/tasks/....
Added parallel endpoints under /teams/{team_id}/tasks/{task_id}/attachments
(POST / GET / DELETE) authorised via team membership. Both routers now
share the same upload, list, and delete logic via a new
backend/src/aexy/services/task_attachment_service.py (S3 put,
storage-quota assertion, AI metadata pipeline dispatch, S3 delete,
quota-cache invalidation — all in one place). The frontend picks the
right endpoint based on task.sprint_id; the gate banner is gone.
The PR linking section in the task modal now works for project-level
tasks — new endpoints GET /teams/{team_id}/tasks/github/pull-requests
and POST /teams/{team_id}/tasks/{task_id}/github-links/pull-requests
mirror the sprint-scoped equivalents (workspace-membership check on
the PR author preserved). The list endpoint at
/teams/{team_id}/tasks/{task_id}/github-links now returns both issue
and PR links (previously filtered to github_issue only). The
EditTaskModal dispatches search and link mutations to either endpoint
based on whether the task has a sprint_id.
New POST /teams/{team_id}/tasks/import (with
projectTasksApi.importTasks on the frontend) imports GitHub issues
into the team's backlog without requiring a sprint, populating the
"Select issue" dropdown across every task in the team. New service
helpers add_project_task and _import_project_task_items keep the
import dedup keyed on (team_id, source_type, source_id).
The History tab previously rendered "Move this task into a sprint to
view its full activity history" for sprint-less tasks because the only
activities + comments routes were sprint-scoped. Added the matching
team-scoped routes (GET /teams/{team_id}/tasks/{task_id}/activities
and POST /teams/{team_id}/tasks/{task_id}/comments) and updated
AssignmentHistoryPanel to dispatch by task.sprint_id vs
task.team_id. Activity rows are keyed on task_id only on the model
side, so existing per-task creation / status / assignment / field-change
events surface for backlog tasks without any data backfill.
Audit pass on every place that mutates a SprintTask. Previously
silent paths now write per-task TaskActivity rows:
SprintTaskService.update_taskinstead of duplicating field assignments, so backlog edits get the
same per-field timeline (title_changed, priority_changed, etc.)
that sprint tasks have.
status_changed row in addition to the workspace EntityActivity it already emitted.
attachment_added / attachment_removed rows attributed to the actor; affects sprint
AND project tasks (this was missing for both).
archived / unarchived rows; actor_id threaded through archive_task, unarchive_task,
and remove_task on the service.
sprint_id, the dedicated move-to-sprint endpoint, and bulk_move_to_sprint) write
sprint_changed with prior and new sprint IDs.
points_changed row when theestimate it stamps onto each task differs from the prior value.
created row so backlogtimelines start with "X created this task" instead of empty.
TaskActivityAction extended with attachment_added,
attachment_removed, archived, unarchived, and sprint_changed,
with renderer cases in both task modals.
POST /teams/{team_id}/tasks accepted start_date, end_date, and
estimated_hours in ProjectTaskCreate but the handler instantiated
SprintTask(...) without passing them through, so a fresh task always
saved with NULL dates and NULL hours regardless of the form. The
frontend create path mirrored the drop —
useProjectBoard.addTaskMutation explicitly listed each forwarded
field and the dates/hours weren't in the list. Wired all three fields
through every layer (SprintTask kwargs in the backend, mutationFn type
and forwarding, and the create and addTask API client signatures).
The same route accepted start_date, end_date, estimated_hours,
and contributes_to_goal in SprintTaskUpdate but the inline
update in project_tasks.py:update_task only handled
title/description/story_points/priority/status/labels/epic_id/sprint_id/
assignee_id/mentions. Editing dates or hours on a backlog task looked
successful but nothing persisted. Added the four missing assignments
with model_fields_set semantics on the date and hours fields so
callers can clear them by sending explicit null; contributes_to_goal
is non-nullable on the model and stays "set when explicitly provided."
task_to_response was duplicated across sprint_tasks.py and
project_tasks.py and the project-tasks copy was missing
attachments, work_started_at, cycle_time_hours,
lead_time_hours, contributes_to_goal, start_date, end_date,
and estimated_hours. Result: uploading an attachment to a backlog
task succeeded server-side, but when the UI re-fetched the task via
the project-task list/get/update endpoints, the response serialized
attachments: [] and stale nulls for dates/hours. Extracted the
canonical builder into a new
backend/src/aexy/services/sprint_task_response.py and pointed both
routers at it, so the response shape stays in lockstep going forward.
The mirror bug on the sprint-scoped route: data.description_json
came in via Pydantic but task_service.update_task had no parameter
for it, so the rich-text representation never updated even when the
plain description did. Added a sentinel-typed description_json
parameter to SprintTaskService.update_task (with no activity-log
entry — description_changed already covers that), and pass it
through from the sprint-tasks PATCH handler.
sprintApi.updateTask, projectTasksApi.update, and
useProjectBoard.updateTaskMutation had TypeScript signatures that
omitted start_date, end_date, estimated_hours, and
contributes_to_goal. The runtime axios call still sent them
(JavaScript is permissive), but the types misled callers. Added the
missing fields so the contract matches the backend.
Patch release on top of 0.7.7. Fixes a production-only file-upload
outage, light-mode contrast on the task-create form, and brings the
deployment docs in line with the real stack.
docker-compose.prod.yml had no rustfs (or any S3-compatible) service
and no S3_ENDPOINT_URL / S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY
env vars on backend or temporal-worker, even though the dev compose
ships rustfs and points the backend at it. Result: in production
StorageService.is_configured() returned False and every file upload —
task attachments, recording uploads, compliance docs — returned `503
File storage is not configured on this deployment. Added a rustfs`
service to the prod compose (internal-network only, with healthcheck),
wired the S3 env vars on backend and temporal-worker, added
rustfs_data and rustfs_logs volumes, added an /storage/ proxy
location to nginx/nginx.conf so uploaded URLs are reachable from the
browser, and seeded RUSTFS_ROOT_USER / RUSTFS_ROOT_PASSWORD /
S3_PUBLIC_ENDPOINT_URL in .env.prod.example. Existing operators
need to set those three values in .env.prod and re-run
docker compose -f docker-compose.prod.yml up -d.
The native <input type="file"> "Choose files" button on the new-task
form and the secondary "Link issue" button on the GitHub Issues panel
both used bg-primary-*/10 + text-primary-200/300 — both very light
blue, which collapses to barely-visible against the form background in
light mode. Reskinned all three controls (two file inputs + the link
button) to the solid bg-primary-600 + text-white style already used
by the primary "+ Link" button, so they pass contrast in both light
and dark mode.
A new docs/guides/database-operations.md is now the canonical
reference for everything that touches PostgreSQL: the custom SQL
migration system at backend/scripts/migrate_*.sql, manual and
automated backups (the production aexy-backup sidecar at 02:00 UTC),
restore from sql dump, restore from volume snapshot, the safe
postgres image-rebuild flow (data on the postgres_data named
volume is independent of the image — down -v is what kills it),
the major-version upgrade dump-and-reload procedure, and pgvector
specifics. Linked from docs/README.md, DEPLOY.md, and the
deployment guide.
DEPLOY.md and docs/guides/deployment.md were brought in line with
the actual stack: the alembic upgrade head references became
python scripts/run_migrations.py, the Celery / Celery beat /
Flower references became Temporal worker / Temporal UI / Temporal
schedules, the postgres prerequisite is now PG 18 with pgvector
(the bundled aexy-postgres:18-alpine-pgvector image) instead of
PG 14/16, and the deployment example compose now includes the
temporal, temporal-ui, and temporal-worker services. The
backup/restore quick-references in both docs now point at the new
Database Operations guide for full procedures.
Workspace owners/admins now have a dedicated breakdown page at
/settings/billing/breakdown answering "what am I being charged this
period and why." Platform admins get the same view across every
workspace at /admin/billing with a margin column and a click-to-drill
drawer. Both reuse a single BillingBreakdownView component so the
shape and behavior stay consistent.
BillingBreakdownService (backend/src/aexy/services/billing_breakdown_service.py) composes LimitsService, UsageService, PostpaidBillingService,
and StorageQuotaService into one typed BillingBreakdown. Line
items: base subscription fee, active seats (with included vs
billable split), LLM usage per provider (tokens, request count,
rate display), storage usage (informational), plus info counters
for plan-included free tokens and postpaid accruals. The service
reads period bounds from WorkspaceSubscription.current_period_*
and falls back to the current calendar month.
GET /api/v1/billing/breakdown?workspace_id=…&period=current|previous|YYYY-MM and GET /api/v1/billing/breakdown/history?workspace_id=…&months=6,
gated by verify_workspace_admin. Margin information is never
exposed via these routes.
/api/v1/platform-admin/billing/*: breakdown, breakdown/history, summary (paginated, filterable
workspace table), and totals (revenue, margin, top workspaces,
plan-tier and billing-model splits). Margin (base_cost_cents
vs charged_cents from the snapshotted UsageRecord rows) is
exposed only here.
BillingBreakdownView renders the period header, total/delta cards,a category-grouped line-item table with per-item drilldown (provider,
request counts, base cost when admin), info counters, invoices for
the period, and a 6-period sparkline. The delta_cents /
delta_pct are computed against the prior month's
usage_aggregates row, falling back to live SQL over
usage_records when the aggregate is missing.
Billing Breakdown (adminOnly) under Account in settingsNavigation.ts, and Billing in the platform-admin sidebar
in (admin)/layout.tsx.
New aggregate_billing_usage activity (analysis.py) wired into
worker.py and scheduled in schedules.py to run every 24h. It
calls UsageService.update_usage_aggregate for every active
customer subscription's current period, plus the current and prior
calendar month for every workspace that has any usage. Without this
job the historical breakdown view stays empty in production —
nothing else writes to usage_aggregates.
Added settings.billing.breakdownPage and settings.platformBilling
translation namespaces in messages/en/settings.json and
messages/hi/settings.json. Every user-facing string in the new
pages and the shared BillingBreakdownView component goes through
useTranslations(). Plan tier and billing-model labels stay in
English in the Hindi translations per project convention.
The breakdown previously emitted a synthetic free_credit line item
with a negative subtotal, dropping total_cents by an estimated
allowance. The Stripe billing pipeline
(UsageService.report_workspace_usage_to_stripe) reports the raw
sum of UsageRecord.total_cost_cents with no such deduction —
per-member free quotas live on Developer.llm_overage_cost_cents
and never reduce the workspace invoice. The result was that the UI
showed a lower bill than what Stripe charged. The synthetic credit
is now surfaced as free_tokens_per_member_per_month and
llm_tokens_used info counters plus a computation note explaining
the per-developer scope, so total_cents always equals what the
billing pipeline reports.
GET /platform-admin/billing/summary was paginating on the workspace
query first, then dropping rows whose computed plan_tier or
billing_model didn't match. A filtered request could return an
empty first page even when matches existed on later pages, and the
total count reflected only the search filter. The filters are now
pushed into SQL: plan_tier joins Workspace.plan_id → Plan.tier,
billing_model joins `WorkspaceSubscription.workspace_id →
WorkspaceSubscription.billing_model. total` reflects the filtered
set, and pagination operates on the filtered query. Workspaces with
no active subscription row are excluded when billing_model is set
(they have no canonical workspace-level billing model to filter on);
plan-tier filtering uses the source plan tier and does not consider
workspace plan overrides.
The History tab on the task modal now shows every change to a task —
not just assignment and status — and every change is attributed to the
user who made it. A reviewer can see who created the task, who renamed
it, who shifted the dates, who edited the description, who reassigned
it, and who dragged it across the board, top-to-bottom in the order
events actually happened.
SprintTaskService.update_task now snapshots each field before mutation and writes a per-task TaskActivity row (title_changed,
description_changed, points_changed, priority_changed,
status_changed, labels_changed, epic_changed,
start_date_changed, end_date_changed,
estimated_hours_changed) for every value that actually changed.
Description bodies are not stringified into old_value/new_value
— only the fact that the description changed is recorded — to keep
the activity row small for rich-text edits.
update_task_status and bulk_update_status now accept an actor_id and write a per-task activity row attributing the status
change to the user who dragged the card or clicked the pill.
Previously the workspace-wide EntityActivity feed had this but
the modal's History tab did not.
create_task records the creator on the created activity row, sothe History tab opens with a "X created this task" line instead of
silently starting at the first edit.
TaskActivityAction union extended in frontend/src/lib/api.tswith the six new field-change actions, and the renderer in
AssignmentHistoryPanel (board page) and ActivityItem (single
sprint page) now switches on every action with human-readable
copy: "renamed to X", "set due date to Y", "cleared estimate", etc.
it shows everything, with the actor name on every line.
Dropping a task into a new column updates the cache before the
network round-trip, so the card stays where the user dropped it
instead of snapping back to its original column for ~100 ms before
re-rendering. Both useSprintTasks (sprint board) and
useProjectBoard (workspace tasks) gained onMutate /
onError / onSettled handlers that snapshot the prior cache,
apply the new status optimistically, roll back on failure, and
invalidate on settle. The "snap back, then move" flicker that
made dnd-kit feel laggy is gone.
TipTap's Link extension was switched to openOnClick: false in
edit mode (when readOnly is false), so single-clicking a link
inside the editor now lands the cursor on it for editing instead
of opening it in a new tab. Cmd/Ctrl+click still opens the link.
In read-only renders (description preview, comment view) plain
clicks open the link as before.
DELETE /sprints/{sprint_id}/tasks/{task_id}/attachments/{id} was
removing the task_attachments row but leaving the underlying S3
object in RustFS forever, so deleted files kept counting against
the workspace's storage quota. The endpoint now derives the storage
key from the attachment URL via the new
StorageService.key_from_url (handles both path-style and the R2
virtual-hosted style), calls delete_object, and invalidates the
workspace usage cache via StorageQuotaService so the quota meter
catches up immediately.
Reassigning a task by sending PATCH /sprint-tasks/{id} with a new
assignee_id updated the row and wrote the assignment activity, but
never dispatched the task.assigned automation trigger — only the
dedicated /assign endpoint did. So workspace automations subscribed
to task.assigned (Slack DMs, Linear sync, etc.) silently missed
every reassignment performed through the task modal's edit flow.
update_task now mirrors assign_task's dispatch_automation_event
call when the assignee changes.
_stringify_field in sprint_task_service renders TaskActivity field values consistently — None stays None (so
the History tab can render "—"), datetimes go through .isoformat(),
and lists join with , . Avoids the "None" string showing up
in old/new value cells.
hasattr(task, "attachments") guard in task_to_response — the attachments relationship is always
present on SprintTask since the v0.7.4 schema migration.
A workspace-wide Drive backed by S3-compatible storage (RustFS in dev),
enriched by an AI metadata pipeline that captions images, tags documents,
and annotates videos with timecoded events from a vision-language model.
drive_files table with folder hierarchy, soft delete, and per-kindrendering hints (file / folder / image / video / audio / pdf / doc).
Smart Views are filter overlays — they don't move files, they translate
a JSONB filter to a file_metadata join. Migration
migrate_drive_v1.sql is idempotent and adds covering partial indexes.
/workspaces/{ws}/drive/files, /folders, /files, /files/{id}, /smart-views, /files/{id}/annotations, /files/{id}/reannotate,
and /usage endpoints. Multipart upload caps at 500 MB per file and
2 GB per batch before the plan-level quota check, protecting worker
memory.
/docs/drive: file grid, smart-view sidebar, hybridsearch bar, multi-file dropzone, quota banner, and a video player that
overlays Qwen-VL annotations on the timeline.
max_storage_gb (with -1 for unlimited),workspace-level overrides, and a Redis-cached usage rollup spanning
drive_files, task_attachments, and compliance_documents. Concurrent
uploads are serialised per-workspace via a Postgres advisory lock so
two simultaneous uploads can't overshoot the cap.
A single file_metadata row per file regardless of where the file lives.
(source_type, source_id) is unique across drive_file,
task_attachment, and compliance_document. file_embeddings and
video_annotations foreign-key to file_metadata.id, so a non-Drive
video (e.g. a task attachment) can carry annotations through the same
machinery. Adding a fourth source type is one resolver registration —
no schema change.
migrate_file_metadata_v1.sql creates the schema in a single transaction with a GIN index on ai_tags/ai_categories and an
ivfflat cosine index on the 1024-dim embedding column.
/workspaces/{ws}/files/{source_type}/{source_id}/metadata and .../reannotate endpoints — the frontend's universal "Reannotate"
button posts here regardless of source.
/workspaces/{ws}/search/files?q=…&kinds=… workspace-wide hybrid search: pgvector cosine over file_embeddings plus an ILIKE pass over
ai_summary and per-source file names. Cmd+K palette
(WorkspaceSearchPalette) is the user-facing surface.
/workspaces/{ws}/source-files?source_type=… browse endpointreturns a unified file row for any source. The Drive sidebar uses it
to render virtual cross-source views ("Task attachments",
"Compliance documents") in the same grid as drive files.
The gateway grows lazy vision and embeddings properties selected via
settings.llm.vision_provider / embeddings_provider. Provider keys
are tracked separately from chat-LLM usage so vision + embedding spend
shows up distinctly in the rate limiter.
qwen/qwen2.5-vl-72b-instruct by default) and local Ollama (any Qwen-VL tag). Both implement analyze_image and
analyze_video_frames.
text-embedding-3-large@1024) and Ollama (bge-m3). Both produce pgvector-compatible 1024-dim vectors
so the two backends are interchangeable.
gateway.embed_batch_limited, vision_image_limited, and vision_video_frames_limited helpers gate every call through the
Redis rate limiter. Provider keys: qwen-openrouter, qwen-ollama,
embeddings-openrouter, embeddings-ollama.
asyncio.to_thread, so a multi-minute video doesn't block the worker
event loop.
A super-admin UI under /admin/plans to inspect plans, edit
per-workspace overrides, and kick off the AI metadata backfill for
existing rows.
scans uncovered drive_files, task_attachments, and compliance_docs
and dispatches the AI pipeline at the configured rate. The button
is idempotent — re-clicking finds the running workflow rather than
starting a parallel one.
vision_provider, vision_model, embeddings_provider,
embeddings_model, and embeddings_dim now live under the LLMSettings
group instead of the root Settings. Existing VISION_PROVIDER /
EMBEDDINGS_* env vars continue to work.
Added to both frontend/src/config/appDefinitions.ts and
backend/src/aexy/models/app_definitions.py so it shows up in app-bundle
permission templates and the sidebar layout filter.
ai_status, ai_summary, ai_tags, ai_categories, and
ai_processed_at were removed from drive_files and the DriveFile
TypeScript interface. AI metadata is now read from file_metadata via
the polymorphic endpoint or the useFileMetadata hook. FileCard fetches
its own AI metadata per row, which means task_attachment and
compliance_document files render with the same AI badges in the Drive
grid.
GET /workspaces/{ws}/drive/search and driveApi.search are gone.
Callers use the workspace-wide /search/files?kinds=drive_file endpoint
(via useDriveSearch, which adapts the response to the legacy hit
shape so the UI didn't have to change).
imports survived the polymorphic-metadata refactor — DriveFileEmbedding
in drive_search_service, VideoAnnotation.file_id in drive_service,
and a max_storage_gb default placed before required dataclass fields
in EffectivePlan. Each one raised at module-import time, taking down
the entire FastAPI app on startup. All cleaned up; drive_search_service
was removed entirely (replaced by the cross-source file_search_service).
gateway was reading settings.vision_provider etc. off the root
Settings, but those fields had been moved to LLMSettings. First
call to gateway.vision or gateway.embeddings crashed.
_scan helper's select(FileMetadata).join(FileMetadata, …) re-joined
FileMetadata onto itself; the source table was never in the FROM
clause. Now starts from the source table and joins file_metadata
correctly.
folder A under one of its own descendants (A → … → D → A) silently
succeeded and corrupted the tree. Now walks the parent ancestry and
rejects on collision.
get_llm_gateway() returned None (misconfigured or no API keys), FileSearchService and the Drive
search route called gateway.embeddings and crashed. Both now accept
Optional[LLMGateway] and degrade to keyword-only search.
route replaced with Body(default_factory=BackfillStartRequest).
match an allowlisted host suffix (.amazonaws.com, .cloudfront.net,
.r2.cloudflarestorage.com, .aexy.io) or the configured
s3_endpoint_url. After DNS resolution, every returned IP is checked
against private / loopback / link-local / multicast / reserved /
unspecified ranges, defending against DNS rebinding attacks where a
"public" hostname resolves to 169.254.169.254 or RFC1918. Storage
endpoints matched verbatim skip the IP check by design (ops controls
those names; they often resolve privately). follow_redirects=False
prevents 30x bypass.
/workspaces/{ws}/files/{source_type}/{source_id}/reannotate endpoint
used to dispatch the LLM pipeline without verifying that source_id
belonged to workspace_id. Any workspace member could trigger
reprocessing of any file in any workspace by guessing a UUID, charging
the LLM bill to the wrong tenant. Now resolves the source row and
rejects with 404 when the workspace doesn't match.
workspace could both pass the cached usage check and overshoot the
cap by ~2× the incoming bytes. assert_storage_available now wraps
the check in pg_advisory_xact_lock(hashtextextended(workspace_id, 0))
and reads the used-bytes total fresh from the DB inside the lock.
migrate_source_files_idx_v1.sql):
- idx_drive_files_workspace_uploaded on
(workspace_id, uploaded_at DESC) partial
WHERE deleted_at IS NULL AND kind <> 'folder' — covers the exact
scan the endpoint runs and skips the sort step.
- idx_task_attachments_task_uploaded on (task_id, uploaded_at DESC)
— speeds the join-then-sort pattern when listing all task
attachments in a workspace.
- compliance_documents already had (workspace_id, created_at DESC)
from migrate_compliance_documents.sql — no new index needed.
messages/en/drive.json and messages/hi/drive.json cover the Drive UI: ~65 keys across drive.page, drive.fileCard,
drive.upload, drive.quota, drive.smartView, drive.video,
drive.aiBadges, drive.metadataPopover, drive.metadataSidecar,
and drive.search. ICU placeholders ({count}, {percent}, {used},
{limit}, {incoming}) match across both locales.
drive-quota.spec.ts, drive-smart-views.spec.ts, drive-upload.spec.ts,
compliance-doc-ai-sidecar.spec.ts, task-attachment-ai-tags.spec.ts,
workspace-search-palette.spec.ts, admin-backfill.spec.ts,
admin-plans-edit.spec.ts. Shared e2e/fixtures/drive-mock-data.ts
fixture seeds files, smart views, AI metadata, and quota state.
.gitignore extended for frontend/playwright-report/, frontend/test-results/, frontend/e2e/debug-screenshot*.png, and
REVIEW_*.md. The previously-tracked playwright-report/index.html
was removed from the index.
Sprint tasks now carry a scheduled timeline and uploaded files, and the
board surfaces when work has slipped.
start_date, end_date, and estimated_hours columns to sprint_tasks, plus a new task_attachments table with cascade delete.
Migration migrate_sprint_tasks_v3.sql is idempotent and indexes
end_date and task_id.
POST/GET/DELETE /sprints/{sprint_id}/tasks/{task_id}/attachmentsendpoints. Multipart uploads stream through the existing S3-compatible
storage service (RustFS).
hours field, and a multi-file uploader. Files are uploaded after the
task is created so cascade delete cleans up cancelled flows.
with download links and delete actions.
Overdue badge when end_date has passed and the task is not done, and an Over estimate badge when actual cycle
time exceeds estimated_hours. Both are pure-frontend computations.
The EditTaskModal grows a History tab showing the full reassignment
chain so reviewers can see who originally assigned a task and every
hand-off in between.
assign_task, unassign_task, and the assignee branch of update_task now write both old and new assignee IDs into the
per-task TaskActivity stream and the workspace-wide
EntityActivity feed.
resolves participant names from workspace members, and renders them
oldest-first so the chain reads in the order it actually happened.
Drag-and-drop listeners moved from the small GripVertical handle onto
the TaskCardPremium root, so the entire card body initiates a drag.
The grip icon remains as a visual affordance. Interactive children
(menu, checkbox, quick-status, archive, quick-edit) stop pointer-down
propagation so clicks on them no longer initiate a drag.
The TipTap Link extension now uses openOnClick: true with
target="_blank" and rel="noopener noreferrer nofollow", so URLs
typed into a task description open in a new tab on click instead of
being inert.
task creation with start/end dates and estimated hours; the Overdue
badge; the Over estimate badge; the assignment history chain; the
whole-card drag affordance; and clickable links in saved
descriptions. A shared task-test-helpers.ts fixture sets up the
board mocks for all of them.
Task modals now link to real synced GitHub pull requests instead of the
old placeholder pr_references field.
task GitHub links, manually link a PR, and unlink an existing PR.
state, and outbound GitHub links.
loading/error feedback through React Query mutations.
link, displaying existing PR links, linking a synced PR, and unlinking
an existing PR.
Tasks can now connect to GitHub issues from the project board.
task_github_links with repository,issue number, title, state, and URL.
backlog tasks.
which repo will be used for bare #123 references.
owner/repo#123 references and GitHub issue URLs. Bare #123 links only when the
project has a single imported GitHub issue repository.
supports manual issue linking from imported GitHub issues.
links using owner/repo, #123, owner/repo#123, or full GitHub
issue URLs.
linking, cross-repo issue override, and issue unlinking.
Closing a task modal opened from /sprints/{projectId}/board?task=...
now removes only the task query parameter and prevents the modal from
immediately reopening while the route updates. The same modal path is
used from the board and deep-link entry points.
Refined the task modal into a wider, more deliberate editing surface:
status changes are saved explicitly, unsaved edits prompt before closing,
dialog accessibility metadata was added, and the GitHub PR section now
lives in the main task content area.
Added direct Microsoft 365 / Entra ID sign-in alongside the existing
Google flow. Tenant defaults to common so both personal (@outlook.com,
@hotmail.com) and work/school accounts can sign in.
GET /api/v1/auth/microsoft/login (basic profile + email), /auth/microsoft/connect-crm (adds Mail + Calendar via Graph), and
/auth/microsoft/callback. Two-scope split mirrors Google.
MicrosoftConnection SQLAlchemy model and migration (migrate_2026_04_14_microsoft_connections.sql), parallel to
GoogleConnection.
DeveloperService.get_or_create_by_microsoft with scope-merge rule:a subsequent basic login never clobbers tokens that already hold
Mail.Read / Calendars.ReadWrite.
/me user info uses mail with userPrincipalName fallback (personal accounts return mail: null).
user signs in, so Azure AD changes propagate.
two CTA blocks on the landing page.
state validation, happy-path callback with mocked Graph responses,
and the personal-account userPrincipalName fallback.
New aexy.services.oauth_token_service centralises refresh-token
behaviour for every OAuth-holding row type (developer connections,
workspace Google integrations, booking calendar connections). Three
ad-hoc copies of the refresh flow (gmail_sync_service,
calendar_sync_service, booking/calendar_sync_service, and
api/chat.py) have been retired — they each had the same two bugs:
rotated refresh tokens were silently dropped, and every non-200
response was treated as "please reconnect" without distinguishing
invalid_grant from a transient 5xx.
ensure_valid_google_token(db, GoogleConnection), ensure_valid_microsoft_token(db, MicrosoftConnection),
ensure_valid_google_integration_token(db, GoogleIntegration), and
ensure_valid_calendar_connection_token(db, CalendarConnection) all
share two primitives (_refresh_google, _refresh_microsoft).
- Nullable refresh_token columns are cleared (raises
RefreshTokenRevokedError).
- GoogleIntegration.refresh_token is NOT NULL, so it's marked
is_active=False + last_error="refresh_token_revoked".
- Booking CalendarConnection additionally flips sync_enabled=False.
and the narrow Calendars.ReadWrite offline_access pair for booking
calendars.
invalid_grantclearing, transient 5xx preserving state, scope propagation, and
the CalendarConnection dispatch-by-provider behaviour.
The persona/preset selector that filters sidebar sections and chooses
dashboard widgets was previously reachable only via the Dashboard
"Customize" modal. It now also lives at /settings/appearance, wired
to the same useDashboardPreferences hook so Dashboard and Settings
stay in sync.
The /sprints empty-state and top action bar now open an inline
project creation modal instead of redirecting to
/settings/projects. On create, the user lands directly on
/sprints/{newProjectId}/board. The shared
CreateProjectModal component is used by both pages.
Next 16 made params in [projectId]/board/page.tsx (and siblings) an
async Promise. Fixed across 12 dynamic routes under /sprints and
/crm/agents: client components use React.use(params), server
components await params.
"Create workspace" link in the sidebar (WorkspaceSwitcher) routed to
/onboarding/workspace, which the OnboardingGuard redirected back to
/dashboard for already-onboarded users — making workspace creation
impossible. The guard now lets /onboarding/workspace through, stale
localStorage state is cleared on visit, and the newly created
workspace is auto-selected via switchWorkspace() so the sidebar
updates immediately.
Added suppressHydrationWarning on <html> in the root layout — the
Redeviation DevTools extension injects data-redeviation-bs-uid onto
the tag before React hydrates.
/settings/projects; it creates the project in-place and jumps to
the new board.
docker-compose.yml and docker-compose.dev.yml no longer set
LLM_PROVIDER, LLM_MODEL, or any *_API_KEY — pydantic reads them
from backend/.env by itself. Previously compose set empty strings
that silently shadowed .env, so switching providers required editing
compose instead of .env. Production compose keeps the injected-via-
shell pattern it was designed for.
npm audit fix cleared the 8 non-breaking advisories (critical axios,
high next/rollup/picomatch, moderate brace-expansion/follow-redirects/
markdown-it/next-intl open-redirect). Upgraded vitest 1.2.1 → 4.1.4
to clear the remaining vite path-traversal + esbuild dev-server
issues; tightened vitest.config.ts include/exclude so vitest 4's
stricter scanner doesn't pull in Playwright e2e specs from
.next/standalone/. Pinned node-fetch ^2.7.0 via overrides
rather than downgrading face-api.js (which npm audit fix --force
wanted to do to no actual security benefit).
Added direct DeepSeek API support alongside Claude, Gemini, Ollama, and OpenRouter. DeepSeek uses an OpenAI-compatible endpoint (https://api.deepseek.com/chat/completions) with models deepseek-chat (non-thinking DeepSeek-V3.2) and deepseek-reasoner (thinking DeepSeek-V3.2).
DeepSeekProvider with model fallback, 429 retry-after handling, usage extractionLLMGateway factory + get_llm_gateway() bootstrapDEEPSEEK_API_KEY and DEEPSEEK_FALLBACK_MODELS env vars (defaults to deepseek-reasoner)DEEPSEEK_REQUESTS_PER_MINUTE, DEEPSEEK_REQUESTS_PER_DAY, DEEPSEEK_TOKENS_PER_MINUTEdeepseek in llm_provider_accesstests/unit/test_deepseek_provider.py (12 tests, mocked HTTP)scripts/check_llm_provider.py — provider-agnostic; runs health_check → call_llm → analyze(CODE) → extract_task_signals and reports pass/fail. Use any time a provider or model is swapped.The sidebar "Create workspace" link routes to /onboarding/workspace, but the OnboardingGuard was redirecting already-onboarded users back to /dashboard — making workspace creation impossible post-onboarding.
OnboardingGuard now allows /onboarding/workspace (and /onboarding/complete) through for existing users/dashboard (instead of /onboarding/connect) and the new workspace is auto-selected via useWorkspace.switchWorkspace() so the sidebar updates immediately<html> caused by the Redeviation browser extension injecting data-redeviation-bs-uid — added suppressHydrationWarning to the root layoutdocker-compose.yml and docker-compose.dev.yml no longer hardcode LLM_PROVIDER, LLM_MODEL, or any *_API_KEY. LLM config is read from backend/.env by pydantic settings — single source of truth. Previously empty-string values in compose silently shadowed .env, breaking provider selection. Prod compose (docker-compose.prod.yml) continues to inject secrets from the host shell env as designed.Comprehensive UX/UI audit of the Performance Reviews feature with screenshot-driven TDD fixes.
confirm()), ARIA tab attributes (role=tablist/tab/tabpanel), breadcrumb navigation consistency, mobile card view for cycles DataTable, user-facing error toasts on API failureshtmlFor/id), live goal card preview on create form, aria-label on icon-only buttons, cycle timeline preview with phase markers, aria-live regions for screen readers, unified loading spinners to primary-500review-screenshots/REVIEW_AI_UX_AUDIT.md with before/after screenshotsnext from 14.1.0 to 16.2.1, react/react-dom to 19.xCustomFieldTypeManager.tsx (stricter parser)@tiptap/suggestion dependencyuseAppAccess.tsFull i18n infrastructure with English + Hindi support across all modules.
npm run i18n:merge (auto-runs on prebuild)useTranslations() — remaining pages can adopt incrementallydocker-compose.dev.yml added with non-conflicting ports for parallel developmentserver_default syntax fix in dashboard and CRM modelsOpenRouter is now available as a first-class LLM provider, giving access to 100+ models (Claude, GPT-4o, Llama, Gemini, DeepSeek, etc.) through a single API key.
LLMProvider implementation using the OpenAI-compatible chat completions API (POST /chat/completions) with Bearer auth, rate limit handling (429 with retry-after), and health checks via /modelsOPENROUTER_FALLBACK_MODELS (comma-separated) to customize the fallback orderOPENROUTER_API_KEY, OPENROUTER_MODEL (default: anthropic/claude-sonnet-4), OPENROUTER_FALLBACK_MODELS (default: google/gemini-2.0-flash,openai/gpt-4o,deepseek/deepseek-chat-v3,meta-llama/llama-3.1-70b-instruct) env varsOPENROUTER_REQUESTS_PER_MINUTE, OPENROUTER_REQUESTS_PER_DAY, OPENROUTER_TOKENS_PER_MINUTE)OPENROUTER_INPUT_PRICE_PER_MILLION, OPENROUTER_OUTPUT_PRICE_PER_MILLION)OPENROUTER_API_KEY passed through in both docker-compose.yml and docker-compose.prod.ymlAuto-CRM contact creation and onboarding drip email sequences triggered on user signup.
PLATFORM_ORG_ID is configuredplatform_on_signup activity dispatched from the signup flow for async processingPLATFORM_ORG_ID env var — set to a workspace UUID to enablePostmark is now available as an email provider across all three sending paths — notification emails, campaign/workflow emails, and mailagent domain-aware sending.
_send_via_postmark() method, is_postmark_configured property, and Postmark routing in _send_email() — set EMAIL_PROVIDER=postmark to use for all notification emailsEmailProvider implementation with send(), verify_credentials(), and native send_batch() (up to 500 per call via /email/batch)create, delete, list) and domains (verify, get) using the Account API tokenPOSTMARK_TRANSACTIONAL_STREAM, default outbound) and broadcast (POSTMARK_BROADCAST_STREAM, default broadcast) stream support — notification emails use transactional, campaigns use broadcastPOSTMARK_SERVER_TOKEN, POSTMARK_ACCOUNT_TOKEN, POSTMARK_SENDER_EMAIL, POSTMARK_SENDER_NAME, POSTMARK_TRANSACTIONAL_STREAM, POSTMARK_BROADCAST_STREAM env vars in backend; POSTMARK_SERVER_TOKEN in mailagentZulip-inspired real-time team chat with channels, topics, and threaded messages, accessible from a dedicated /chat page and a floating widget on every page.
ChatWebSocketProvider (no duplicate connections)Integrated AI chat assistant with multi-provider LLM support, server-side tool execution, and streaming responses.
chat_mention and ai_conversation_shared event typesApiToken model with aexy_ prefixed tokens, CRUD endpoints, create/validate/revoke service methods_check_workspace membership guard to every chat API endpoint (channels, topics, messages, presence, file upload)_check_channel_access helper enforcing membership checks on topic listing, creation, message listing, and message sending for private channelsmax_length constraints on all chat message and channel inputsonline, away, offline allowed)LIMIT 200 to prevent unbounded topic queriesdb.commit() in ChatService with db.flush(); explicit await db.commit() in all mutating API endpointsmessage_count updates; correlated subqueries for list_conversations in Ask AIIntegrityError handling for concurrent topic/message creationReact.memo on MessageItem, memoized WebSocket context value, deduplicated markTopicRead callsget_current_developer_id now uses the injected DB session instead of creating a separate one via get_async_session()NEXT_PUBLIC_API_URL env var instead of hardcoded localhostpermission fields in share schemas use Literal["read", "write"] instead of strAskAIChatPanel now uses useChatWebSocketContext() instead of creating a second useChatWebSocket() connectionaction_url is a relative path (starts with /, not //)http:/https: only) before rendering user-provided URLs as <img> or <a> elementsuseStreamMessage accepts override conversationId parameter, eliminating unreliable setTimeout in widget first-message flowuseStreamMessage uses useAskStore.getState() for mutations during streaming, preventing cascading re-rendersAskShareDialog wraps participantIds Set in useMemo for stable dependency trackingMessageThread queue-flush effect uses ref for sendMessage to prevent infinite re-render loops/chat pageswindow.confirm() before proceedingcomponents/ui/copy-buttonconfirm()migrate_ask_collaborative.sql — ask_conversation_participants and ask_share_links tables for collaborative AI conversationsFull multi-channel notification infrastructure with 4 delivery channels (in-app, email, Slack, web push) and workspace-wide event coverage.
slack_sent/slack_sent_at tracking columnssend_notification_web_push Temporal activitymention:user:{uuid} links from ticket comments, CRM notes, and sprint task comments; deliver in-app notifications respecting preferences (self-mentions skipped)Governance layer that evaluates agent tool calls before execution, with audit trail and billing integration.
tool_block, tool_require_approval, field_restriction, rate_limit, token_budget — workspace-scoped, priority-ordered, per-agent or globalBaseAgent._process_tools — blocked calls return [BLOCKED] reason as ToolMessage so the LLM can adjustagent_policy_decisions table with confidence contextagent_config_audits table tracks agent create/update/delete/toggle with old/new field diffsUsageService.record_usage() with analysis_type="agent_execution"/workspaces/{ws}/crm/agent-policies with admin-only mutations and workspace permission checksCross-module activity logging surfaced in a dedicated /activity page with filtering and infinite scroll.
log_activity() helper using begin_nested() savepoints so logging failures never roll back parent transactionsuseActivityFeed hook with useInfiniteQuery and IntersectionObserver-based paginationActivityFeedService.get_entity_url() resolves entity-specific deep linksis_archived) replaces hard delete for sprint tasksuser_id/user_name query params with JWT token verificationasyncio.Lock for WebSocket connect/disconnect to prevent race conditionsis_(False) instead of == Falseconfirm()/alert() with proper modal dialogs and toast notificationsPlanningPokerVote, PlanningPokerState, etc.)organization_id (Assessment model doesn't have workspace_id)log_activity block that created 2 entries per commentlog_activity calls where service layer already logs the same operationscurrent_user dependency and actor_id to submit_self_review, submit_manager_review, finalize_reviewNotificationService instead of bypassing itsprint_goals table for sprint goal trackingmigrate_notification_slack_sent.sql — slack_sent tracking columns on notificationsmigrate_notification_events.sql — 22 new event types and category preferencesmigrate_notification_providers.sql — web push subscription storage and VAPID configmigrate_agent_policies.sql — agent_policies, agent_policy_decisions, agent_config_audits tables with updated_at triggermigrate_app_access_requests.sql — app access request/approval workflowmigrate_sprint_goals.sql — sprint goals tableFull AI-powered go-to-market automation system for outreach, lead scoring, visitor tracking, competitor intelligence, and account-based marketing.
Phase 2A — Scoring Feedback Loop & Foundation
score_lead activities, linking engagement to CRM records/providers/available, displays configured providers with "Coming Soon" for unimplemented onesreply_received when routing replies to sales; Temporal workflows finalize with exit_reason="replied"Phase 2B — Outreach Excellence & Warmup
variant_index tracking on step executionsthread_id forwarding for conversation continuity across outreach stepsincrement_send_count naming, can_send() missing workspace_id, warming metrics field mismatchPhase 2C — Intelligence Layer & LLM Integration
Phase 2D — Scale & Ops
useSidebarPersona hook with server-persisted preferences+ buttonusePageVisitTracker hook records page visits for smart favoritesCATEGORY_LABELS and PERSONA_LABELS to appDefinitions.tssanitize_for_llm() strips injection patterns from external content before LLM promptsaexy-track.js with data-consent attribute, GPC signal support, and blocked identify() without consentsetattr with explicit allowlists in update_provider, update_template, update_competitorstr.format(**event_data) with string.Template.safe_substitute() in alert templatingrequired_role="admin" to 44 write/delete GTM endpointsapi/gtm.py (2844 lines) into 20 focused sub-modules under api/gtm/ packagetemporal/activities/gtm.py (1616 lines) into 9 domain modules under activities/gtm/purge_behavioral_events activity with 365-day configurable retentionmigrate_sidebar_preferences.sql — sidebar_pinned_items and sidebar_page_visits preferences/tables route for creating and managing standalone data tables, independent of CRM objects/api/v1/workspaces/{id}/tables) with listing, detail, field CRUD, record CRUD, and bulk operationsuseTables, useTableFields, useTableRecords, useTableAccess hooks for frontend data fetching/api/v1/public/tables endpoints for unauthenticated shared accesstable_audit_log table and TableAuditService for tracking all table mutationscrm_lists with entity_type for shared views across entity types%, _) in filter inputs to prevent filter injectionX-Share-Password headerWorkspaceMember queries into 1 in resolve_access__import__ hack, return type annotations, and ip_address type mismatches in backend_strip_hidden_columns methoduseMemo unstable dependency array in frontend componentsupdate_table and create_share_link endpointsDataTableServicemigrate_data_tables.sql — Core tables for data table supportmigrate_data_tables_phase3_7.sql — Audit log and share link tablesaria-current="page" supportg then X navigation pattern (like GitHub/Linear) for 19 modules? key opens categorized shortcut referenceticket.reopened, ticket.priority_changed, ticket.escalated, response.sent, response.received, sla.breachedcandidate.rejected, candidate.hired, assessment.score_above, assessment.score_belowsprint.velocity_calculated, sprint.burndown_off_trackmonitor.ssl_expiring, monitor.repeated_failurescampaign.sentstatusColors.ts, migrated 34 filesassessment.workspace_id did not exist on the Assessment model — changed to assessment.organization_id so score_above/score_below triggers actually fireTicketStatus.OPEN did not exist in the enum — changed to TicketStatus.ACKNOWLEDGEDid: "nav-templates" causing React key collision — renamed second to nav-automation-templatesonSuccess always showed success even when WebhookTestResult.success was false — now checks the resultuseMemoloadConfig, handleToggle, handleDelete used try/finally with no catch — added error handling with toast notificationsuseEffect missing loadConfig in dependency array — wrapped in useCallbackgetConfiguration caught everything and returned null — now only catches 404createExport(data as any) — replaced with proper type assertioncurrentWorkspaceId! non-null assertion could produce /workspaces/null/ API calls — added guardcandidate.rejected/candidate.hired dispatch calls in try/except for consistencyconfirm() with styled confirmation modalworkspaceId prop from CommandPalette, unused useAuth import from SSO pagerole="dialog", aria-modal, role="combobox" on search input, role="listbox" on resultsaria-sort on sortable headers, tabIndex and keyboard handlers (Enter/Space) for sortable headers and clickable rowsrole="dialog", aria-modal, aria-labelledby, aria-label="Close" on close buttonrole="dialog", aria-modal, aria-labelrole="alert" on error containeraria-label="Clear search" on clear buttonaria-current="page" on last breadcrumb itemaria-label="Dismiss banner" on dismiss buttonsComprehensive improvements to the automation workflow builder across all 10 modules.
TRIGGER_REGISTRY and ACTION_REGISTRY now return {id, description} objects instead of plain strings, with backward-compatible helper functions (get_trigger_ids, get_action_ids)standup.streak, time_entry.anomaly, blocker.pattern_detected, training.bulk_overdue, certification.prerequisite_unmet, etc.) and actions across all modulesid and description fieldsstarts_with, ends_with, not_contains, and between operators in CRMAutomationService._check_condition() which previously fell through to return Trueprint() calls in AutomationService.process_module_trigger() with proper logger.info/debug/error callsReplaced all "Coming Soon" placeholder widgets with full implementations using live data from existing hooks.
MyGoalsWidget, GrowthTrajectoryWidget, PeerBenchmarkWidget, LearningPathWidget, SkillGapsWidgetStandupStatusWidget, TimeTrackingWidget, UpcomingDeadlinesWidgetSLAOverviewWidget, RecentTicketsWidget, TicketsByPriorityWidget, FormSubmissionsWidget, RecentFormsWidgetRecentDocsWidget, DocActivityWidgetPerformanceReviewsWidget, PendingReviewsWidget, ReviewCycleWidgetHiringPipelineWidget, CandidateStatsWidget, OpenPositionsWidget, InterviewScheduleWidgetDealStatsWidget, RecentDealsWidget, CRMQuickViewWidgetTeamOverviewWidget, TeamActivityWidget, OrgMetricsWidget, SystemHealthWidgetTeamStatsSummaryWidget to use correct nested aggregate property pathsTicketChartWidget to use theme-aware colors instead of hardcoded dark-mode hex valuesTicketPipelineWidget to remove unnecessary as any castPeerBenchmarkWidget ordinal suffixes (1st, 2nd, 3rd instead of always "th")TicketsByPriorityWidget (unreachable priority breakdown branch)UpcomingDeadlinesWidget to use sprint end date and incomplete tasks instead of nonexistent due_date fieldFull leave management system with request/approval workflows, balance tracking, and holiday calendar management.
LeaveTypeService, LeavePolicyService, LeaveRequestService, LeaveBalanceService, HolidayServiceLeaveRequestForm, LeaveRequestCard, LeaveApprovalCard, LeaveBalanceCard, LeavePolicySettings, LeaveTypeSettings, HolidaySettings, TeamLeaveTableUnified calendar view showing leave, holidays, and team availability.
TeamCalendar, CalendarFilters, EventDetailModal, WhoIsOutPanelTemporal-powered automation for compliance monitoring and developer activity tracking.
standup.streak, time_entry.anomaly, blocker.pattern_detected, training.bulk_overdue, certification.prerequisite_unmetBacklogOverviewWidget, BlockersOverviewWidget, SprintBurndownWidget, TasksCompletedChartWidget, TeamStatsSummaryWidget, TicketChartWidget, TicketPipelineWidget, VelocityTrendWidget, WorkloadDistributionWidgetLeaveBalanceWidget, PendingLeaveApprovalsWidget, TeamAvailabilityWidget, TeamCalendarWidgetCampaign open/click tracking endpoints for email marketing analytics.
Dynamic app/module registration via AppDefinitions model and frontend config.
Temporal activity for periodic AI-powered insights generation with scheduled execution.
ghu_) using stored refresh tokens — tokens no longer silently expire after 8 hoursGitHubNotFoundError exception with non-retryable Temporal retry policyauth_status="error") instead of flooding Temporal with failing workflows@github_username and repo full name instead of opaque UUIDsSettingsShell, SettingsSidebar, and SettingsSearch componentsfix_subscription_plans.py script for correcting plan dataNodePalette expanded with compliance and tracking trigger/action nodesuseNotifications and useReminders hooksdocker-compose.prod.yml with additional service configurationtest:e2e / test:e2e:ui npm scriptsmigrate_leave_management.sql, migrate_github_auth_status.sql, migrate_developer_email_nullable.sql, migrate_repo_sync_settings.sqlReplaced the hardcoded dashboard layout with a fully dynamic, preference-driven widget rendering system. Widgets now render from widget_order and visible_widgets stored in user preferences, with drag-and-drop reordering support.
Widget Extraction (9 new components):
WelcomeWidget — greeting, GitHub connection status, quick action linksQuickStatsWidget — language count, framework count, avg PR size, work styleLanguageProficiencyWidget — language bars with proficiency scores, commit counts, trendsWorkPatternsWidget — complexity preference, peak hours, review turnaroundDomainExpertiseWidget — domain tags with confidence scoresFrameworksToolsWidget — framework/tool tags with proficiency scoresAIInsightsWidget — composite widget wrapping InsightsCard, SoftSkillsCard, GrowthTrajectory, PeerBenchmarkSoftSkillsWidget — Reviews & Goals section with My Goals and Performance ReviewsComingSoonWidget — placeholder for unimplemented widget IDsWidget Registry (`widgetRegistry.tsx`):
getWidgetComponent() helper with ComingSoonWidget fallbackisWidgetImplemented() check for registry membershipDashboard Page Rewrite (`page.tsx`):
orderedVisibleWidgets computed via widget_order intersected with visible_widgetsgetWidgetProps() switch maps widget IDs to their specific data propsgetWidgetGridClass() maps widget sizes to CSS grid column spansrenderWidget() skips composite children and renders from registry or ComingSoonWidgetSortableWidgetGrid Updates:
space-y-6 vertical stack to CSS grid: grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6renderableWidgets filter to skip null renders from composite childrentop-2 right-2Customize Modal — Reorder Tab:
DashboardCustomizeModalWidgetReorderList component — dnd-kit vertical list showing widget icon, name, size badge, and drag handleEnriched Non-Developer Presets:
aiAgents, upcomingDeadlines, recentDocsaiInsights, aiAgentsquickStats, aiAgents, upcomingDeadlines, myGoalsquickStats, aiAgents, teamOverview, myGoalsquickStats, aiAgents, teamOverview, upcomingDeadlinesquickStats, aiAgents, myGoals, upcomingDeadlines, recentDocsAdded end-to-end testing infrastructure for the dashboard.
playwright.config.ts — Chromium project, baseURL localhost:3000, auto-start dev servere2e/fixtures/mock-data.ts — mock user, preferences, insights, soft skills fixturese2e/dashboard.spec.ts — 18 tests across 6 describe blocks:- Widget Rendering (7 tests): welcome, quickStats, languageProficiency, workPatterns, domainExpertise, frameworksTools, ComingSoon
- Widget Ordering (2 tests): order from preferences, only visible widgets rendered
- Edit Layout Toggle (2 tests): button toggle, drag handles in edit mode
- Customize Modal (4 tests): three tabs, tab switching, reorder tab content, close
- Manager Preset (1 test): cross-cutting widgets present
- Grid Layout (2 tests): CSS grid container, full-span widgets
0.5.5 to 0.5.6@playwright/test dev dependencytest:e2e and test:e2e:ui npm scriptsExtended GitHub sync to capture all contributors' commits, PRs, and reviews — not just the connecting user. External contributors are auto-created as "ghost" Developer records.
Backend:
author_github_login and author_email on Commit for preserving original author identity_resolve_developer_for_commit() and _resolve_developer_for_pr() in SyncService to match or auto-create Developer records by GitHub ID or emailauthor=github_username filter from _sync_commits_with_session() — now fetches all commitslogin != github_username filter from _sync_pull_requests_with_session() and _sync_reviews_with_session()migrate_commit_author_fields.sql — adds author_github_login, author_email columns with indexesGhost Developer Support Across Insights:
_get_all_contributor_ids() in developer_insights.py — discovers external contributors by querying commits/PRs/reviews in workspace reposAdded hover tooltips with explanations across all insights pages.
Compare Page (`/insights/compare`):
<title> elementRadarDataPoint interface with optional desc fieldCustomAngleTick component in MetricsRadar.tsx for tooltip-enabled axis labelsRADAR_METRICS config includes desc for each metricExecutive Dashboard (`/insights/executive`):
Multiple insights pages displayed truncated UUIDs (e.g., 8f983e00-386...) instead of developer names.
devNameMap lookupdeveloper_name from APIdeveloper_name from APIdeveloper_name field to backend responses: compute_executive_summary(), estimate_sprint_capacity()ExecutiveSummaryResponse, SprintCapacityDeveloperFixed /insights/developers/[id] crashing on gaming flags section due to API schema mismatch.
{type, severity, description, evidence(object)} but frontend expected {pattern, severity: "low"|"medium"|"high", evidence: string}Record<string, unknown> type and proper field fallbacks (flag.type || flag.pattern, severity includes "warning")flag.pattern?.replace() to prevent TypeErrorFixed analytics_dashboard.py using stale CodeReview.pull_request_id column (renamed to pull_request_github_id).
CodeReview.pull_request_github_id == PullRequest.github_idconftest.py test fixture using the same stale field name_resolve_developer_for_pr() now auto-creates ghost Developer records (by GitHub login) when no existing developer matches, consistent with _resolve_developer_for_commit() behavior.
0.5.4 to 0.5.5from sqlalchemy import or_ to top-level import in developer_insights.pyComprehensive developer productivity analytics platform with AI-powered insights, alerting, and forecasting.
Backend:
DeveloperMetricsSnapshot, TeamMetricsSnapshot, InsightSettings, DeveloperWorkingSchedule, InsightAlertRule, InsightAlertHistory, InsightReportSchedule, SavedInsightDashboardapi/developer_insights.py - 25+ endpoints for individual developer metrics, team insights, leaderboard, executive summary, sprint capacity, bus factor, rotation impact, project insights, alert rules, and AI narrativesservices/developer_insights_service.py - Metric computation across 6 dimensions (velocity, efficiency, quality, sustainability, collaboration, sprint productivity), forecasting, gaming detection, health scoring, percentile rankings, role benchmarking, and executive summariesservices/insights_ai_service.py - LLM-powered narrative generation for team/developer performance, anomaly detection, root cause analysis, 1:1 prep notes, sprint retro insights, trajectory forecasting, team composition recommendations, and hiring timeline estimationcache/insights_cache.py - Redis caching with 5-min TTL, deterministic key generation, and pattern-based invalidationschemas/developer_insights.py - Complete Pydantic schemas for all metrics, responses, settings, and alertsmigrate_developer_insights.sql, migrate_developer_insights_v2.sql, migrate_developer_insights_v3.sqltests/integration/test_developer_insights_api.pytests/unit/test_developer_insights_service.pyMetrics Computed:
Advanced Features:
Alert System:
INSIGHT_ALERT_WARNING, INSIGHT_ALERT_CRITICALFrontend:
- /insights - Team overview with stat cards and workload distribution chart
- /insights/leaderboard - Ranked developer metrics
- /insights/developers/[developerId] - Individual developer drill-down
- /insights/compare - Side-by-side developer comparison
- /insights/allocations - Resource allocation view
- /insights/alerts - Alert management
- /insights/executive - Executive dashboard
- /insights/sprint-capacity - Sprint planning with capacity estimation
- /insights/ai - AI-powered insights (narratives, anomalies, recommendations)
- /insights/me - Personal insights
- /settings/insights - Insights configuration (working hours, metric weights, snapshot frequency)
useInsights hook - React Query integration with 10+ hooks for metrics, trends, leaderboard, alerts, and AI narrativesActivityHeatmap, MetricsRadarINSIGHTS with can_view_insights and can_manage_insightsinsights in app catalog with team_overview, leaderboard, and developer_drilldown modulesfull_access bundleteamInsights, developerInsights, insightsLeaderboard, workloadDistributioncelery_app.py) - all background processing now uses Temporal; celery_app set to None with deprecation warningget_celery_stats to get_temporal_stats)use_celery to use_backgrounddeveloper to user in auth hook (useAuth) - updated AppAccessGuard and SidebarGoogleIcon export from named to local function in landing page (moved to dedicated components/icons/GoogleIcon.tsx)formatRelativeTime utility function to lib/utils.ts0.5.3 to 0.5.4New top-level Compliance module for managing regulatory compliance, documents, reminders, training, and certifications.
New Routes:
/compliance - Compliance dashboard with overview stats, upcoming reminders, and category breakdown/compliance/reminders - Recurring compliance reminder management with list and calendar views/compliance/reminders/new - Multi-step reminder creation wizard (basic info, schedule, assignment, review)/compliance/reminders/[reminderId] - Reminder detail and instance history/compliance/reminders/calendar - Calendar view of upcoming reminder instances/compliance/reminders/compliance - Questionnaire import and analysis/compliance/documents - Document Center with folder tree, search, filtering, and upload/compliance/documents/[documentId] - Document detail with metadata, tags, and entity linking/compliance/training - Mandatory training management with assignment tracking/compliance/certifications - Certification tracking with developer enrollment and progress/compliance/calendar - Unified compliance calendarFull-featured recurring reminder engine for compliance tasks with escalation, assignment, and scheduling.
Backend:
Reminder, ReminderInstance, ReminderEscalation, ControlOwner, DomainTeamMapping, AssignmentRule, ReminderSuggestionapi/reminders.py - 30+ endpoints for reminders, instances, control owners, assignment rules, domain mappings, suggestions, dashboard stats, calendar, and bulk operationsservices/reminder_service.py - Reminder CRUD, instance generation, acknowledgment, completion, skip, reassignment, escalation, and dashboard statisticsschemas/reminder.py - Complete Pydantic schemas for all reminder operationsmigrate_reminders.sql - 7 tables with proper indexes, triggers, and constraintsTemporal Activities (temporal/activities/reminders.py):
generate_reminder_instances - Daily task to generate upcoming instances from recurrence rulescheck_overdue_reminders - Hourly check for overdue instances with automatic escalationsend_reminder_notifications - Sends due/upcoming reminder notificationssend_weekly_slack_summary - Weekly compliance status summary (logging only for now)check_evidence_freshness - Daily check for stale evidence on completed instancesFeatures:
Frontend:
useReminders hook - React Query integration with 10+ hooks for all reminder operationsReminderCard, ReminderInstanceCard, ReminderStatusBadge, ReminderPriorityBadge, ReminderCategoryBadge, InstanceStatusBadge, RecurrenceDisplayReminderCreationWizard - 4-step wizard with validation and team/owner assignmentImport compliance questionnaires from Excel/CSV with AI-powered column detection and automatic reminder generation.
Backend:
QuestionnaireResponse, QuestionnaireQuestion with status trackingapi/questionnaires.py - Upload, analyze, accept/reject suggestions, list responsesservices/questionnaire_service.py - 3-tier column detection (exact alias match, fuzzy substring, LLM fallback), cross-questionnaire deduplication, and automatic reminder suggestion generationmigrate_questionnaire.sql - Questionnaire tables with proper indexingFrontend:
useQuestionnaires hook - Upload, analysis, and suggestion managementUpload, organize, and manage compliance documents with folder hierarchy, tagging, and entity linking.
Backend:
ComplianceFolder, ComplianceDocument, ComplianceDocumentTag, ComplianceDocumentLinkapi/compliance_documents.py - Document CRUD, folder management, tag operations, entity linking, search with filteringservices/compliance_document_service.py - Document upload, folder tree management, tag operations, entity linkingmigrate_compliance_documents.sql - Document and folder tables with S3 key storageFrontend:
useComplianceDocuments hook - React Query integration for documents, folders, tags, and entity linksDocumentCard, FolderTree, CreateFolderModal, UploadModal, DocumentFilters, DocumentLinkPanelReplaced R2-specific storage with a generic S3-compatible StorageService supporting RustFS (dev) and any S3-compatible provider (production).
Backend:
services/storage_service.py - Generic S3 client with presigned URL generation, direct upload, multipart upload, and downloadr2_upload_service.py re-exports StorageService as R2UploadServiceS3_ENDPOINT_URL, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET_NAME, S3_REGION, S3_PUBLIC_ENDPOINT_URL, S3_RECORDINGS_PREFIX, S3_COMPLIANCE_PREFIX, COMPLIANCE_MAX_FILE_SIZE_MBDocker:
aexy-storage bucket on startup via rustfs-init helper containerCOMPLIANCE with can_view_compliance and can_manage_compliancecompliance in app catalog with reminders, document_center, training, and certifications modulespeople and full_access bundles, disabled in engineering and sales_marketingREMINDER_DUE, REMINDER_ACKNOWLEDGED, REMINDER_COMPLETED, REMINDER_ESCALATED, REMINDER_OVERDUE, REMINDER_ASSIGNEDcomplianceOverview, complianceDocumentsR2UploadService into generic StorageService with S3-compatible backend supportf4e79d9)73e7641)croniter>=2.0.0 for cron expression parsinggithub_app_install_url to production GitHub App URL in config.py instead of empty stringGITHUB_APP_INSTALL_URL environment variable to docker-compose.prod.yml backend serviceReplaced Celery 5.3+ task queue with Temporal Python SDK for all background processing, workflow orchestration, and scheduled tasks.
Infrastructure:
Activities & Workflows:
dispatch() function replacing Celery .delay() for fire-and-forget executionSingleActivityWorkflow wrapper for dispatching individual activitiesTask Queues:
analysis - Developer profiling, code analysis, LLM taskssync - GitHub sync, Google sync, external dataworkflows - CRM automations, workflow executionemail - Campaigns, onboarding, transactional emailintegrations - Webhooks, Slack, external servicesoperations - Stats aggregation, cleanup, maintenanceRetry Policies:
STANDARD_RETRY - General tasks with exponential backoffLLM_RETRY - AI/LLM calls with longer timeoutsWEBHOOK_RETRY - External webhook deliveryEmailCampaignService - 9 async methods for email campaign management, extracted from Celery tasksOnboardingService.check_due_steps() - Checks and dispatches due onboarding step processingOnboardingService API signaturesUpdateWarmingMetricsInput dataclass instead of raw dictMigrated automations from CRM-specific to a platform-wide automation framework accessible from /automations.
New Routes:
/automations - List all automations with module filtering (CRM, Tickets, Hiring, Email, etc.)/automations/new - Create new automation with module selector/automations/[automationId] - Edit automation with workflow builderModule Support:
record.created, record.updated, field.changed, stage.changedticket.created, ticket.status_changed, sla.breached, ticket.assignedcandidate.created, candidate.stage_changed, interview.scheduledcampaign.sent, email.opened, email.bouncedmonitor.down, incident.createdtask.status_changed, sprint.completedform.submittedbooking.confirmed, booking.cancelledBackend:
api/automations.py at /workspaces/{id}/automations/*schemas/automation.py with AutomationModule enumservices/automation_service.py for generic automation handlingmigrate_platform_automations.sql adds module column to automationsCRM Routes Redirected:
/crm/automations → /automations?module=crm/crm/automations/new → /automations/new?module=crm/crm/automations/[id] → /automations/[id]Agents can now have dedicated email addresses and manage their own inboxes.
Email Address Allocation:
support@workspace.aexy.emailAgent Inbox: frontend/src/app/(app)/agents/[agentId]/inbox/page.tsx
pending, processing, responded, escalated, archivedBackend:
models/agent_inbox.py - AgentInboxMessage for storing received emailsservices/agent_email_service.py - Email allocation, routing, and processingapi/email_webhooks.py - Inbound email webhook handlersmigrate_agent_email.sql - Agent email fields and inbox tableAgent Model Extensions:
email_address - Unique email address for the agentemail_enabled - Toggle email processingauto_reply_enabled - Enable automatic responsesemail_signature - Custom signature for outgoing emailsNew conversational interface for interacting with AI agents.
New Routes:
/agents/[agentId]/chat - Start new conversation with agent/agents/[agentId]/chat/[conversationId] - Continue existing conversationFeatures:
Backend:
migrate_agent_conversations.sql - Conversation and message tablesapi/agents.py with conversation endpointsuser, assistant, system, tool_call, tool_resultConnect AI agents to workflow automations for intelligent task handling.
New Model: models/automation_agent.py
AutomationAgent - Links agents to automation workflowsAutomationAgentExecution - Tracks agent executions within workflowsAutomationAgentConfig - Stores agent-specific workflow configurationNew API: api/automation_agents.py
POST /automations/{id}/agents - Add agent to automationDELETE /automations/{id}/agents/{agent_id} - Remove agentGET /automations/{id}/agents - List agents in automationPOST /automations/{id}/agents/{agent_id}/execute - Manually trigger agentWorkflow Actions: services/workflow_actions.py
run_agent action type for workflow nodesMigration: migrate_automation_agents.sql
Client for communicating with the mailagent microservice.
New Integration: integrations/mailagent_client.py
Configuration:
MAILAGENT_URL environment variable (default: http://mailagent:8001)Agent Detail Page: /agents/[agentId]
Agent Edit Page: /agents/[agentId]/edit
Agents List Page: /agents
/agents routes/automations routessend_email, create_draft, get_email_history, get_writing_styleSendingDomainResponse.provider_id is now optional (nullable)metadata → decision_metadata)LLMConfig export in mailagent LLM modulerun_migrations.py)roadmap_voting model and related codepublic_projects API (consolidated into projects API)A new standalone microservice for email administration, AI agent processing, and domain management.
Core Service: mailagent/
Email Provider Support: mailagent/src/mailagent/providers/
Domain Management: mailagent/src/mailagent/api/domains.py
Agent System: mailagent/src/mailagent/agents/
support, sales, scheduling, onboarding, recruiting, newsletter, customreply, forward, escalate, schedule, create_task, update_crm, wait, request_approvalLLM Integration: mailagent/src/mailagent/llm/
API Endpoints:
/api/v1/admin/* - Provider CRUD and dashboard/api/v1/domains/* - Domain management and verification/api/v1/onboarding/* - Inbox creation and verification/api/v1/agents/* - Agent CRUD and configuration/api/v1/agents/{id}/process - Process email with agent/api/v1/invocations/* - Execution history and metrics/api/v1/webhooks/* - Inbound email processing/api/v1/send/* - Outbound email sendingEmail Processing Pipeline:
A comprehensive interface for creating and managing custom AI agents with configurable tool access and behavior settings.
New Routes:
/agents - Agent list page with grid view, stats, filtering, and search/agents/new - Multi-step agent creation wizard/agents/[agentId] - Agent detail page with execution history and metrics/agents/[agentId]/edit - Tabbed configuration editorFrontend Components: frontend/src/components/agents/
AgentCreationWizard - 7-step wizard (type, basic info, LLM, tools, behavior, prompts, review)AgentTypeBadge - Type indicator with icon and colorAgentStatusBadge - Active/inactive statusToolSelector - Multi-select tool picker with categoriesLLMProviderSelector - Provider and model selection (Claude, Gemini, Ollama)ConfidenceSlider - 0-1 range slider for thresholdsWorkingHoursConfigPanel - Hours, timezone, and days configurationPromptEditor - System prompt editor with variable hintsDashboard Widget:
AIAgentsWidget - Shows active agents, total runs, success rateSidebar Navigation:
Product Page:
/products/ai-agents - Marketing page for AI Agents featureBackend API Extensions:
GET /agents/check-handle - Verify mention handle availabilityGET /agents/{id}/metrics - Agent performance metrics (runs, success rate, avg duration)Database Migration: backend/scripts/migrate_agent_extended_config.sql
mention_handle, llm_provider, temperature, max_tokens, confidence_threshold, require_approval_below, max_daily_responses, response_delay_minutes, working_hours, custom_instructions, escalation_email, escalation_slack_channelDocumentation:
/docs/ai-agents.md - Comprehensive guide covering agent types, configuration, tools, and API/docs/README.md with AI Agents in guides and products/CLAUDE.md with AI Agents key files and API testing commandscheck_auto_sync_integrations) runs every minute to check which integrations need syncinggmail_last_sync_at and calendar_last_sync_at for accurate schedulingDatabase Migrations:
migrate_auto_sync_interval.sql - Adds auto_sync_interval_minutes columnmigrate_auto_sync_calendar_interval.sql - Adds auto_sync_calendar_interval_minutes columntiptap-markdown integration for seamless markdown parsing/serializationisInitialized state guard in useWorkspace hookdir() check that always returned 0 for total integrations checkedto_emails field to properly extract email addresses from recipient objects--legacy-peer-deps for dependency compatibilitycontainer class for full-width layoutstiptap-markdown@^0.8.10y-prosemirror@^1.3.7/p/my-project-k3f9x2)- Overview, Backlog, Board, Stories, Bugs, Goals, Releases, Timeline, Roadmap, Sprints
Pagination component with ellipsis support and accessibility labelsRoadmapRequest, RoadmapVote, RoadmapComment for voting system/api/v1/public/projects/{public_slug}/... for unauthenticated accessbackend/src/aexy/core/sanitize.py)POST /workspaces/{id}/projects/{id}/toggle-visibility - Toggle project public/privateGET/PUT /workspaces/{id}/projects/{id}/public-tabs - Configure visible tabsGET /public/projects/{slug} - Get public project infoGET /public/projects/{slug}/backlog|board|stories|bugs|goals|releases|roadmap|sprints|timeline - Public data endpointsGET/POST /public/projects/{slug}/roadmap-requests - List/create feature requestsPOST /public/projects/{slug}/roadmap-requests/{id}/vote - Vote on requestsGET/POST /public/projects/{slug}/roadmap-requests/{id}/comments - CommentsProject model includes is_public (boolean) and public_slug (unique string) fieldsalembic/versions/61fd11a7e0ea_add_public_project_visibility.py - Adds visibility columnsscripts/migrate_roadmap_voting.sql - Creates roadmap voting tables with indexes`
47 files changed, ~5,900 insertions(+), ~500 deletions(-)
`
Backend:
api/public_projects.py (new - 903 lines)api/projects.py (+186 lines)models/roadmap_voting.py (new - 205 lines)models/project.py (+37 lines)schemas/project.py (+265 lines)core/sanitize.py (new - 107 lines)Frontend:
app/p/[publicSlug]/page.tsx (new - 265 lines)components/public-project-page/* (new - 12 components)components/ui/pagination.tsx (new - 136 lines)app/(app)/settings/projects/[projectId]/page.tsx (+254 lines)lib/api.ts (+351 lines)A comprehensive intelligence analysis system that extracts insights from GitHub activity to provide developer profiling, burnout detection, expertise tracking, and team collaboration analysis.
Semantic Commit Analysis:
! suffix and BREAKING CHANGE: footerNew Service: backend/src/aexy/services/commit_analyzer.py
POST /api/v1/intelligence/commits/analyzeGET /api/v1/intelligence/commits/distributionPR Review Quality Analysis:
New Service: backend/src/aexy/services/review_quality_analyzer.py
GET /api/v1/intelligence/reviews/qualityPOST /api/v1/intelligence/reviews/analyzeGET /api/v1/intelligence/reviews/response-timeExpertise Confidence Intervals:
New Service: backend/src/aexy/services/expertise_confidence.py
GET /api/v1/intelligence/expertisePOST /api/v1/intelligence/expertise/updateGET /api/v1/intelligence/team/{workspace_id}/expertise/{skill_name}Burnout Risk Indicators:
New Service: backend/src/aexy/services/burnout_detector.py
GET /api/v1/intelligence/burnoutPOST /api/v1/intelligence/burnout/updateGET /api/v1/intelligence/team/{workspace_id}/burnoutCollaboration Network Analysis:
New Service: backend/src/aexy/services/collaboration_network.py
GET /api/v1/intelligence/collaboratorsGET /api/v1/intelligence/team/{workspace_id}/collaborationGET /api/v1/intelligence/team/{workspace_id}/collaboration/graphProject Complexity Classification:
New Service: backend/src/aexy/services/complexity_classifier.py
GET /api/v1/intelligence/complexityPOST /api/v1/intelligence/complexity/analyzePOST /api/v1/intelligence/complexity/updateGET /api/v1/intelligence/team/{workspace_id}/complexityTechnology Evolution Tracking:
New Service: backend/src/aexy/services/technology_tracker.py
GET /api/v1/intelligence/technologyPOST /api/v1/intelligence/technology/updateGET /api/v1/intelligence/team/{workspace_id}/technologyFull Analysis Endpoint:
POST /api/v1/intelligence/analyze-all - Runs all analysis types in one callDatabase Migration:
backend/scripts/migrate_github_intelligence.sqlsemantic_analysis JSONB column to commits tablequality_metrics JSONB column to code_reviews tableexpertise_confidence JSONB column to developers tableburnout_indicators JSONB column to developers tablelast_intelligence_analysis_at timestamp to developers tablecomplexity_analysis JSONB column to pull_requests tabledeveloper_collaborations table for collaboration graph storageNew API Router:
backend/src/aexy/api/intelligence.py with 22 endpointsFixed an issue where Slack notifications were not being sent for uptime monitor incidents when the monitor didn't have a specific slack_channel_id configured.
Root Cause:
monitor.slack_channel_id to be set, but most monitors relied on the workspace's default Slack channel configurationslack_channel_configsChanges:
slack to notification_channels when creating new monitors if Slack is configured for the workspaceslack to existing monitors when a Slack channel is first configured for a workspaceCentralized Slack Integration Helpers:
backend/src/aexy/services/slack_helpers.py module with shared functions: - get_slack_integration_for_workspace() - finds integration by workspace/org ID
- get_slack_channel_config() - gets channel config for an integration
- get_workspace_notification_channel() - combines both to get channel ID
- check_slack_channel_configured() - boolean check for Slack setup
uptime_service.py and uptime_tasks.pyAdded Constants for Notification Channels:
NOTIFICATION_CHANNEL_SLACK = "slack"NOTIFICATION_CHANNEL_WEBHOOK = "webhook"NOTIFICATION_CHANNEL_TICKET = "ticket"Improved Type Safety:
db: AsyncSession) to notification helper functions_send_slack_notification()Better Exception Handling:
Exception catches to specific SQLAlchemyError for database operationsHTTPError handling for Slack API callsGraceful Error Handling:
add_slack_to_monitors() call in try/except to prevent channel configuration failures if monitor update failsFiles Changed:
backend/src/aexy/services/slack_helpers.py (new)backend/src/aexy/services/uptime_service.pybackend/src/aexy/processing/uptime_tasks.pybackend/src/aexy/api/slack.pyProvider Edit Modal:
Provider Card Improvements:
Provider Test Feedback:
Credential Encryption (Security):
secret_key via SHA256core/encryption.pyEmailProvider TypeScript interface with credentials, description, settings, and status fieldscredentials and description parametershas_credentials boolean field to provider API responses for secure credential status indicationhas_credentials flag indicates if configured--force flag not re-running changed migrationshas_credentials from API)DNS Records UI:
Provider Management:
- SES: checks for access_key_id and secret_access_key
- SendGrid: checks for api_key
- Mailgun: checks for api_key and domain
- Postmark: checks for server_token
- SMTP: checks for host
provider_id nullable in SendingDomain modelSET NULL on delete for provider foreign key relationshipdns_records, verification_token, and verified_at fields to SendingDomainListResponse schemaA comprehensive real-time proctoring system for assessment integrity with AI-powered face detection, violation tracking, and chunked video recording with cloud storage.
Face Detection & Monitoring:
Violation Tracking:
Screen & Webcam Recording:
Proctoring Settings:
Security Features:
Backend Proctoring Service:
ProctoringService for event logging and analysisR2 Upload Service:
Assessment Settings UI (Step 3):
enable_webcam, enable_screen_recording, enable_fullscreen_enforcementenable_face_detection, enable_tab_tracking, enable_copy_paste_detectionallow_calculator, allow_ideAssessment Review UI (Step 5):
New Files:
frontend/src/hooks/useChunkedRecording.ts - Chunked recording hookfrontend/src/services/recordingUploadService.ts - R2 upload servicefrontend/src/constants/index.ts - MAX_VIOLATION_COUNT constantfrontend/public/models/ - Face-api.js model filesbackend/src/aexy/services/proctoring_service.py - Proctoring event servicebackend/src/aexy/services/r2_upload_service.py - Cloudflare R2 integrationDependencies Added:
face-api.js - Browser-based face detectionMonitor Visibility Bug:
/monitors endpoint, but frontend expected { monitors: [], total } formatAPI Response Format Alignment:
monitors.list() - Now correctly handles array response from backendincidents.list() - Now correctly handles { items: [] } response formatmonitors.getChecks() - Now correctly handles { items: [] } response formatUnknown Status Handling:
unknown status support for newly created monitors (before first check runs)unknown to STATUS_COLORS in all uptime pages to prevent render crashesDEFAULT_STATUS_STYLE fallback for unrecognized status valuesNull-Safe Data Handling:
?.) when accessing API response properties|| []) for all list dataTypeError: Cannot read properties of undefined (reading 'length') errorsFiles Updated:
frontend/src/lib/uptime-api.ts - API response normalizationfrontend/src/app/(app)/uptime/page.tsx - Dashboard null safetyfrontend/src/app/(app)/uptime/monitors/page.tsx - Monitors list null safetyfrontend/src/app/(app)/uptime/monitors/[monitorId]/page.tsx - Monitor detail null safetyfrontend/src/app/(app)/uptime/incidents/page.tsx - Incidents list null safetyfrontend/src/app/(app)/uptime/incidents/[incidentId]/page.tsx - Incident detail null safetyfrontend/src/app/(app)/uptime/history/page.tsx - Check history null safetyA comprehensive uptime monitoring system for tracking HTTP endpoints, TCP ports, and WebSocket connections with automatic incident management and ticket creation.
Core Features:
Incident Management:
ongoing, acknowledged, resolvedHTTP Check Features:
TCP Check Features:
WebSocket Check Features:
Notification Channels:
Database Tables:
uptime_monitors - Monitor configurationsuptime_checks - Individual check results (time-series)uptime_incidents - Incident tracking with ticket integrationAPI Endpoints:
GET /workspaces/{id}/uptime/monitors - List monitorsPOST /workspaces/{id}/uptime/monitors - Create monitorGET /workspaces/{id}/uptime/monitors/{id} - Get monitor detailsPATCH /workspaces/{id}/uptime/monitors/{id} - Update monitorDELETE /workspaces/{id}/uptime/monitors/{id} - Delete monitorPOST /workspaces/{id}/uptime/monitors/{id}/pause - Pause monitoringPOST /workspaces/{id}/uptime/monitors/{id}/resume - Resume monitoringPOST /workspaces/{id}/uptime/monitors/{id}/test - Run immediate testGET /workspaces/{id}/uptime/monitors/{id}/checks - Check historyGET /workspaces/{id}/uptime/monitors/{id}/stats - Monitor statisticsGET /workspaces/{id}/uptime/incidents - List incidentsGET /workspaces/{id}/uptime/incidents/{id} - Get incident detailsPATCH /workspaces/{id}/uptime/incidents/{id} - Update incident notesPOST /workspaces/{id}/uptime/incidents/{id}/resolve - Manually resolvePOST /workspaces/{id}/uptime/incidents/{id}/acknowledge - Acknowledge incidentGET /workspaces/{id}/uptime/stats - Workspace-level statisticsFrontend Pages:
/uptime - Uptime dashboard with stats and overview/uptime/monitors - Monitors list with create modal/uptime/monitors/[id] - Monitor detail with stats, checks, and configuration/uptime/incidents - Incidents list with filtering/uptime/incidents/[id] - Incident detail with timeline and post-mortem notes/uptime/history - Check history viewerProduct Page:
/products/uptime - Marketing landing page for uptime monitoringCelery Background Tasks:
process_due_checks - Runs every minute, dispatches checks for due monitorsexecute_check - Performs individual HTTP/TCP/WebSocket checkssend_uptime_notification - Sends Slack and webhook notificationscleanup_old_checks - Daily cleanup of check history (keeps 30 days)Access Control Integration:
- Engineering bundle: Uptime enabled
- People bundle: Uptime disabled
- Business bundle: Uptime disabled
- Full Access bundle: Uptime enabled
can_view_uptimeStatistics & Metrics:
Extended the booking module with team scheduling capabilities.
All Hands Mode:
ALL_HANDS assignment type for team event typesRSVP System:
response_token for accepting/declining/rsvp/{token} for viewing booking details and respondingpending, confirmed, declinedTeam Calendar View:
/booking/team-calendarCustom Booking Links:
/book/{workspace} - Lists all public event types/book/{workspace}/{event}/team/{team}?members=id1,id2,id3New Database Table:
booking_attendees - Stores team meeting attendees with RSVP status and response tokensNew API Endpoints:
GET /booking/rsvp/{token} - Get booking details for RSVPPOST /booking/rsvp/{token}/respond - Submit RSVP response (accept/decline)GET /public/book/{workspace}/teams - List workspace teams for bookingGET /public/book/{workspace}/team/{team_id} - Get team info for booking pageGET /booking/calendars/callback/{provider} - OAuth callback endpointNew Frontend Pages:
/booking/team-calendar - Team availability calendar view/book/{workspace} - Public workspace landing page/book/{workspace}/{event}/team/{team} - Team-specific booking page/rsvp/{token} - Public RSVP response page/docs/booking.md/products/booking/docs/README.md to include booking in documentation index/docs/google.md with booking calendar callback URLsCalendar OAuth Flow:
Callback URL Change:
/api/v1/booking/calendars/callback/{provider}?success=true and ?error=... query paramsAn intelligent knowledge graph feature that automatically extracts entities from documentation and visualizes relationships in an interactive force-directed graph.
Core Features:
Entity Types:
Relationship Types:
mentions, related_to, depends_on, authored_by, implements, references, links_to, shares_entityBackend Components:
knowledge_entities, knowledge_entity_mentions, knowledge_relationships, knowledge_document_relationships, knowledge_extraction_jobs/workspaces/{id}/knowledge-graph/KnowledgeExtractionService, KnowledgeGraphServiceAPI Endpoints:
GET /graph - Full graph data with filtersGET /graph/document/{id} - Document-centric viewGET /graph/entity/{id} - Entity neighborhoodGET /entities - List/search entitiesGET /path - Find path between nodesGET /statistics - Graph statisticsGET /temporal - Timeline dataPOST /extract - Trigger extractionGET /jobs - Extraction job statusFrontend Components:
/docs/knowledge-graphTemporal Features:
Quality Metrics:
A comprehensive calendar booking system similar to Calendly, fully integrated into the Aexy ecosystem.
Core Features:
Backend Components:
EventType, Booking, UserAvailability, AvailabilityOverride, CalendarConnection, TeamEventMember, BookingWebhookBookingService, AvailabilityService, CalendarSyncService, BookingPaymentService, BookingNotificationServiceFrontend Pages:
/booking - Booking dashboard with stats, event types overview, and upcoming bookings/booking/event-types - List and manage event types/booking/event-types/new - Create new event type/booking/event-types/[id] - Edit existing event type/booking/availability - Weekly availability schedule editor/booking/calendars - Calendar connections managementPublic Booking Pages:
/public/book/[workspace]/[event] - Public event booking page with calendar picker/public/book/confirmation/[bookingId] - Booking confirmation page/public/book/cancel/[bookingId] - Booking cancellation page/public/book/reschedule/[bookingId] - Booking reschedule pageEvent Type Configuration:
Availability Features:
Calendar Integration:
Booking Management:
Access Control Integration:
- Engineering bundle: Booking disabled
- People bundle: Booking disabled
- Business bundle: Booking enabled with all modules
- Full Access bundle: Booking enabled with all modules
can_view_bookingBackground Tasks (Celery):
send_booking_reminders - Send reminder emails 24h and 1h before meetingssync_all_calendars - Periodic calendar synchronizationprocess_booking_webhooks - Dispatch webhooks to registered endpointscleanup_expired_pending_bookings - Cancel stale pending bookingsmark_completed_bookings - Auto-mark past bookings as completedgenerate_booking_analytics - Generate booking statisticsEnterprise Features (Planned):
Migration Runner Script:
backend/scripts/run_migrations.py for running SQL migrationsschema_migrations table with checksums--list, --dry-run, --file, --force, --database-url optionsTest Token Generator:
backend/scripts/generate_test_token.py for API testingThe foundational release of Aexy - a comprehensive Engineering OS platform for team management, performance tracking, hiring, and business operations.
Customizable Dashboards:
Daily Standups:
Work Logs:
Time Tracking:
Blockers:
Activity Patterns:
Sprint Management:
Task Management:
Task Types:
Sprint Metrics:
Retrospectives:
Task Templates:
GitHub Integration:
Review Cycles:
Individual Reviews:
Review Submissions:
Peer Review Requests:
Work Goals (SMART Framework):
Contribution Summaries:
Assessment Platform:
Question Types:
Question Configuration:
Assessment Settings:
Candidates:
Attempts & Proctoring:
Evaluation:
Question Analytics:
Objects & Attributes:
Records:
Record Lists:
Activities:
Automations:
Sequences & Campaigns:
Webhooks:
Templates:
Campaigns:
Recipient Tracking:
Email Tracking:
Analytics:
Subscriber Management:
Document Management:
Sharing & Permissions:
Collaboration:
Form Builder:
Form Features:
Analytics:
Learning Goals:
Approvals:
Budget Management:
Ticket Management:
Multi-Workspace:
Integrations:
Security & Compliance: