Last updated

Windshift 0.9.3

Released 4 November 2026. Published as tag v0.9.3 on GitHub and as ghcr.io/windshiftapp/windshift:v0.9.3. Binaries and the macOS app are on the download page.

Windshift 0.9.3 turns milestones into real planning hubs and makes the hierarchy views usable at scale. A milestone now carries a Markdown description, a comment thread, linked pages, and an Activity feed, and the global milestones page lists everything you can see with filters and an Updated column. The tree, map, and roadmap share one lazy loader, so you can work at a chosen hierarchy level and expand only what you need. On the plugin side, host functions and plugin routes are now default-deny, and a new editor extension point lets a plugin replace the Markdown editor. Alongside those, the release preserves Jira issue keys through import, stops imports from triggering live side effects, bounds a long list of read paths, and fixes a batch of AI, email, and time-tracking bugs.

Important upgrade notice: if you run plugins, read the upgrade notes. Plugins must now declare the host functions they use, and plugin routes require a session unless they opt into public access. It is also advised to update the ws CLI alongside this release.

Highlights

Milestone planning hubs

A milestone is no longer just a target date. It has a Markdown description, a comment thread, linked pages, and an Activity tab that merges milestone changes, comments, and the status and comment activity of the items in it. The global milestones page lists every milestone the viewer can see (global plus each accessible workspace) with name, workspace, and status filters, a Created and an Updated column, and completed or cancelled milestones hidden by default. The list opens in manual order and can sort by the latest activity.

Activity is cheap to sort because each milestone stores last_activity_at, set in the same transaction as any activity write (description, status, target date, page links, comments, and item membership or status changes). Moving an item to another workspace now keeps its global milestones and drops only the old workspace's; the move preview says which are kept.

The API adds milestone comment, page-link, and activity endpoints, and the CLI gains ws milestone comment list|add|edit|delete and ws milestone page list|add|remove. Milestone comments and page links are audited, and comment permissions follow the item comment rules: local milestones use their workspace's item.comment and comment.edit_others, while global milestones let any viewer comment and only authors edit or delete their own. Thanks to pienter-tech for this feature (#382).

Hierarchy views work at scale

The tree, map, and roadmap now share one lazy hierarchy loader. A level picker chooses the hierarchy level to show, with a filtered count per level and a sensible default, and the base page loads at that level while subtrees load on expand. Each root pages with its own offset, so a large subtree offers a per-row "Load more" instead of a single truncated page. When a filter hides an intermediate parent, the matching child appears under its nearest visible ancestor with a parent reference chip (GH #271) rather than a synthetic row. The roadmap tree panel also gains expand and collapse all (GH #397). On the backend this adds include_has_children on the item list, GET /items/level-counts, and POST /items/hierarchy.

Plugins are default-deny

Plugin host functions were previously registered for every plugin, so a plugin that asked for little still received process execution, outbound HTTP and SMTP, KV storage, comments, and SCM access. A plugin manifest now declares the permissions it needs from a closed catalog (process.exec, network.egress, mail.send, storage.kv, comments.write, scm.write), and anything it does not declare is denied. log stays available to every plugin, an unknown permission name fails the load instead of silently disabling a host function, and when license enforcement is configured the effective set is the declared permissions and a valid license. The admin plugin list reports requested versus effective permissions, and Module settings shows a badge per permission (#421).

Plugin routes had the mirror problem: they mounted on the raw mux with no authentication, authorization, CSRF, rate limit, or body cap, so every enabled route was public. A route in the manifest now declares its auth (session by default, admin, or none), and the router applies the matching chain, including a body cap and an IP-keyed limiter for public routes. Public routes are additionally gated behind PLUGIN_PUBLIC_ROUTES, which defaults to off. Plugin handlers receive an authoritative principal, and the admin list shows each route's auth level.

Bring your own Markdown editor

A new editor.markdown extension point lets an installed plugin provide its own editor for an item description or a knowledge page. A provider declares whether it toggles beside the native editor or replaces it (the manifest also accepts comments as an entity type, though no surface is wired to it yet). The host stays the only writer: it hands the plugin the current Markdown, persists saves through the existing API, confirms before discarding unsaved edits, and refuses a save over 256 KB. The native Milkdown editor is untouched when no provider is installed, and the opt-in toggle only appears when an enabled plugin offers one. The plugin itself ships from a separate repository (#444, #445).

Also in 0.9.3

  • Italian joins the language picker. The frontend now ships a complete Italian (it) catalog with all 8045 keys, placeholders, and plural forms matching the English reference, and it negotiates it-* browsers automatically. Thanks to @m0nt0 (#449).
  • Jira issue keys survive import. An imported Jira issue keeps the numeric suffix of its source key, so existing commit and PR references to PROJECT-123 keep resolving after a project import (GH #416). If the number is already taken or retired in the target workspace, the importer falls back to the normal allocator and records a fidelity finding.
  • Deleted item keys are retired. Deleting the highest-numbered item in a workspace no longer lets the next create reissue a key that commits and PRs already reference; the number is reserved in the same transaction as the delete (WI-1958).
  • Loop transitions are real transitions. Jira teams that use same-status loop transitions no longer lose them on import (WI-1954), and a new status_loop action trigger fires when a declared self-transition runs, with optional status and item-type filters. Undeclared same-status updates stay plain no-ops (WI-1960).
  • Email intake and replies are easier to clear. A stuck intake receipt can be discarded by its outbox row from the Processing Log (GH #371), and a channel's customer-reply tab offers "Discard all pending (N)" to cancel a burst in one action while leaving in-flight deliveries alone (WI-1967).
  • Imports never drive live side effects. Imported comments, items, and assets are stamped with their origin, and the durable email, notification, item/asset action, and logbook consumers skip them, so a Jira Service Management import can no longer queue one customer email per imported agent comment (GH #439).
  • Completed iterations leave the backlog. Terminal (completed and cancelled) iteration sections are hidden by default, with a per-view "Show completed iterations" toggle, and closed sections reject drag-and-drop (GH #448).
  • Workspace lookups stop fanning out. GET /workspaces accepts an ids filter, so a channel with many allowed workspaces resolves its picker in one authorized read instead of one request per id (#431).
  • Create-form pickers follow the workspace. The assignee and user custom-field pickers reload when the workspace scope changes, so Editors and Viewers no longer see a denied, empty roster on a reopened create form (WI-1961).
  • Time tracking survives customer changes. A worklog's customer is derived from its project instead of stored on the row, so deleting a customer organisation no longer deletes time logs for an unlinked project (GH #403), and the time customers view surfaces its delete guard error instead of swallowing it (GH #402).
  • The tree reads correctly. One guide line is drawn per ancestor level and the parent chip sits after the type icon, so nesting no longer appears to run backwards (WI-1955).
  • A bound sort without a direction is ignored. A table given { key: 'name' } no longer shows a descending arrow while leaving rows unsorted; the next click starts ascending.
  • Settings pages share one scaffold. The catalog, status, role, group, and notification managers moved onto a common EntityManagerPage, with shared inline editor and detail-shell scaffolds, so the settings surfaces behave and look consistent (WI-1940; #411, #413, #414, #415, #417, #418).
  • Shared frontend building blocks. The work-item list row and pagination footer, edge-drop and rank recovery, progress detail, the set detail shell, mobile draft handling, the attachment lightbox, and the inline editor scaffold were extracted into shared pieces, and the remaining Label+Input sites moved to TextField (#401, #404, #405, #406, #407, #408, #409, #410, #412).
  • One refresh reconciliation. The global refresh-work-items broadcast now has a single listener that coalesces a burst and notifies each subscriber once, instead of seven independent listeners starting unrelated reads (WI-1924; #435).

Performance

  • N+1 read paths are batched. Six paths flagged in the backend performance review now issue one query per batch instead of one per row: example results, asset type fields, dependency workspace lookups, intra-set workflow conflicts, hierarchy root resolution (now a single recursive CTE), and multi-user agent cache invalidation (#398).
  • Permission snapshots load memberships and roles once. A non-admin snapshot no longer loads group memberships and role assignments twice per build (#399).
  • Filtered item totals are bounded. Every filtered, collection, QL, and search list wraps its count in a LIMIT 10000, matching the client's existing page cap, so the aggregate stops scanning all matching rows (#422).
  • Optional editors are split into named chunks. Excalidraw, mermaid, and cytoscape each get a stable named chunk with an entry guard, so a deploy that touches one no longer invalidates a shared file (#436).
  • Hot-path allocations are cleaned up. Hierarchy and permission services reuse a single item repository, the item count cache is count-bounded with a tombstone that cannot store a stale count, and the SQLite read and write pools get the same connection lifetimes as PostgreSQL (#437).
  • The effective-project cache is wired and bounded. Item-detail reads no longer run the recursive project CTE on every request; the cache moved off BigCache onto the shared typed cache with per-entry TTL and a diagnostics registration, and warm-up actually populates it (#443).
  • Attachment uploads are bounded. A shared pool of three replaces five separate upload loops, including an unbounded editor drop path, so a large batch no longer competes with foreground reads for the per-user concurrency budget. A failed file is recorded instead of aborting the batch (#430).

Security

  • Plugin host functions are default-deny and driven by a closed permission catalog, with license enforcement layered on top (#421).
  • Plugin routes authenticate by default and carry CSRF, rate-limit, and body-cap protection; public routes require an explicit opt-in.
  • Plugin construction now fails loudly if a host function has no permission mapping, instead of shipping it callable.
  • Milestone comment and page-link create, update, and delete operations are audited.
  • Imports are marked as historical at the fact level, so no import path can queue or send a live customer side effect.

Bug fixes

  • AI failures are visible. Chat and agent-run failures now log the wrapped provider error, persist it on the run, and return it to system admins instead of a generic 503, and the connection test sends a real chat-shaped request so it fails when chat would (GH #441).
  • Content-filter responses are classified correctly. Providers that spell the finish reason content-filter no longer fall through to done or empty and skip the review warning (#423).
  • Anthropic thinking requests drop sampling parameters. temperature and top_p are cleared whenever thinking is enabled, including the effort path and an operator override, so the provider no longer rejects the request (#424).
  • Unknown models get a conservative output cap. The fallback no longer exceeds the smallest catalogued model's limit; operators can raise it per connection (#426).
  • Transient HTTP reads are not cached. A 408, 429, or 5xx tool read is retried instead of being suppressed as a duplicate and stopping the loop (#427).
  • Agent model changes are atomic. Instructions, connection, and runner image are written in one version transition, and activation is guarded on the validated version, so a concurrent change can no longer leave an unvalidated model Ready or record a different model than the one submitted (#428, #429).
  • Pre-4.7 Claude models are gone from the default catalog. They reject the adaptive-thinking request the coding agent sends; the Anthropic and OpenRouter lists now use 4.7+ models (#425).
  • A missing parent answers a validation error. Creating an item with a non-existent parent_id now returns the 400 parent_id contract on both v1 and v2 instead of a 404 or 500.
  • Hierarchy load-more is correct. A root's later pages report has_more from the per-root offset, so the tree no longer offers an extra empty page that resets the total.
  • An omitted item type is resolved before hierarchy validation, so ws task create --parent without a type can no longer parent the workspace's default top-level type under anything.
  • Milestone edits keep their category, and denied page-link changes answer 404 like other links; milestone timestamps and history follow the same user rules as the rest of the app.
  • Jira workflow loop transitions import, and the readiness finding that claimed they were no-ops is gone.

Upgrade notes

  • Plugins must declare host-function permissions. This is a breaking change: a plugin that declares no permissions now gets only log. It keeps loading, but denied calls fail and the server logs a warning naming the plugin and the host functions it can no longer call. Add the missing permissions to the plugin manifest. capabilities is unchanged and still advertises product features such as sso.multi-provider.
  • Plugin routes require a session by default. A route that should be reachable without a session must set auth: none and requires PLUGIN_PUBLIC_ROUTES to be enabled; otherwise it is unreachable. Routes that should be admin-only set auth: admin. Two routes that can match the same request with different auth fail the load.
  • Pre-4.7 Claude models were removed from the default catalog. If a connection referenced one, select a 4.7+ model. An operator can still configure a model explicitly per connection.
  • Schema migrations run automatically. This release adds milestone_comments, milestone_history, and milestones.last_activity_at, backfills NULL time-project customers to the no-customer placeholder, and drops the denormalised time_worklogs.customer_id (SQLite table rebuild, PostgreSQL DROP COLUMN). No configuration change or re-login is required.
  • PostgreSQL 15 or newer is required. Startup refuses an older server with a clear message instead of failing later on schema features it lacks. SQLite deployments are unchanged.
  • The backlog hides completed iterations by default. Toggle "Show completed iterations" to bring them back; the choice is stored per view in your user preferences.

Thank you

Thank you to pienter-tech (@pienter-tech) for the milestone planning hubs: the Markdown descriptions, comments, page links, Activity feed, and global milestone list (#382).

Thank you to Pietro Montorfano (@m0nt0) for the Italian locale (#449).