github Windshiftapp/core v0.9.3
Windshift v0.9.3

3 hours ago

Windshift 0.9.3

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 (the 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 —
    all 8045 keys, with placeholders and plural forms matching the English reference — and
    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).

Don't miss a new core release

NewReleases is sending notifications on new releases.