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
negotiatesit-*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 toPROJECT-123keep 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 newstatus_loopaction 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 /workspacesaccepts anidsfilter, 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 commonEntityManagerPage, 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 toTextField(#401,
#404,
#405,
#406,
#407,
#408,
#409,
#410,
#412). - One refresh reconciliation. The global
refresh-work-itemsbroadcast 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 aLIMIT 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-filterno longer fall through todoneoremptyand skip the review warning
(#423). - Anthropic thinking requests drop sampling parameters.
temperatureandtop_pare 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_idnow returns the 400parent_idcontract on both v1 and v2 instead of a 404 or 500. - Hierarchy load-more is correct. A root's later pages report
has_morefrom 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 onlylog. 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.capabilitiesis unchanged and still advertises
product features such assso.multi-provider. - Plugin routes require a session by default. A route that should be reachable without a
session must setauth: noneand requiresPLUGIN_PUBLIC_ROUTESto be enabled; otherwise it
is unreachable. Routes that should be admin-only setauth: 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, andmilestones.last_activity_at, backfills NULL time-project customers to
the no-customer placeholder, and drops the denormalisedtime_worklogs.customer_id(SQLite
table rebuild, PostgreSQLDROP 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).