mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
d276991c603e6041d20fb5c1e65df8756d35d2ae
61
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f01eb5a06c |
MUL-6928 perf(issues): index scalar property contains behind a bigram prefilter (#7878)
* MUL-6928 perf(issues): index scalar property contains behind a bigram prefilter
Scalar `contains` property filtering runs `(properties ->> def) ILIKE
'%needle%'`, which no index can serve, so it degrades to a workspace-wide
sequential scan. At 1M issues that costs ~8x the throughput of an equality
filter, and the pgxpool connections it holds queue every other tenant's cheap
query behind it.
Put a redundant, indexable prefilter in front of the per-key check:
`LOWER(properties::text) LIKE LOWER('%needle%')`, served by a new pg_bigm GIN
index on the same expression. The per-key ILIKE still decides the result; the
prefilter only narrows the candidate set. On a 500k-row pg_trgm proxy a
selective needle goes from a 183ms Parallel Seq Scan to a 1.4ms Bitmap Index
Scan.
The prefilter is skipped, falling back to today's behaviour, for needles it
cannot screen soundly or usefully: jsonb escapes `"`, `\` and control
characters on serialization, so those never appear literally in
properties::text, and a single character carries no bigram to narrow with.
Migration 446 cannot use the DO ... EXCEPTION guard migrations 032 / 036 use
for pg_bigm — a concurrent build cannot run inside one — so the runner gates it
on the operator class instead: databases without pg_bigm record the version
with its SQL skipped and keep the unaccelerated (still correct) filter.
cmd/migrate/README.md documents building it out of band afterwards.
Co-authored-by: multica-agent <[email protected]>
* MUL-6928 fix(issues): prefilter short contains needles too
The single-character skip rule was derived from the pg_trgm proxy used to
develop this change, where a 1-character needle can only drive a sequential or
full-index scan. pg_bigm is different — indexing 1- and 2-character keywords is
the capability it exists for over pg_trgm — so the rule denied the target
extension exactly the searches it was chosen to serve, and a single CJK
character is already a word.
Measured on PostgreSQL 16.15 with pg_bigm 1.2 over 1M issues: a rare
1-character CJK needle goes from a 134ms Parallel Seq Scan to a 1.2ms Bitmap
Index Scan (8.3 -> 10527 tps at 4 clients). Only the JSON-escaping guard
remains, which is about soundness rather than benefit.
Co-authored-by: multica-agent <[email protected]>
* MUL-6928 fix(issues): analyze the properties expression index after building it
Building an expression index does not collect statistics for its expression.
Until they exist the planner falls back to a pattern-length heuristic that
estimates any single-character LIKE at a few percent of the table, so it cannot
tell a needle matching 50 rows from one matching 50,000 — and migration 446's
index ends up built, valid, ready, and never chosen. Autovacuum does not rescue
a large, quiet issue table promptly: its default threshold is 10% of rows
changed.
Measured on PostgreSQL 16.15 with pg_bigm 1.2 over 1M issues, with
max_parallel_workers_per_gather=4: a rare single-character CJK needle plans as a
Parallel Seq Scan at ~101ms with 446 alone — indistinguishable from the
pre-change baseline — and as a 0.64ms Bitmap Index Scan once the statistics
exist. Whether the wrong estimate actually flips the plan depends on how cheap a
parallel scan looks, so without this the shipped behaviour would vary by
instance sizing.
447 is deliberately not gated on pg_bigm the way 446 is: ANALYZE is valid, cheap
(557ms on 1M rows) and side-effect-free everywhere, and refreshing ordinary
column statistics is useful even where the expression index was skipped.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
4fe6586b32 |
MUL-6790 feat(issues): support operator matching in scalar property filters (#7765)
* feat(issues): support operator matching in scalar property filters Closes #7692. Scalar custom-property filters (text / number / date / url) previously matched by exact equality only, so a text fragment or a numeric range returned an empty list. A property filter member is now either a plain string (exact equality — the unchanged default that keeps every existing saved view, URL, and API call meaningful) or an operator object: - contains — case-insensitive substring (text / url) - gt / gte / lt / lte — numeric comparison (number) - before / after — lexicographic comparison of the stored "YYYY-MM-DD" strings (date) Backend: parsePropertiesFilterParam accepts {op, value} members and compiles them to operator patterns; propertiesFilterPredicate renders ILIKE / numeric / date-string comparisons and the static ListOpenIssues unroll in issue.sql mirrors them. contains values are LIKE-escaped at parse time and ops are fully validated before compilation, so the SQL branches only ever see legal shapes. The table-filters request carries members as raw JSON so both shapes survive the round-trip, and the query fingerprint canonicalizes them byte-exactly. Frontend: the filter menu grows an operator row above the scalar value input ("is" is the default and still commits a bare string), the client matcher in issueMatchesPropertyFilters mirrors the server predicates, and chips summarize operator members with per-locale phrases or math symbols. Operator members ride in saved-view baselines and chip deltas through a canonical propertyFilterValueKey. Verified: full Go suite (race) and the internal/handler property, facet, and issue-table tests; new unit + DB-backed operator tests on both tiers; pnpm typecheck and the core/views Vitest suites including new operator matrix, menu, and saved-view baseline tests. * test(issues): cover operator members through the table-rows endpoint The properties filter serves through three paths — the paginated list, the open_only static unroll, and the table rows/facets compile — but only the first two had operator-member coverage. The table path has PR-specific code (raw-JSON members in issueTableFiltersRequest and the byte-exact sortedUniqueRawJSON fingerprint) that no test exercised. Drive ListIssueTableRows with operator members ANDed across two definitions so the decode → fingerprint → parsePropertiesFilterParam chain is pinned like the other paths. * fix(issues): canonicalize numeric operator bounds and align empty-needle contains Adversarial review findings: - strconv.ParseFloat accepts forms Postgres ::numeric rejects ("0x1p4" hex-float, "1_000" underscores on older PG). The dynamic predicate binds the parsed float and succeeds while the static open_only unroll casts the raw string inside SQL and 500s — same filter, two list endpoints, different outcomes. Canonicalize the compiled pattern value with strconv.FormatFloat(num, 'f', -1, 64) at validation time; the dynamic re-parse and the static cast now see plain decimals. Regression tests cover the hex-float bound on both serving paths. - issueValueMatchesOperator treated an empty contains needle as match-all ("".includes("") is true) while the server rejects empty operator values — a hand-edited saved-view blob could match every issue client-side. The matcher now refuses empty needles, and the test asserts it against a SET value (the unset case short-circuits before the operator runs and could never catch this). - Server e2e: pin unset-key exclusion for contains / before / after on both serving paths via the seeded empty issue. * fix(issues): align scalar property operator semantics * test(issues): cover boolean contains mismatch * fix(i18n): use exclusive Japanese wording for date before/after filters The `before` / `after` operators compare strictly (`<` / `>` on both the server predicate and the client matcher), so a date equal to the bound is never a match. Japanese 以前 / 以後 mean "on or before" / "on or after" — they include the reference date, so a user filtering 「2026-03-01 以前」 reasonably expects March 1 in the results and does not get it. より前 / より後 carry the exclusive meaning the implementation actually has. English "before"/"after" and Chinese 早于/晚于 are already exclusive and are left unchanged. Korean 이전 / 이후 share the same Sino root and are inclusive by dictionary definition, but they are also the conventional rendering for before/after filters in Korean software — left as-is pending a native speaker's call rather than guessed at. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
db86c75e12 |
MUL-6878: avoid per-row issue status resolution (#7814)
* perf(server): avoid per-row issue status resolution Co-authored-by: multica-agent <[email protected]> * fix(server): address issue status query review nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5fa65bd125 |
feat(billing): enforce Cloud issue count limits (#7619)
* feat(billing): enforce Cloud issue count limits * fix(billing): address issue limit review feedback * test(billing): exercise validation as manager * fix(billing): resolve second review feedback * fix(core): preserve entitlement limit discrimination |
||
|
|
8c563b49be |
MUL-6555 feat(issues): preserve source context for sub-issues (#7461)
* feat(issues): preserve source context for sub-issues * fix(issues): address source context review findings * fix(migrations): guard source context rollback preflight |
||
|
|
8ade96e671 |
MUL-6584: establish Public API v1 foundation (#7450)
* feat(api): establish public v1 contract foundation Co-authored-by: multica-agent <[email protected]> * fix(plugins): close public API contract gaps Co-authored-by: multica-agent <[email protected]> * fix(issues): enforce revision guard during update Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
aa4c504c78 |
fix(issues): re-rank an issue when its status moves it to a new column (#7422)
position ranks an issue inside its own (workspace, status) column, but a status change carried the old column's value over unchanged. The value that put an issue on top of Todo is roughly zero, and lands it below every hand-dragged issue in Done — so agent-completed work sank into the middle of a long Done column and read as "the task disappeared". Re-rank to the top of the destination column on both status-write paths, reusing the MIN(position) - 1 policy new issues already get. An explicit position still wins, so cross-column drag-and-drop keeps the slot it dropped on, and a same-status write leaves position alone. The re-rank reads MIN(position) for the destination (workspace, status), which idx_issue_status could only serve with a heap fetch per issue in the column — paid on every completion against the column that grows without bound. Add (workspace_id, status, position) to make it an index-only min. Closes #7410 Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b4bfe083be |
MUL-6407: mint UUIDv7 primary keys for append-heavy tables (#7230)
* perf(db): mint UUIDv7 primary keys for the append-heavy tables
Random v4 primary keys scatter every insert across the whole primary-key
B-tree, so each row dirties a different page. On the tables that grow
continuously — the task queue, messages, comments, activity and audit rows —
that is pure write amplification. UUIDv7 puts a big-endian millisecond
timestamp in the leading 48 bits, so consecutive inserts share a page.
The 12 tables converted here are the append-only hot paths:
agent_task_queue, activity_log, inbox_item, comment, issue, chat_session,
chat_message, autopilot_run, webhook_delivery, task_token,
channel_inbound_audit, sys_cron_executions
Ids come from the new pkg/dbid, which returns a pgtype.UUID ready to assign to
a sqlc params struct. Upsert-dominated tables (task_usage, agent_runtime, ...)
are deliberately left alone: v7 only buys locality for rows that are actually
inserted. Lease and claim tokens keep gen_random_uuid() — they must stay
unpredictable and carry no timestamp.
No migration, and no backfill. Every converted INSERT binds its id as
COALESCE(sqlc.narg('id')::uuid, gen_random_uuid()), so the column default stays
reachable: a caller that passes no id inserts exactly as it does today. Tables
will hold a mix of v4 and v7 ids permanently, which the uuid type and every
parser in the repo already accept.
Verification: - pkg/dbid: version/variant, uniqueness, and a non-decreasing timestamp
prefix; a shape test asserting every converted query keeps the COALESCE
fallback; and a source scan asserting every production params literal mints
an id, so a call site added later cannot silently fall back to v4.
- internal/handler: round-trips issue, comment, activity_log, inbox_item,
chat_session, chat_message, agent_task_queue, task_token and
channel_inbound_audit against Postgres and asserts the stored id is v7,
plus one case proving an unset id still inserts (as v4).
- internal/scheduler: covers the one hand-written INSERT, asserting the
execution id is v7 while lease_token stays v4.
Co-authored-by: multica-agent <[email protected]>
* fix(db): address UUIDv7 review feedback
Co-authored-by: multica-agent <[email protected]>
* fix(db): cover Telegram audit UUIDv7
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
66af64b9a6 |
feat(issues): support filtering custom properties by "no value" (#7156)
Add a "No value" (__none__) option to the custom-property filter for
checkbox/select/multi_select/actor properties. Selecting it matches issues
where the property is unset — the inverse of picking a value — so teams can
express "everything that is NOT parked", e.g. exclude a `挂起` Hold checkbox.
Backend:
- parsePropertiesFilterParam compiles "__none__" into a
{"__none__": "<defId>"} marker; propertiesFilterPredicate renders it as a
key-absence disjunction (NOT (i.properties ? $n)) instead of jsonb
containment. The static ListOpenIssues unroll recognizes the same marker.
- Property table facets report an unset "__none__" bucket so the filter menu
shows a real count for the option.
Frontend:
- issueMatchesPropertyFilters matches unset issues when "__none__" is selected
(OR-ed with other values within the definition, AND-ed across definitions).
- PropertyFilterOptions offers a "No value" option for every property type;
filter chips render it via pickers.custom_property.no_value_label.
- i18n keys added for en/zh-Hans/ja/ko.
Co-authored-by: deo <deo@deo的macbook\032pro.tyaio>
|
||
|
|
5d5fe64672 |
MUL-6343: Add semantic issue activity timestamps (#7193)
* feat: add semantic issue activity timestamps Co-authored-by: multica-agent <[email protected]> * fix(migrations): register issue activity index cleanup Co-authored-by: multica-agent <[email protected]> * fix(issues): address last activity review Co-authored-by: multica-agent <[email protected]> * fix(issues): close last activity review gaps Co-authored-by: multica-agent <[email protected]> * fix(issues): harden activity rollout safeguards Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4b8873df3a |
feat(issues): add revision-aware concurrency guards (#7038)
* feat(issues): add revision-aware concurrency guards * fix(concurrency): decouple text edits from aggregate revisions |
||
|
|
446080bd57 |
MUL-6243 feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (#7065)
* feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (MUL-6243) Co-authored-by: multica-agent <[email protected]> * fix(issue-status): address review — normalize all status consumers, close archive race, gate rollout, split PK index Review round 1 (MUL-6243 / PR #7065): 1. Behavior normalization was incomplete. Adds issue_effective_status() as a SQL mirror of issuestatus.Effective so the six SQL consumers the Go resolver cannot reach (duplicate guard, open-issue list, sub-issue progress, project stats, completed-inbox archive, open_only) resolve categories too. Wires the resolver into the task-failure sweeper and the stage-barrier helpers. 2. Archive TOCTOU. Archive now runs its in-use census and the archive itself in one transaction under an exclusive catalog lock; writes that target a custom status take the shared side. Built-in writes stay lock-free — a built-in can never be archived. 3. Rollout safety. Creating a custom status is gated behind the default-off CustomIssueStatuses flag, so a value old pods cannot interpret cannot exist until the fleet is updated. Readers ship ungated and are safe. 4. Primary key index. Follows the repo convention (327-329): table without a PK, unique index built CONCURRENTLY in its own migration, attached via PRIMARY KEY USING INDEX, and registered for invalid-index cleanup. 5. Status normalization vs stored value. Write paths now persist the canonical key from Resolve, so 'HUMAN_REVIEW' no longer validates and then fails the column format constraint as a 500. Also fixes the two CLI tests failing in CI: they asserted the pre-MUL-6243 contract that an unknown-but-well-formed status is rejected locally. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): close the archive race in both orderings; resolve categories in batch barrier and CLI progress Review round 2 (MUL-6243 / PR #7065): 1. The archive race was only half closed. Handlers resolved the status BEFORE opening a transaction, so an archive could commit in that window and the write would still land on an archived key. assertIssueStatusStillActive now takes the shared catalog lock and RE-RESOLVES inside the caller's transaction, which is what actually makes the status provably active at write time. Applied to create, update, batch, and both description-merge branches, which previously bypassed the guard entirely. A losing writer gets 409 rather than a silent partial update. 2. The batch stage-barrier prefilter still compared literals, so a batch that moved the last child onto a custom done status left childDoneCompleted empty and skipped the parent notification. It now resolves categories first. 3. multica issue children --output json counted only literal done/cancelled. ListChildIssues now emits status_category and the CLI reads it, falling back to status for older backends. TestArchiveCommitsInsideTheWriteRaceWindow interleaves the two operations so the writer validates against an active status and the archive commits before the write. Verified it fails without the re-resolve (issue stranded on the archived status) and passes with it. Co-authored-by: multica-agent <[email protected]> * refactor(issue-status): address review nits — honest race-test naming, one catalog read per list Review round 3 nits (MUL-6243 / PR #7065): 1. TestArchiveRaceBothOrderings claimed more than it proved. Its 'archive wins' cases archived first and only then sent the request, which the pre-flight check alone rejects. Split into TestWritesRejectAnArchivedStatus (the sequential case, named for what it is) and TestArchiveRefusesAfterACommitted Write, and extended TestArchiveCommitsInsideTheWriteRaceWindow — the only test that truly interleaves archive between pre-flight and write — from create alone to all five write endpoints. Re-verified by deleting the in-transaction re-resolve: 4 of 5 fail with issues stranded on the archived status (create is guarded in the service layer and checked separately). 2. ListChildIssues / ListChildrenByParents issued one category lookup per custom-status child. issuestatus.Resolver now amortizes that to a single catalog read per request, lazily on the first custom key, so built-in statuses still cost zero queries however long the list is. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d09019cd95 |
MUL-5839: preserve media in /issue descriptions (#6536)
* fix(channel): preserve images in issue descriptions * fix(views): hide channel-media provenance in board card previews descriptionPreview strips image Markdown but not HTML comments, so a `/issue` description whose media was materialized rendered its provenance marker as the card's visible preview text. cardProperties.description defaults to true, so this showed on the default board view for every issue created from a channel message carrying an image. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
24d209794e | fix(channel): attach /issue media to created issues (#6388) | ||
|
|
581d9527ba |
feat(vcs): self-hosted Git providers (Forgejo, Gitea, GitLab) alongside GitHub (MUL-3772) (#5006)
Adds self-hosted Git provider support (Forgejo, Gitea, GitLab) alongside GitHub: per-workspace token connection, a provider-dispatched webhook, PR/MR and CI mirroring, and the shared issue auto-link / auto-close machinery. Off until MULTICA_VCS_SECRET_KEY is set, so existing deployments are unaffected. Co-authored-by: Bohan <[email protected]> |
||
|
|
18d41151eb |
feat(gc): batch issue reconciliation (#5534)
Co-authored-by: Eve <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b85bb71a58 |
feat: custom issue properties — typed workspace-defined fields with list-surface support (MUL-4463) (#5335)
* feat(server): custom issue properties — definitions, typed values, CLI (MUL-4463)
Workspace-level property definitions (issue_property table; 7 types:
text/number/select/multi_select/date/checkbox/url) plus a typed value bag
on each issue (issue.properties JSONB keyed by definition UUID, mirroring
the metadata machinery: single-key atomic writes, 16KB cap, GIN index).
- Definitions: owner/admin only; agent actors rejected (agents propose,
humans confirm). 20 active per workspace, 50 options per select,
reserved built-in names blocked, archive instead of delete.
- Values: any member or agent; per-type validation with self-correcting
error messages that enumerate legal option ids.
- API: /api/properties CRUD + PUT/DELETE /api/issues/{id}/properties/{propertyId};
issue responses always emit the properties bag.
- CLI: multica property list/get/create/update/archive/unarchive and
multica issue property list/set/unset with name→id translation.
- Events: property:created/updated, issue_properties:changed.
Co-authored-by: multica-agent <[email protected]>
* feat(web): custom properties settings tab + issue sidebar editors (MUL-4463)
- Settings → Properties: definition management mirroring the Labels tab
(list with type badges/option chips/usage counts, create/edit dialog
with option editor, archive/restore, 20-cap indicator). Admin-gated;
members see a read-only catalog.
- Issue detail sidebar: custom properties join the built-in optional
props' progressive disclosure — set values render as rows with
type-appropriate editors (select/multi-select pickers, calendar,
yes/no, inline input for text/number/url), unset ones live in the
same '+ Add property' menu behind a separator. Archived definitions
render read-only until cleared.
- Core: property types, zod schemas (lenient type strings for forward
compat), api client methods, React Query hooks with optimistic
single-key value writes, ws-updaters + realtime wiring for
property:created/updated and issue_properties:changed.
- Locales: en/zh-Hans/ja/ko strings; Issue fixtures gain properties: {}.
Co-authored-by: multica-agent <[email protected]>
* fix(properties): address MUL-4463 review round 1 — mobile CI, option guard, mutation safety, schema tolerance
- mobile: EMPTY_ISSUE_FALLBACK gains the required properties field (mobile
typecheck was the red CI check).
- server: PATCH /api/properties/{id} rejects config updates that remove
select options still referenced by issues (409 with a per-option usage
census via jsonb ?); renames keep ids and pass. Integration test included.
- core: property value mutations are serialized per workspace via mutation
scope, snapshot the bag from detail OR list caches (board surfaces have no
detail cache — the old path overwrote whole bags with one key), roll back
to the snapshot or invalidate on error, and the last settled mutation does
an authoritative detail+catalog invalidate (usage counts reconcile).
- schemas: unknown-shaped property values (future server types) are dropped
per-entry in a preprocess step instead of failing the whole IssueSchema
and blanking lists through parseWithFallback; test updated to lock the
tolerant behavior.
- realtime: reconnect invalidation covers the property catalog; every
issue_properties:changed event also refreshes catalog usage counts.
- ui: number editor accepts decimals (step=any); settings usage count
pluralizes (issue/issues) with CJK-safe plural keys.
Co-authored-by: multica-agent <[email protected]>
* fix(migrations): renumber issue properties to 179 and build the GIN index concurrently
main's migration sequence advanced twice under this PR (167 collision, then
an upstream renumber wave that claimed 178), so issue properties now sits at
179 — verified against main's current tip by the prefix-uniqueness lint.
The properties GIN index moves to its own single-statement migration (180)
using CREATE INDEX CONCURRENTLY — a plain CREATE INDEX on the hot issue
table would block writes for the duration of the build. Mirrors the
119_user_created_at_index pattern; full-chain dry-run on a fresh database
passes through 180.
Co-authored-by: multica-agent <[email protected]>
* feat(web): custom-property list surfaces — filter, cards, sort, board grouping (MUL-4463 M2)
Brings custom properties to the issue list surfaces on top of the M1
definitions/values core:
- Filter: per-definition sections in the Filter dropdown (select /
multi_select options with color dots and counts; checkbox as Yes/No
pseudo-options). OR within a definition, AND across definitions;
client-side in applyIssueFilters, mirrored into filterAssigneeGroups
for the assignee-grouped board. Included in active-filter count and
Clear all.
- Cards: per-property Display toggles (cardPropertyIds) render value
chips on board cards and list rows via CustomPropertyValueDisplay.
- Sort: SortField gains property:<id> for number/date definitions.
Server keeps position order (fixed sort enum); the surface controller
re-sorts client-side, swimlane/gantt reuse the same comparator.
Date-only strings compare lexically; missing values sort last.
- Board grouping: IssueGrouping gains property:<id> for select
definitions — one column per option (definition order) plus a
trailing No-value column, option-colored headings. Drag-drop moves
position via UpdateIssue and applies the value through
useSetIssueProperty/useUnsetIssueProperty (properties are not part
of UpdateIssueRequest). Stale persisted property groupings fall back
to status columns.
View-store: propertyFilters + cardPropertyIds persisted via the
partialize allowlist; clearFilters resets property filters; new fields
deep-merge cleanly into pre-existing persisted snapshots.
Co-authored-by: multica-agent <[email protected]>
* fix(properties): address MUL-4463 review round 2 — desc sort, option bucketing, archived-state reconciliation
- sort: direction now applies to value comparison only; issues without a
value sort last in BOTH directions (the whole-array reverse flipped them
to the front on desc). Test covers the desc+missing case.
- board: values referencing an option removed from the definition bucket
into the No-value column instead of vanishing (unmatched column ids
dropped the issue entirely). Defense-in-depth behind the new server-side
in-use guard; drag-utils test locks both behaviors.
- controller: persisted propertyFilters keyed by archived/deleted
definitions are stripped before reaching the filter predicates, and a
persisted property sort on a non-active definition degrades to manual
order — previously both kept silently applying while the header claimed
otherwise. The filter badge counts only active-definition filters.
Co-authored-by: multica-agent <[email protected]>
* feat(properties): server-side property filtering and sorting on list endpoints
Property filter/sort now execute in the database, so results are correct
across the full issue set — not just the loaded 50-per-status window
(closes MUL-4493 item 1's filter/sort half; requested on MUL-4463).
- New `properties` query param on ListIssues and ListGroupedIssues:
JSON {definitionId: [values]} compiled to an AND-of-ORs containment
check (double NOT EXISTS over jsonb_array_elements). One value expands
to every storage shape it could match — string (select), array element
(multi_select), boolean (checkbox) — so the handler stays type-agnostic.
Guarded at 20 definitions / 50 values.
- `sort=property:<definitionId>` resolves the definition and orders by a
typed expression (numeric CASE cast for number, NULLIF text for
date/text/url); missing values sort last in both directions. Malformed
ids 400; unknown/archived definitions degrade to position order instead
of breaking stale clients.
- Frontend: the property filter and property sort ride the IssueSortParam
window bag, so every surface (workspace + my-issues variants), query
key, and per-status load-more page carries them automatically. The
client-side re-sort layer is gone; applyIssueFilters keeps its property
predicate as an optimistic-update backstop.
- Regression test seeds 55 issues and proves a match at position 55 is
returned by a filtered 50-row page, plus sort order/missing-last,
AND-across-definitions, and the 400/fallback sort paths.
Co-authored-by: multica-agent <[email protected]>
* fix(properties): address review round 3 — cache reconcile, merged-scope order, GIN-indexable filter, pool loader
- Cache reconciliation: property value writes (mutation settle + WS event)
now invalidate every issue window whose server-side shape depends on
property values — queries filtered by `properties` or sorted by
`property:<id>` (detected via query-key predicate), covering flat lists,
assignee groups, and my-issues variants. Windows without property params
keep the cheap in-place patch. Fixes stale ordering/membership/counts
under staleTime:Infinity.
- My Issues "All" scope: merged assigned/created/involves results are
re-sorted with a comparator mirroring the server ORDER BY semantics
(including property sorts and missing-last, created_at DESC tiebreak) in
both the flat and assignee-grouped merge paths — relation concatenation
no longer overrides the user's sort.
- Filter predicate rebuilt as plain bind-parameter containment ORs
(AND across definitions): EXPLAIN now shows BitmapOr over
idx_issue_properties_gin (the correlated jsonb_array_elements form
defeated the index). Alternatives capped at 256 bind params.
- Property-grouped board gains a pool loader strip: one sentinel per
status that still has server rows, keeping every issue reachable until
per-column pagination lands (MUL-4493).
- Windowing regression test hardened: explicit positions + an assertion
that the unfiltered first page excludes the target (the old fixture tied
at position 0 and the created_at DESC tiebreak put the target on page
one, proving nothing).
- Rollback safety: /api/properties 404 (old server) degrades to an empty
catalog instead of a query error, which also keeps property params from
ever being sent to pre-property servers; migration 179's CHECK
constraints switch to NOT VALID + VALIDATE so the exclusive lock is
instantaneous.
Co-authored-by: multica-agent <[email protected]>
* fix(properties): harden concurrency and cache coordination from clean-room review
Backend (MUL-4762 F1/F4/F5):
- withPropertyLock: pg_advisory_xact_lock helper; definition create/update
and value writes now serialize config-vs-value and cap-vs-insert races
(workspace-level 'props:' lock + per-definition 'prop:' lock, ordered).
- propertySortExpr degrades archived definitions to position sort.
Frontend (F2/F3/F6):
- onIssuePropertiesChanged invalidates plain assignee-group caches too.
- Property value mutations cancel list refetches in onMutate and roll back
only the touched key against the current bag (concurrent WS writes to
other keys survive a failed write).
- useUpdateIssue reconcile drops the stale properties bag from the server
snapshot; the property pipeline owns that field.
- Surface controller passes persisted property filters/sorts through
until the catalog query settles (cold cache no longer strips them).
Co-authored-by: multica-agent <[email protected]>
* fix(properties): open_only branch honors the properties filter
ListOpenIssues takes the parsed AND-of-ORs containment groups as a single
jsonb properties_filter param and unrolls them with a static double
NOT EXISTS; previously the open_only path parsed the properties param and
silently dropped it (clean-room review F7a).
Co-authored-by: multica-agent <[email protected]>
* fix(properties): toast on failed board drag to a property column
Property-column drags rolled the card back silently on failure; mirror
the status/assignee drag path (use-issue-surface-actions) so the
snap-back is explained (clean-room review F3, drag half).
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
3f17c2717b |
WS-1465 fix autopilot duplicate issue dispatch (#4936)
Co-authored-by: JC的AI分身 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
79c9158097 |
fix(issue): order sub-issues by number ASC instead of position (#4511)
ListChildIssues and ListChildrenByParents ordered by `position ASC, created_at DESC`. position is assigned by NextTopPosition as MIN(position)-1 scoped to (workspace, status), not relative to siblings, so a parent's children interleave unpredictably across creation batches and statuses. Order by `number` (a per-workspace monotonic counter) instead. ASC keeps sub-issues in stable creation order (oldest first), so a parent's plan reads top-to-bottom in the order tasks were added. Adds ordering tests covering both queries with scrambled positions and mixed statuses. Closes #4232 MUL-3362 Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
a123dfc2df |
MUL-3508: stage sub-issues so the parent wakes per stage, not per child (#4410)
* feat(issues): stage sub-issues so the parent wakes per stage, not per child Sub-issues under a parent can be grouped into ordered stages (issue.stage). The child-done -> parent notification + assignee wake now fire only when a stage barrier closes: every sub-issue in the lowest unfinished stage has reached a terminal status (done/cancelled). An unstaged sibling set is one implicit stage, so the parent is woken once when the last sub-issue finishes instead of on every child — the default fix for the fire-on-every-child cascade reported in discussion #4320 / MUL-3508. Stage advancement stays agent-driven: the server only detects the closed barrier and wakes the parent assignee, who decides whether to promote the next stage. - DB: nullable issue.stage (CHECK >= 1) + sqlc regen - API: stage on issue create/update/response and batch update - CLI: `issue create`/`issue update` --stage; new `issue children` command that lists sub-issues grouped by stage (table + json) - stageBarrierClosed / stageProgressSummary in issue_child_done.go, with the wake comment now stage-aware, plus unit tests - skill docs (multica-working-on-issues SKILL.md + source map) Web UI (create-form stage picker, sidebar edit, group-by-stage display) is a follow-up; the API already returns stage for it to consume. MUL-3508 Co-authored-by: multica-agent <[email protected]> * fix(issues): address review on stage barrier (cancel, batch, unstaged) Resolves the three blockers from the PR review: 1. Cancel can close a stage. The child-done barrier now fires on any non-terminal -> terminal transition (done OR cancelled), not just done. isTerminalChildStatus already treats cancelled as terminal (a cancelled sibling never finishes, so it must not hold a stage open), so a cancelled last-open child now closes its stage and wakes the parent. Keying on the transition also makes a later cancelled -> done edit a no-op, avoiding a lagging duplicate wake. 2. Batch update of stage no longer no-ops. `hasMutation` now includes "stage", so `{"updates":{"stage":N}}` persists instead of returning {"updated": 0}. 3. Unstaged children no longer participate in the staged frontier. In a staged sibling set, NULL-stage children neither hold a stage open nor fire on their own completion, and the wake comment no longer renders "Stage 0". This matches migration 123 ("NULL does not participate in staged grouping") and the CLI's separate unstaged group, removing the footgun where an unstaged backlog child silently blocked Stage 1. Tests: cancellation closes a stage (staged + unstaged), unstaged ignored in a staged set, stage summary skips unstaged, and a stage-only batch update persists. MUL-3508 Co-authored-by: multica-agent <[email protected]> * feat(web): stage UI — create picker, sidebar edit, group sub-issues by stage Frontend for the sub-issue stage feature (web + desktop, shared via packages): - core: `stage` on the Issue type + create/update request types; zod IssueSchema parses it (defaults to null for older backends) with schema tests for the numeric and omitted cases. - StagePicker component (mirrors the other property pickers): "No stage" + Stage 1..N, offering one beyond the current/sibling max. - Create-issue modal: a Stage pill, shown only when a parent is selected, threaded into the create payload. - Issue detail sidebar: an editable Stage row + "add property" entry, gated to sub-issues (issues with a parent). - Sub-issue list grouped by stage with per-stage headers (flat when unstaged). - i18n: stage keys across en / zh-Hans / ja / ko (parity test passes). Verified: full typecheck (6/6), core (591) + views (1433) vitest suites, lint clean (no new findings). Backend/CLI shipped earlier in this PR. MUL-3508 Co-authored-by: multica-agent <[email protected]> * test(issues): add stage to Issue fixtures merged from main The merge brought in new Issue fixtures that predate the required `stage` field: core issues/batch.test.ts, views batch-action-toolbar.test.tsx, and the mobile EMPTY_ISSUE_FALLBACK sentinel. Add `stage: null` so they satisfy the Issue type (mobile reuses core's IssueSchema for parsing, so only the sentinel needs it). MUL-3508 Co-authored-by: multica-agent <[email protected]> * fix(web): feed StagePicker the sibling max stage so higher stages stay selectable The StagePicker accepts maxStage to extend its option list beyond the floored Stage 1-3, but neither call site passed it, so a parent with an existing Stage 4/5 child could not pick that stage when creating a new sub-issue or editing one in the sidebar. - Compute the sibling max stage at both call sites: the create modal now loads the parent's children (childIssuesOptions) and the detail sidebar reuses the already-loaded parentChildIssues. - Extract maxSiblingStage + stageOptions as pure helpers on stage-picker and unit-test them (the regression: a Stage 5 sibling keeps Stage 5 selectable and offers Stage 6). MUL-3508 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5900d8b637 |
fix(issues): make start_date/due_date timezone-stable calendar days (#3618) (#3692)
* fix(issues): store start_date/due_date as DATE, not timestamp (MUL-2925) These fields are calendar days (the pickers offer no time-of-day), but were stored as TIMESTAMPTZ. A client serializing local midnight via toISOString() folded its timezone into the instant, so the day shifted by the local offset (GH #3618). Migrate the columns to DATE and parse/serialize date-only "YYYY-MM-DD". ParseCalendarDate still accepts legacy RFC3339 (truncated to the UTC day) so older clients keep working. Co-authored-by: multica-agent <[email protected]> * fix(issues): render start_date/due_date as timezone-stable calendar days (MUL-2925) Pickers now emit date-only "YYYY-MM-DD" (local calendar day) instead of toISOString(), and every read formats via the shared @multica/core/issues/date helpers with timeZone:"UTC" so the day never shifts with the viewer's offset. The Gantt's existing UTC bucketing is now correct. Covers web/desktop pickers, quick-set menu, list/board/detail/activity, and the mobile due-date picker. Co-authored-by: multica-agent <[email protected]> * fix(issues): address date-only review — loud-fail ambiguous dates, finish display sweep (MUL-2925) Review follow-ups on #3692: - ParseCalendarDate no longer silently truncates a legacy non-midnight RFC3339 to the wrong UTC day; it accepts only YYYY-MM-DD or an exact UTC-midnight instant and rejects ambiguous ones loudly. Adds util unit tests. - migration 112 pins the TIMESTAMPTZ->DATE conversion to UTC explicitly via AT TIME ZONE 'UTC' (was session-timezone dependent); down migration too. - Convert remaining date-change display sites to formatDateOnly: inbox detail label (web) and mobile activity + inbox labels (were new Date()+local format). - CLI --start-date/--due-date help now says YYYY-MM-DD, not RFC3339. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bdb60acae9 |
fix: swimlane empty lanes in due to pagination (MUL-2724) (#3326)
* fix: Swimlane lazy load issues * wip * refactor * fix: Rebase issues * fix: rerender * refactor bactch and chunking |
||
|
|
1c91c2a3b2 |
security(db): scope DELETE/UpdateIssueStatus by workspace_id (defense-in-depth) (#3027)
* fix(security): scope DELETE/UpdateIssueStatus by workspace_id Add workspace_id to the WHERE clause of DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, and UpdateIssueStatus as SQL-layer defense-in-depth. Handler loaders (loadIssueForUser / loadSkillForUser / etc.) already enforce workspace membership today, so this is not patching a known live vuln. But the tenant invariant is currently a handler-layer guarantee — a future loader bypass or a new caller skipping the loader would be silently catastrophic. Making workspace_id part of the SQL identity collapses the trust surface to the schema itself: forging a sibling-workspace UUID becomes ErrNoRows instead of a cross-tenant write. Reference: incident #1661 (util.ParseUUID silent zero UUID returning 204 on a DELETE that matched zero rows) — same class of failure, prevented at a different layer. Scope: - 5 DELETE queries: issue, comment, project, skill, chat_session - 1 simple UPDATE: UpdateIssueStatus (2 narg, no SET ordering risk) - All callers updated (handlers, service, runtime sweeper fallback) Multi-narg UPDATE queries (UpdateIssue, UpdateProject, UpdateSkill, UpdateComment, UpdateChatSession*) are deferred to a follow-up to keep this change reviewable: each needs its narg pinning shifted and per-caller verification. sqlc was regenerated by hand (no local sqlc toolchain); CI's backend job is the authoritative compile check. * test(security): add workspace_scope_guard regression test Locks in the SQL-layer tenant guard added in this PR. For each of the 6 scoped queries (DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, UpdateIssueStatus), creates the resource in workspace A, invokes the query with a foreign workspace UUID, and asserts the row is untouched (0 rows affected with no error for :exec; pgx.ErrNoRows for :one). A future refactor that drops the workspace_id arg from any of these queries will now fail loudly instead of silently regressing. Includes a sanity sub-test that the in-workspace path still mutates, so a buggy guard that returns no-op for every call would not pass. Co-Authored-By: Claude Opus 4 <[email protected]> --------- Co-authored-by: Tom Qiao <[email protected]> Co-authored-by: Claude Opus 4 <[email protected]> |
||
|
|
5bacfd9742 |
MUL-2526 feat: add member(user_id, workspace_id) index + upgrade sqlc to v1.31.1 (#3046)
- Add migration 106: CREATE INDEX CONCURRENTLY on member(user_id, workspace_id) - Rewrite ListWorkspaces to drive from member table with explicit fields - Regenerate all sqlc code with v1.31.1 (intentional version upgrade) Co-authored-by: multica-agent <[email protected]> |
||
|
|
0c767c0052 |
feat(issues): per-issue metadata KV (MUL-2017) (#2845)
* feat(issues): per-issue metadata KV (MUL-2017)
Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).
All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.
GET /api/issues/:id/metadata
PUT /api/issues/:id/metadata/:key body: { "value": <primitive> }
DELETE /api/issues/:id/metadata/:key
Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.
CLI: multica issue metadata {list,get,set,delete}
multica issue list --metadata key=value (repeatable, AND)
set has --type to override the default value-sniffing
Co-authored-by: multica-agent <[email protected]>
* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)
- Fix two failing handler tests blocking backend CI:
- reset decode target after delete so map merge does not mask removal
- url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
the issue has no keys so it stays quiet in the common case.
Co-authored-by: multica-agent <[email protected]>
* feat(runtime): teach agents to read/write issue metadata (MUL-2017)
Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.
Co-authored-by: multica-agent <[email protected]>
* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)
main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.
Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
54368fd826 |
feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) (#2856)
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) Project Gantt now fetches its own scheduled-only data instead of riding the Board/List pagination cache. The Unscheduled drawer and pagination warning banner are gone, and any WS-driven issue change (create / update / delete) invalidates the new cache so the timeline stays live. - Backend: `GET /api/issues?scheduled=true` adds an `(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler. - Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single fetch and lives under its own cache key. WS handlers and mutations invalidate the prefix on create/update/delete so the bar reacts to start_date / due_date changes from other tabs and from this tab without waiting on the WS round-trip. - GanttView: drops the Unscheduled section, the pagination warning banner, and the load-all button; renders only scheduled rows. - Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`, `summarizeIssueListPagination`, and the gantt locale strings that supported the old plumbing. Co-authored-by: multica-agent <[email protected]> * fix(projects): page through Gantt fetch and isolate per-view data sources - Walk paginated `scheduled=true` issues until total is reached so projects with more than 500 scheduled bars no longer silently truncate. - Gantt mode disables the bucketed Board/List query and reads its own scheduled cache for the project empty-state check, so the page never short-circuits Gantt with a Board-derived "no issues" CTA. - `onIssueLabelsChanged` patches matching rows in the Project Gantt cache in-place, keeping label filters consistent after attach/detach from other tabs or agents. MUL-1881 Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
93153d08b7 |
feat(my-issues): cover squad assignees via involves_user_id (MUL-2397) (#2829)
Re-introduces the `involves_user_id` filter on the issues list / open-list / count / grouped paths, but with the semantics nailed down for the second time around: tab 3 surfaces issues whose assignee is an *indirect* extension of the user (owned agent, or a squad they're a human member of / lead via owned agent / have an owned agent inside) — and explicitly NOT direct member assignment, which is tab 1's meaning. - server/pkg/db/queries/issue.sql: 4-branch filter on ListIssues / ListOpenIssues / CountIssues. Each subquery clamps workspace_id because issue.assignee_id is polymorphic with no FK. Leader resolution reads squad.leader_id directly, not the squad_member copy row (squad.go ignores errors when seeding that copy, so it can be missing). FindActiveDuplicateIssue switched from positional $2/$3/$4 to named sqlc.arg() — pure hygiene so the generated struct field names don't drift when new nargs are added. - server/internal/handler/issue.go: parse involves_user_id and plumb it into the three sqlc params; ListGroupedIssues (hand-written dynamic SQL) gets a mirrored 4-branch fragment, no shortcut. - packages/core: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter / api.listIssues / api.listGroupedIssues all carry the new param through. - packages/views/my-issues: tab 3 switches from client-side agent-fanout to involves_user_id=user.id. agentListOptions import and the myAgentIds memo go away. - server/internal/handler/issue_involves_test.go: 13 integration tests cover every branch (positive + cross-workspace negatives) plus the critical ExcludesDirectMemberAssignee negative on BOTH the sqlc and the grouped paths, locking tab 3 ∩ tab 1 = ∅. Co-authored-by: Claude Opus 4.7 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5476e7678d |
Revert "feat(my-issues): cover squad assignees via involves_user_id (MUL-2364…" (#2828)
This reverts commit
|
||
|
|
3c510c31ed |
feat(my-issues): cover squad assignees via involves_user_id (MUL-2364) (#2801)
* feat(my-issues): cover squad assignees via involves_user_id (MUL-2364) The "My Agents" tab on /my-issues only resolved agents owned by the caller, so issues assigned to squads (member, leader, or agent-member of mine) never surfaced. This added a UNION-based involves_user_id filter that the backend expands to "me + agents I own + squads I relate to" in a single query. - SQL: ListIssues / ListOpenIssues / CountIssues accept narg involves_user_id and OR a workspace-scoped 3-branch UNION on the squad assignee subquery. Leader is sourced from canonical squad.leader_id (not the best-effort squad_member copy row whose AddSquadMember error is dropped in squad.go:177-188 and :259-263). - Handler: parses involves_user_id via parseUUIDOrBadRequest, plumbs into all three list params, and mirrors the same UNION fragment into the grouped dynamic SQL path. - Frontend: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter gain involves_user_id; api client forwards it to the querystring. - My Issues page: "agents" scope now passes involves_user_id instead of fanning out owned-agent IDs client-side. Tab label widens to "我的智能体 / 小队" / "My Agents / Squads". - Tests: Go suite covers all three squad relations including the canonical-leader-without-squad_member-copy variant, cross-workspace isolation for agent / leader / squad_member branches, combination with creator_id, and the malformed-UUID 400 path. Client test pins the involves_user_id querystring wiring for both list endpoints. The FindActiveDuplicateIssue query gets explicit sqlc.arg() names so sqlc regeneration keeps the existing struct field names regardless of the local sqlc version (no behavior change). Co-Authored-By: Claude Opus 4.7 <[email protected]> Co-authored-by: multica-agent <[email protected]> * test(my-issues): tighten cross-workspace negatives for involves_user_id UNION Cross-workspace negative tests previously put both the foreign actor and the foreign issue in the foreign workspace, so the outer i.workspace_id = $1 already excluded the row before the UNION branches were exercised. Stripping a.workspace_id = $1 / s.workspace_id = $1 from any of the UNION subqueries would not have failed the tests. Rewrite the three existing negative cases to seed the issue in testWorkspaceID with a polymorphic assignee_id pointing at a foreign-workspace agent or squad (issue.assignee_id has no FK per migrations/001_init.up.sql:61). Now each UNION branch must enforce its own workspace scoping for the issue to stay out of the result. Also add ExcludesOtherWorkspaceSquadAgentMember: the squad_member.agent UNION branch had only positive coverage; this test pins that s.workspace_id = $1 and a.workspace_id = $1 must both hold there too. Verified by mutation: stripping the workspace clause from each branch makes the corresponding test fail. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 4.7 <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3645bdb5b6 |
feat(issues): add start_date field with progressive disclosure (MUL-2274) (#2696)
* feat(issues): add start_date field with progressive disclosure (MUL-2274) Mirrors the existing due_date implementation end-to-end so an issue can express a planned start in addition to a deadline. Surfaces start_date as an optional sidebar property alongside priority / due_date / labels (added in MUL-2275), with consistent picker, board/list/sort, activity, and inbox plumbing. Backs the Project Gantt work (parent MUL-1881) and keeps the progressive-disclosure attribute experience consistent. - DB: migration 091 adds issue.start_date TIMESTAMPTZ. - sqlc: ListIssues / CreateIssue / UpdateIssue / CreateIssueWithOrigin / ListOpenIssues read & write start_date. - Backend: IssueResponse + create/update/batch-update handlers parse and emit start_date with RFC3339 validation; new start_date_changed activity event + subscriber notification (with prev_start_date in event payload). - CLI: --start-date flag on `multica issue create` / `issue update`. - Frontend: StartDatePicker component, start_date wired into Issue type, Zod schema, draft / view stores, sort util, header sort + card-property options, list-row / board-card display, create-issue modal, and the issue-detail progressive-disclosure "+ Add property" surface (visibility rule, picker row, add-property menu icon + label). - i18n: en + zh-Hans for sort_start_date / card_start_date / prop_start_date / activity start_date_set / start_date_removed / picker start_date.trigger_label / clear_action / inbox labels. - Tests: new TestNotification_StartDateChanged; existing Issue / draft / modal fixtures extended with start_date. Co-authored-by: multica-agent <[email protected]> * feat(issues): align start_date with due_date in actions menu and CLI table - Add Start Date submenu (today / tomorrow / next week / clear) in actions menu, mirroring Due Date — parity with the Due Date quick setters in list/board context and 3-dot menus. - Add corresponding en / zh-Hans i18n keys (actions.start_date / start_today / start_tomorrow / start_next_week / start_clear). - CLI human table for `multica issue list` and `multica issue get` now shows a START DATE column next to DUE DATE; --full-id variant too. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
d8635ad580 |
fix(issues): prevent duplicate active issue creation (MUL-2225) (#2602)
* fix: prevent duplicate active issue creation * fix(issues): address duplicate guard review * fix(autopilot): skip duplicate issue admissions * fix(issueguard): tighten duplicate lookup edge cases * test(issues): cover duplicate guard autopilot skips * feat(autopilots): group skipped runs in history |
||
|
|
5ad1641b72 |
Revert "Squad archive dialog + role editor + transactional DeleteSquad (#2680)" (#2687)
This reverts commit
|
||
|
|
2980ead4c7 |
Squad archive dialog + role editor + transactional DeleteSquad (#2680)
* docs(squad): address plan-review feedback for archive + role plan
Resolve the 4 items the reviewer raised on MUL-2265:
1. TS schema: declare `active_issue_count` as optional (`number | null | undefined`)
so list/create/update Squad responses don't lie about their shape; only
`getSquad` parses through SquadSchema.
2. Archive semantics: restrict TransferSquadAssignees to active issues
(status NOT IN done, cancelled) so dialog count and SQL operate on one set
and terminal-state issues keep their historical assignee.
3. Index assumption: corrected — `idx_issue_assignee (assignee_type,
assignee_id)` exists and is sufficient at realistic squad cardinality;
no new index needed.
4. Fixed `*int64` test comparison and added `.loose()` to SquadSchema per
the local schemas.ts convention.
Co-authored-by: multica-agent <[email protected]>
* docs(squad): plan v3 — revert to count-all/transfer-all on archive
Reviewer round 2 surfaced two structural problems with plan v2's
active-only carve-out:
1. useActorName resolves squad names via ListSquads, which filters
archived_at IS NULL. A closed issue with an archived-squad assignee
would render as "Unknown Squad".
2. The status-only update path in UpdateIssue skips validateAssigneePair,
so a done/cancelled issue with an archived-squad assignee could be
reopened to in_progress, violating the "no active issue on an archived
squad" invariant enforced elsewhere.
Both problems disappear by reverting to count-all + transfer-all: after
ArchiveSquad runs, no issue points at the archived squad, so neither
case can occur. The product trade-off is that closed historical issues
now show the leader agent instead of the archived squad in their
"Assigned to" badge — consistent with existing agent-level reassignment
behavior elsewhere in the product.
Field rename: active_issue_count -> issue_count.
TransferSquadAssignees SQL is unchanged (already transfers all).
Co-authored-by: multica-agent <[email protected]>
* docs(squad): add Task 2b — wrap DeleteSquad transfer + archive in one tx
Reviewer round-3 flagged that the v3 invariant ("after archive no
issue points to the squad") was asserted on the happy path only.
DeleteSquad's current best-effort impl breaks it two ways:
- transfer failure → slog.Warn but archive proceeds (Unknown Squad,
reopen-into-archived-squad bugs reappear)
- archive failure after a committed transfer → 500 with squad still
active but emptied
Task 2b rewrites DeleteSquad to run TransferSquadAssignees +
ArchiveSquad inside one pgx tx, mirroring the project.go:266-314
pattern. Publish moves below Commit. Adds two regression tests that
lock both partial-write failure modes.
Co-authored-by: multica-agent <[email protected]>
* feat(squad): replace native confirm() with AlertDialog and rewrite role editor as combobox
Backend:
- Add CountIssuesForSquad sqlc query (counts every issue assigned to a squad,
no status filter — matches the existing transfer-all archive semantics).
- Extend SquadResponse with optional `issue_count` (`*int64` + omitempty,
populated only by GetSquad to avoid an N+1 in the list endpoint).
- Wrap DeleteSquad's transfer + archive in a single pgx transaction so the
v3 invariant ("after archive, no issue points to the squad") is durable
rather than best-effort. Promote slog.Warn to slog.Error and check the
parseUUIDOrBadRequest ok flag (silent zero-UUID was a #1661-class latent
bug). Publish only after Commit so realtime never sees rolled-back state.
- Tests cover happy path (count, transfer-all including terminal statuses)
and both rollback directions (transfer fail / archive fail) via a
fault-injecting tx wrapper.
Frontend:
- Extend Squad TS type with `issue_count?: number | null` (optional —
list/create/update legitimately omit it). Add SquadSchema with `.loose()`
and wrap getSquad with parseWithFallback so older servers and count-error
responses degrade to the dialog's "no count" copy variant.
- Replace `window.confirm()` with shadcn `ArchiveSquadConfirmDialog`
(destructive variant, leader name + count + closed-issue caveat in the
copy, Loader2 while pending). i18n keys added under squads.archive_dialog.
- Rewrite RoleEditor as a Popover + Command combobox: Pencil affordance is
always visible, suggestions aggregate other members' roles, commit only
on Enter or selecting a suggestion (blur discards), per-member savingId
drives Loader2 so the spinner only renders on the row being saved.
Co-authored-by: multica-agent <[email protected]>
* fix(squad): discard RoleEditor draft on close and no-op blank Enter
Two reviewer findings on
|
||
|
|
2d9c153695 |
feat: quick-create issue (async agent + inbox completion) (#1786)
* feat(server): add quick-create issue async task path Adds POST /api/issues/quick-create which validates the picked agent's reachability up front (not archived, has runtime, runtime online) then queues an issue-less agent task whose context JSONB carries the user's natural-language prompt + requester + workspace. Daemon claim resolves the workspace from the context, and the prompt builder switches to a quick-create template instructing the agent to translate the prompt into a single multica issue create call. Task completion writes a success inbox item to the requester pointing at the newly-created issue (located by querying the agent's most recent issue in the workspace since task start, so we don't depend on agent stdout shape). Failures write an action_required inbox item carrying the original prompt + agent id so the frontend can offer "Edit as advanced form" without losing input. * feat(views): quick-create issue modal + inbox failure CTA Adds a streamlined create-issue UI bound to the c shortcut: pick an agent, type one line, submit. The modal closes immediately and the agent translates the prompt into a multica issue create call in the background. Shift+c keeps the legacy advanced form for users who want every field. The "Advanced" button inside the new modal seeds the shared issue-draft store with the prompt + picked agent so switching mid-flow doesn't lose input. Last-used agent persists per (user, workspace) via a workspace-aware zustand store so frequent users skip the picker on every open. Inbox renders quick_create_done items with a status pin to the new issue and quick_create_failed items with an "Edit as advanced form" CTA that re-seeds the legacy modal with the original prompt. ApiError now carries the parsed JSON body so the modal can branch on the structured agent_unavailable code without parsing the error message. * fix(quick-create): execenv injection, claim race, private-agent permission Addresses GPT-Boy review on #1786: 1. execenv was rendering the assignment-task issue_context.md / runtime workflow even for quick-create, telling the agent to call `multica issue get/status/comment add` against an empty IssueID. Adds QuickCreatePrompt to TaskContextForEnv, plus a quick-create branch in renderIssueContext + the runtime_config workflow that instructs the agent to run a single `multica issue create` and exit, with explicit "do NOT call issue get/status/comment add" guards. 2. ClaimAgentTask serialized only on issue_id / chat_session_id, so concurrent quick-creates on the same agent (both NULL on those columns) ran in parallel — making the success-inbox lookup race over "most recent issue by this agent". Adds a third OR clause that treats "all four FKs NULL" as a serialization key for the same agent, so quick-create tasks on a given agent run one at a time. 3. QuickCreateIssue handler bypassed the private-agent ownership rule that validateAssigneePair enforces elsewhere — a user could POST a private agent_id they didn't own and trigger it. Now routes the picked agent through validateAssigneePair before the runtime liveness check. 4. Clarifies the quick-create-store namespacing comment to match the actual workspace-aware StateStorage convention used by the other issue stores (per-user is browser-profile-local). * fix(quick-create): branch Output section + deterministic origin lookup Addresses GPT-Boy's second-pass review on #1786: 1. The runtime_config.go Output section forced "Final results MUST be delivered via multica issue comment add" for every non-autopilot task — quick-create still got this conflicting instruction even though there's no issue to comment on. Switched the Output block to a three-way switch so quick-create gets a tailored "stdout is captured automatically; do NOT call comment add" branch matching the autopilot variant. 2. Completion lookup was "most recent issue created by this agent since task.started_at", which races against concurrent issue creates by the same agent (assignment task running alongside quick-create when max_concurrent_tasks > 1). Replaced with a deterministic origin link: - Migration 060 extends issue.origin_type CHECK to allow 'quick_create'. - Daemon sets MULTICA_QUICK_CREATE_TASK_ID env var when running a quick-create task. - multica issue create CLI reads the env var and stamps the new issue with origin_type=quick_create + origin_id=<task_id>. - Server CreateIssue handler accepts (origin_type, origin_id) from trusted callers (only "quick_create" is allowed; the pair is rejected unless both fields are provided together). - notifyQuickCreateCompleted now calls GetIssueByOrigin keyed on (workspace_id, "quick_create", task.ID) — no more time-window racing against parallel agent activity. The old GetRecentIssueByCreatorSince query is removed. |
||
|
|
abd69890a8 |
Revert "feat(issues): server-side filters incl. label, fixing pagination drop…" (#1779)
This reverts commit
|
||
|
|
246fcd4ce4 |
feat(issues): server-side filters incl. label, fixing pagination drops (#1776)
* feat(issues): server-side label + filter querying for issue list Extends GET /api/issues with label_ids, priorities, creator_ids, project_ids, include_no_assignee, and include_no_project params, and moves the existing single-value filters onto array-form. Each filter becomes part of the SQL WHERE clause so paginated buckets reflect the user's selection — fixes the bug where client-side filtering hid matches sitting past the first page (#1491). CLI gains a repeatable --label flag; legacy --priority/--assignee/ --project keep working via the single-value compatibility paths. * feat(issues): drive workspace + my-issues filters from the server issueListOptions and myIssueListOptions now key the React Query cache on a normalized filter object, so each filter combination has its own cache entry and a filter change re-fetches with the wire-shape filter applied server-side. Drops the client-side filterIssues step on the issues page, my-issues page, and project detail — that step silently hid matches that lived past the first paginated page (#1491). Adds a Label submenu to the workspace issues filter dropdown, plus labelFilters in the view store. Mutations and ws-updaters fan their optimistic patches across every filter-keyed list cache via qc.setQueriesData on issueKeys.listPrefix(wsId), and the editor's mention-suggestion reads from any matching list cache for instant first paint regardless of which filter is active. * fix(issues): route Members/Agents scope through server-side filter The Members/Agents scope tabs on the workspace issues page were still narrowing client-side via `assignee_type === 'member'`. That hits the exact pagination-blind bug this PR is meant to fix: if the first 50 issues per status don't include the right assignee type, the tab shows "No issues" while later pages have matches. Adds an `assignee_types text[]` filter to ListIssues / ListOpenIssues / CountIssues, threads it through the API client, normalizer and view filter, and maps the scope tab to it. Each scope now keys its own list cache and refetches with the correct first page. Also disables the My Issues "My Agents" query when the user owns no agents — `assignee_ids: []` was getting dropped by both the API client and the query-key normalizer, so the request went out unfiltered and surfaced unrelated issues under "My Agents". |
||
|
|
637bdc8eb3 |
feat(analytics): full PostHog pipeline + 6 funnel events (MUL-1122) (#1367)
* feat(analytics): add PostHog client with async batch shipping Introduces server/internal/analytics, the shipping layer for the product funnel defined in docs/analytics.md. Capture is non-blocking — events are enqueued into a bounded channel and a background worker batches them to PostHog's /batch/ endpoint. A broken backend drops events rather than blocking request handlers. Local dev and self-hosted instances run a noop client until the operator sets POSTHOG_API_KEY. This is PR 1 of MUL-1122; signup and workspace_created emission land in the follow-up commit so this change is independently reviewable. * feat(server): emit signup and workspace_created analytics events Wires analytics.Client through handler.New and main, then emits the first two funnel events: - signup fires from findOrCreateUser (which now reports isNew), covering both the verification-code and Google OAuth entry points — a single emission site guarantees Google signups aren't missed. - workspace_created fires after the CreateWorkspace transaction commits, with is_first_workspace computed from a post-commit ListWorkspaces count so we can distinguish fresh-user activation from returning-user expansion. Tests use analytics.NoopClient so nothing ships from test runs. PR 1 of MUL-1122; runtime_registered and issue_executed follow in later PRs per the plan. * refactor(analytics): drop is_first_workspace from workspace_created Stamping "is this the user's first workspace?" at emit time races under concurrent CreateWorkspace requests: two transactions committing close together can both read a post-commit count greater than one and both emit false. Fixing it at the SQL layer requires a schema change we don't want in PR 1. PostHog answers the same question exactly from the event stream (funnel on "first time user does X" / cohort on $initial_event), so removing the property loses no information and makes the emit side race-free. * docs(analytics): document self-host safety defaults Spell out why self-hosted instances never ship events upstream by default (empty POSTHOG_API_KEY → noop client) and explain how operators can point at their own PostHog project without any code change. * feat(analytics): emit runtime_registered, issue_executed, team_invite_* Three server-side funnel events, all gated on first-time state transitions so retries and re-runs don't inflate the WAW buckets: - runtime_registered fires from DaemonRegister when UpsertAgentRuntime reports (xmax = 0) — i.e. the row was inserted, not updated. Heartbeats and re-registrations stay silent. - issue_executed fires from CompleteTask after an atomic UPDATE issue SET first_executed_at = now() WHERE id = $1 AND first_executed_at IS NULL flips the column for the first time. Retries, re-assignments, and comment-triggered follow-up tasks hit the WHERE clause and no-op. Carries nth_issue_for_workspace so the ≥1/≥2/≥5/≥10 buckets filter without extra queries. - team_invite_sent fires from CreateInvitation and team_invite_accepted from AcceptInvitation, closing the expansion funnel. Adds a 050 migration for issue.first_executed_at plus a partial index so the workspace-scoped executed-count query doesn't scan the never-executed tail. * feat(config): surface PostHog key via /api/config Extends AppConfig with posthog_key / posthog_host sourced from env on every request (so operators can rotate the key via secret refresh without a restart). Reading the key off the server — rather than baking it into the frontend bundle via NEXT_PUBLIC_* — means self-hosted instances inherit the blank key automatically and never ship events upstream. * feat(analytics): wire posthog-js identify + UTM capture on the client Adds @multica/core/analytics — a thin wrapper around posthog-js that owns attribution capture and identity merge. Posthog-js config comes from /api/config (not NEXT_PUBLIC_*), so self-hosted instances whose server returns an empty key automatically run the SDK inert. captureSignupSource stamps a multica_signup_source cookie with UTM params and the referrer's origin (never the full referrer — that can leak OAuth code/state in the callback URL). The backend signup event reads this cookie on new-user creation. Identity flows: - auth-initializer fires identify() right after getMe() resolves, on both cookie and token paths. A getConfig/getMe race is handled by buffering a pending identify inside the analytics module and flushing it once initAnalytics finishes. - auth store calls identify() on verifyCode / loginWithGoogle / loginWithToken and resetAnalytics() on logout so the next login merges cleanly without bleeding events. * docs(analytics): describe runtime_registered, issue_executed, invite events Fills in the schema for the remaining funnel events. Captures the design commentary that belongs next to the contract rather than in a PR description — in particular why issue_executed uses the atomic first_executed_at flip instead of counting task-terminal events, and why runtime_registered relies on xmax = 0 rather than a query-then-write. * fix(analytics): drop non-atomic nth_issue_for_workspace from issue_executed Computing the workspace's Nth-issue ordinal at emit time is not atomic under concurrent first-completions — two transactions can both run MarkIssueFirstExecuted, then both run CountExecutedIssuesInWorkspace, and both observe count=1 before either has committed, so both events go out stamped as n=1. Serialising it would mean a per-workspace advisory lock or a SERIALIZABLE-isolated tx; PostHog answers the same question exactly at query time via row_number() partitioned by workspace_id, so the emit-time property adds risk without adding information. Removes the property from analytics.IssueExecuted, deletes the unused CountExecutedIssuesInWorkspace query, and regenerates sqlc. The partial index stays — any future workspace-scoped executed-issue query will want it. * fix(analytics): wire $pageview and harden signup_source cookie payload Two frontend fixes from the PR review: - PageviewTracker, mounted under WebProviders, fires capturePageview on every Next.js App Router path / query-string change. Without this the capturePageview helper in @multica/core/analytics was never called and the acquisition funnel's / → signup step was empty. - captureSignupSource now caps each UTM / referrer value at 96 chars *before* JSON.stringify, and drops the whole cookie when the serialised payload still exceeds 512 chars. Previously the overall slice(0, 256) could leave a half-JSON string on the wire that neither the backend nor PostHog could parse. Both capturePageview and identify now buffer a single pending call when fired before initAnalytics resolves — otherwise the initial "/" pageview and same-turn login identify race the /api/config fetch and get dropped. resetAnalytics clears both buffers so a logout→login cycle stays clean. * fix(analytics): URL-decode signup_source cookie on read Go does not URL-decode Cookie.Value automatically, so the frontend's JSON-then-encodeURIComponent payload was landing in PostHog as percent-encoded garbage (%7B%22utm_source...). Unescape on read so the backend receives the original JSON string the frontend intended, and drop values that fail to decode or exceed the server-side cap — sending truncated garbage is worse than sending nothing. Oversized-cookie guard matches the frontend's SIGNUP_SOURCE_MAX_LEN. * docs(analytics): reflect nth-issue drop, $pageview wiring, cookie encoding Pulls the schema doc back in line with the code: issue_executed no longer advertises nth_issue_for_workspace (with a note about why PostHog derives it at query time instead), the frontend $pageview section names the actual PageviewTracker component that fires it, and the signup_source section documents the per-value cap / overall drop rule and the encode-on-write / decode-on-read contract. --------- Co-authored-by: Jiang Bohan <[email protected]> |
||
|
|
0db7d2fb64 |
fix(issues): include description in list queries for board card display (#1375) (#1377)
The ListIssues and ListOpenIssues SQL queries omitted the description column, so the API response never included description data. Board cards checked issue.description (always null) and never rendered it, even when the Description card property was enabled. Add description to both SQL queries, the generated Go structs/scan calls, and the response mapping functions. |
||
|
|
d88fe2608e |
feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents Introduce the Autopilot feature — recurring automations that assign work to AI agents on a schedule or manual trigger. Supports two execution modes: create_issue (creates an issue for the agent to work on) and run_only (directly enqueues an agent task without issue pollution). Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService with concurrency policies (skip/queue/replace), HTTP CRUD + trigger endpoints, background cron scheduler (30s tick), event listeners for issue→run and task→run status sync. Frontend: types, API client methods, TanStack Query hooks with optimistic mutations, realtime cache invalidation, list page with create dialog, detail page with trigger management and run history, sidebar nav + routes for both web and desktop apps. * feat(autopilot): improve UX — trigger config, edit dialog, template gallery - Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone - Fix Select components showing UUIDs instead of names (Base UI render function pattern) - Add Edit button on detail page opening a unified edit dialog - Remove project/concurrency/issue-title-template from create/edit (simplify for users) - Add trigger configuration inline during autopilot creation - Add template gallery on empty state (6 step-by-step workflow templates) - Rename "Description" to "Prompt" throughout UI - Inject autopilot run timestamp into issue description for agent date awareness - Treat issue status "in_review" as run completion (fixes skip on next trigger) - Make migration idempotent with IF NOT EXISTS clauses |
||
|
|
a94c6481dd |
fix: compute sub-issue progress from database instead of paginated client cache (#865)
The sub-issue progress indicator (e.g. "0/2") was undercounting because it was computed from the client-side issue list, which only loads the first 50 done issues. Sub-issues marked as done beyond that page were excluded from both the total and done counts. Added a dedicated backend endpoint (GET /api/issues/child-progress) that aggregates child issue counts directly from the database, ensuring accurate totals regardless of client-side pagination or filtering. Fixes MUL-702 |
||
|
|
a76194744a |
feat(cli): add --project filter to issue list (#691)
Co-authored-by: nocoo <[email protected]> |
||
|
|
cce210ed3a |
feat(assign): sort members & agents by user's assignment frequency (#652)
The Assign dropdown now sorts members and agents by how frequently the current user assigns issues to them. Frequency is computed from two sources: assignee_changed activities in the activity log and initial assignments on issues created by the user. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
b52c048c8e |
fix(my-issues): use server-side filtering instead of client-side (#649)
* fix(my-issues): use server-side filtering instead of client-side My Issues was fetching ALL workspace issues and filtering client-side, causing the Done column to show wrong counts (269 vs user's actual count) and only 2-3 done issues to appear from the first 50-item page. Backend: - Add creator_id and assignee_ids (uuid[]) filters to ListIssues, ListOpenIssues, and CountIssues SQL queries - Parse creator_id and assignee_ids (comma-separated) query params Frontend: - Add myIssueListOptions with per-scope server-filtered queries - Each tab now calls the API with the right filter: Assigned → assignee_id, Created → creator_id, My Agents → assignee_ids - Add useLoadMoreMyDoneIssues for server-filtered done pagination - WS events invalidate My Issues cache via issueKeys.myAll Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * refactor(my-issues): merge duplicate load-more hooks into one Both board-view and list-view were unconditionally calling two hooks (useLoadMoreDoneIssues + useLoadMoreMyDoneIssues) and picking one at runtime. Merged into a single useLoadMoreDoneIssues with an optional myIssues param so only one hook runs per render. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
02cdfcb93f |
feat(search): improve ranking with ILIKE, identifier search, multi-word support (#601)
* feat(search): improve ranking with ILIKE, identifier search, multi-word support - Replace LIKE with ILIKE for case-insensitive matching - Support identifier search (e.g. "MUL-123" or bare "123") - Refine sorting tiers: number match > exact title > title starts with > title contains > all words in title > description > comment - Add status-based tiebreaker (active issues rank higher) - Support multi-word search where all terms must match somewhere - Move search query from sqlc to dynamic SQL for flexibility Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(search): fix parameter type error for single-word queries Only allocate per-term SQL parameters when there are multiple search terms. For single-word queries, the phrase parameter already covers the search — unused term params caused PostgreSQL error "could not determine data type of parameter $3". Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
fee8f41ea5 |
perf(api): omit description from list issues response
Change ListIssues and ListOpenIssues SQL queries to select specific columns (excluding description, acceptance_criteria, context_refs). Reduces list API payload size, especially for issues with embedded images. Frontend handles null description gracefully — board card short-circuits, issue detail fetches full data via its own query. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
68e2a14ba2 |
feat(projects): add Project entity with full-stack CRUD support (#552)
Implements the Project concept as a higher-level grouping for issues. Hierarchy: workspace → project → issue → sub-issue. Backend: - Migration 034: project table + issue.project_id FK - sqlc queries for project CRUD - Project handler with list/get/create/update/delete - Issue handler updated to support project_id in create/update - Routes at /api/projects, WebSocket event constants Frontend (new monorepo structure): - @multica/core: Project types, API client methods, queries/mutations, status config, realtime sync - @multica/views: Projects list page, detail page (overview + issues tabs), project picker for issue detail panel - apps/web: Route pages, sidebar navigation entry All TypeScript type checks and tests pass. |
||
|
|
23136da34f |
feat(search): implement full-text search for issues (#507)
* feat(search): implement full-text search for issues Add pg_bigm-based full-text search across issue titles and descriptions, with API endpoint, CLI subcommand, and web Cmd+K search dialog. - Migration 032: pg_bigm extension + GIN indexes on title/description - Server: GET /api/issues/search?q=... with pagination and total count - CLI: `multica issue search <query>` with table/json output - Web: Cmd+K command palette using cmdk, with debounced search Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(search): address review feedback on search implementation 1. Escape LIKE special characters (%, _, \) in handler to prevent matching anomalies from user input. 2. Wire AbortController signal into searchIssues fetch so in-flight requests are actually cancelled on new input. 3. Fix offset=0 falsy check — use !== undefined instead of truthiness. 4. Merge results + count into single query using COUNT(*) OVER() window function, eliminating the duplicate DB round-trip. 5. Exclude done/cancelled issues by default; add include_closed parameter to API, CLI (--include-closed), and web client. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(search): default web search to include all statuses Pass include_closed: true in the web Cmd+K search so results include done and cancelled issues by default, matching the reviewer's request. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(search): add comment search with snippet extraction Extend search to cover issue comments in addition to title/description. Results are deduplicated at the issue level, with match_source and matched_snippet fields indicating where and what matched. - Migration 033: pg_bigm GIN index on comment.content - SQL: EXISTS subquery for comment matching, correlated subquery for snippet extraction, 3-tier ranking (title > description > comment) - Server: SearchIssueResponse with match_source and matched_snippet - Web: show comment icon + snippet below issue title when matched - CLI: MATCH column shows source and truncated snippet Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat(search): redesign search dialog to match Linear's spacious style - Widen dialog from sm (384px) to xl (576px) with top-20% positioning - Larger search input with icon, generous padding, and ESC hint - Use cmdk primitives directly for full style control - Taller result list (400px / 50vh), spacious result items (py-2.5) - Rounded-lg items with accent highlight on selection - Cleaner border separator between input and results --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
a8a8ff6eca |
feat(issues): add sub-issue support (#483)
* feat(issues): add sub-issue support
- Backend: Add ListChildIssues SQL query, add parent_issue_id to UpdateIssue,
add GET /api/issues/{id}/children endpoint
- Frontend: Display parent issue breadcrumb and link in issue detail sidebar,
show child issues list with status icons, add "Create sub-issue" action in
dropdown menu and sidebar, pass parent_issue_id through create issue modal
- Update test mocks for new API method
* fix(issues): add parent validation, cycle detection, and improve child refresh
- CreateIssue: validate parent issue exists in the same workspace
- UpdateIssue: validate parent exists, prevent self-referencing, detect
circular parent chains (up to 10 levels deep)
- Frontend: derive child issues from store when available instead of
refetching on every global issue count change
|
||
|
|
39ca8ed9e8 | Revert "feat(issues): add structured ticket search" | ||
|
|
34c39b765e | feat(issues): add structured ticket search |