agy's --print-timeout defaults to 5m when the flag is omitted, but the
daemon treated "omit the flag" as "no cap". In the default no-cap config
every Antigravity turn was therefore silently capped at 5 minutes: any
run whose build/tests outlived the budget had agy abort mid-turn, print
"Error: timed out waiting for response", and exit 0 — which the backend
recorded as a successful "completed" with truncated output (the reported
"Antigravity disconnects", MUL-3570 / #4453).
- Always pass --print-timeout: the configured cap when positive, else a
large value (24h) that defers to the daemon's idle/tool watchdogs.
- Detect agy's print-mode timeout marker in the run log and surface the
result as a timeout instead of a truncated success.
Verified by reproducing against agy 1.0.8 and with new unit + end-to-end
backend tests.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* docs: add 2026-06-23 changelog entry
Co-authored-by: multica-agent <github@multica.ai>
* docs(changelog): sharpen 0.3.28 title and handoff wording
- Headline the two flagship features in the title: staged Sub-Issues and Qoder runtime support
- Rewrite the vague agent-handoff line to spell out the pre-trigger confirmation (whether/which agent will start, apply without running) and the handoff note as next-run context
- Apply across en/ja/ko/zh
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: J <j@multica.ai>
The transcript dialog opened from a running task's row showed only a
one-shot snapshot taken at open time: TranscriptButton fetched once via
api.listTaskMessages and cached it locally, never subscribing to the
shared ["task-messages", taskId] cache that the WS task:message stream
already seeds. New tool calls / thinking / text never appeared until the
task finished or the page reloaded.
Add a live-cache mode to the shared TranscriptButton: when isLive and the
parent provides no items and the task id is a persisted UUID, render from
the shared task-messages cache so the open dialog grows in real time. On
open (and again on the running→terminal transition) force a backfill via
api.listTaskMessages and merge it into the cache by seq — taskMessagesOptions
is staleTime:Infinity, so a plain subscription never heals a WS reconnect
gap. The cache observer is read-only (enabled:false) so React Query never
blind-replaces the cache; only the WS handler and the seq-merged backfill
write it. The subscription mounts only while the dialog is open, so closed
live rows add no baseline requests; terminal tasks keep the lazy one-shot
fetch.
Covers issue execution-log and agent activity. Autopilot issue-less
run_only live log is out of scope: the backend doesn't broadcast
task:message for tasks with no issue/chat session, so there's nothing to
subscribe to — backend broadcast unchanged.
Extract mergeTaskMessagesBySeq into packages/core/chat/queries.ts and route
both the realtime task:message handler and the new backfill through it, so
there is one seq-merge semantics for that cache instead of two.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): stop issue-trigger preview flicker
The pre-trigger preview re-rendered/refetched on every workspace task
event: WS task lifecycle invalidated issueTriggerPreviewAll (staleTime 0),
forcing a background refetch whose isFetching was surfaced as isLoading,
collapsing and reopening CreateRunHint's reveal band.
The assign source (create / assignee change) cancels existing tasks before
enqueuing, so its verdict can't shift from a task event at all; the status
source's pending dedup could, but the preview is advisory and the write
path re-evaluates authoritatively, so a rare stale label is harmless. Drop
the WS invalidation so the preview refetches only on input (signature)
change. Keep the comment-trigger invalidation — its verdict genuinely
changes mid-compose and its chips drive an immediate, unconfirmed send.
Align the hook's data handling with the comment-trigger preview:
keepPreviousData so an input switch swaps in place instead of collapsing,
and treat only the first load (no prior data) as loading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(issues): skip run-confirm modal for backlog assign
Assigning a Backlog issue to an agent/squad never starts a run (the
parking lot — server/internal/service/issue_trigger.go), so the
pre-trigger confirm modal only rendered an empty "won't start" box with
a single Apply button. Apply directly instead: the single path checks
issue.status, the batch path skips only when every selected issue is
Backlog (mixed selections still confirm — the non-backlog ones trigger).
Mirrors the existing backlog short-circuit in handleBatchStatus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(modals): run-confirm loading state + submit spinner
The dialog grew in height after open: it rendered the short "won't
start" variant while POST /api/issues/preview-trigger was in flight, then
the note box appeared when the predicate landed. Keep the note box
mounted (disabled) during loading so assign mode opens at its resolved
height, and show a Spinner + 'checking' headline while loading.
Submit had no feedback — buttons only disabled, which read as frozen for
note assigns (the request starts an agent server-side). Track which
footer action is in flight and show a Spinner on the clicked button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): show handoff note in execution-log trigger text
An assignment-triggered run that carried a handoff note showed the
generic "Initial run" label. Surface the note inline (truncated, like
comment triggers show their text) so the row reads as the handoff.
taskToResponse now populates handoff_note for all callers (dropping the
now-redundant explicit set in ClaimTaskByRuntime); the field is added to
the AgentTask type + zod schema (optional, additive — old clients ignore
it via the loose schema, new clients fall back to "Initial run").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(landing): show live GitHub star count on the header GitHub button
Add a small client hook (useGithubStars) that fetches stargazers_count
from the GitHub API and a formatStarCount helper that renders it in
GitHub's compact repo-header style (e.g. "37.6k"). The landing header's
GitHub button now appends a star badge (faint divider + filled star +
count) on both the desktop and mobile menu entries.
Fetched client-side on purpose: LandingHeader is shared across every
marketing page, so one client fetch covers them all without threading a
server value through each render site, and each visitor calls the API
from their own IP, sidestepping the shared-outbound-IP rate limit the
server-side github-release fetcher works around with a PAT. The result
is memoized at module scope (plus in-flight dedupe); a failed fetch
caches null and the button degrades to the plain "GitHub" label.
* fix(landing): drop the star glyph from the GitHub star badge
In the GitHub button context the number already reads as the star count,
so the icon is redundant. Keep the divider + count only.
`GET /api/issues/:id/usage` endpoint (handler `GetIssueUsage`). Returns the
aggregated token usage for an issue, summed across all of its task runs.
The usage is already captured server-side and shown in the issue detail view,
but was not reachable from the CLI, so it couldn't feed billing/cost scripts.
This closes that gap with no backend changes.
$ multica issue usage MUL-139
INPUT_TOKENS OUTPUT_TOKENS CACHE_READ CACHE_WRITE RUNS
5625 83880 9190806 154078 1
$ multica issue usage MUL-139 --output json
{ "total_input_tokens": 5625, ... "task_count": 1 }
Accepts an issue key (`MUL-139`) or UUID, mirroring `issue runs`.
- Table cells use the existing `formatMetadataValue` helper so large cache-token
counts render as plain integers (not scientific notation).
- Scope is the per-issue aggregate the endpoint already returns (`task_count` =
run count); a per-run token breakdown is out of scope.
- `go test ./cmd/multica/ -run TestRunIssueUsage` (added) ✅
- `go vet ./cmd/multica/` ✅
- Verified against a live self-hosted server; numbers match the issue UI.
- `server/cmd/multica/cmd_issue.go` — command + handler
- `server/cmd/multica/cmd_issue_test.go` — unit test
- `CLI_AND_DAEMON.md` — docs
* feat(issues): unify run-enqueue decision behind WillEnqueueRun + preview endpoint
Collapse the issue update/batch enqueue copies into one service predicate
service.IssueService.WillEnqueueRun, shared verbatim with a new dry-run
endpoint POST /api/issues/preview-trigger so the four entry points stop
drifting (squad/self-loop/batch omissions, MUL-3375). The private-agent gate
stays at the HTTP boundary: write paths inject allow-all, preview injects the
real gate so it never leaks a private agent's readiness.
Add suppress_run to issue update/batch: the change applies but no run starts.
Remove the now-dead handler mirrors shouldEnqueueSquadLeaderOnAssign /
isSquadLeaderReady. service.Create and the comment trigger chain are untouched.
Tests: preview behavior, preview<->write-path match, batch aggregation,
member no-trigger, suppress_run skip, malformed-body 400.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): inject handoff note into assigned runs via first-class task field
Add an optional handoff_note carried by issue assign/promote into the run's
opening prompt and issue_context.md, via a dedicated agent_task_queue column
(migration 122) and a daemon assignment-handoff render branch — never a
fabricated comment, never trigger_comment_id (MUL-3375 §6.1).
Thread the note through enqueueIssueTask/enqueueMentionTask + WithHandoff
public variants and dispatchIssueRun; suppress_run or a parked write drops it
(no run = nothing to inject). Soft version gate: MinHandoffCLIVersion +
HandoffSupported, surfaced per-trigger as handoff_supported in the preview so
the UI can gray the note box on old daemons; the assignment never hard-fails.
Tests: daemon prompt + issue_context render via the assignment branch (not
quick-create/comment), version helper matrix, note persists on the task,
suppressed assign enqueues nothing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): leave a display-only handoff record on the timeline
When an assign/promote with a handoff note starts a run, write one
type='handoff' timeline record via TaskService.RecordHandoff — a direct
Queries.CreateComment + timeline event that bypasses Handler.CreateComment, so
it never reaches triggerTasksForComment and cannot start a second run
(MUL-3375 §6.2, the must-not-retrigger invariant). Author is the actor who
handed off; body is the note. Migration 123 admits the 'handoff' comment type.
Recorded only on a real run start: suppress_run or a parked write writes
nothing. enqueueSquadLeaderTask now reports whether it enqueued so the trace
is gated on an actual dispatch.
Test: exactly one handoff record on assign-with-note, exactly one task (no
re-trigger), and no record when suppressed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): frontend plumbing for issue-trigger preview + handoff (core)
Add api.previewIssueTrigger + IssueTriggerPreviewSchema (zod parseWithFallback),
the use-issue-trigger-preview hook, issueKeys.issueTriggerPreview(+All) with WS
queue-state invalidation, suppress_run/handoff_note on UpdateIssueRequest, the
'handoff' CommentType, and stripping of the control fields from optimistic
update/batch cache patches (MUL-3375 §9).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): exclude handoff records from new-comment counting
type='handoff' is a display-only timeline record, not conversation. Exclude it
from CountNewCommentsSince so a handoff note never inflates the count of
"new comments to catch up on" fed to a claiming agent (MUL-3375 §12). Analytics
already excludes it (RecordHandoff is a direct write that emits no analytics
event), and the comment-trigger path is already bypassed.
Test: a handoff record does not bump the new-comment count; a real comment does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): pre-trigger preview UI, handoff note, timeline card (web/desktop)
Wire the §9 frontend onto the preview endpoint + handoff fields:
- Delete the backlog blocking dialog (backlog-agent-hint*) and its modal type;
the over-eager nag is gone. Backlog awareness is now a passive label.
- RunConfirmModal: single assign + batch assign/status route here. Shows the
backend predicate's verdict ("将启动 @X" / "将启动 N 个" / parked), an optional
handoff note (assign only, soft-gated by handoff_supported), and 暂不启动 —
then applies via update/batch. No frontend guessing.
- create modal: passive CreateRunHint ("将启动 @X" / backlog parked).
- single status change stays a direct apply (unchanged).
- timeline: render type='handoff' as a distinct, non-interactive handoff card.
- i18n run_confirm + handoff_card across en/ja/ko/zh-Hans; drop backlog action
keys; locale parity green.
Tests: use-issue-actions (assign → run-confirm modal, member → direct),
create-issue + comment-card suites updated/green; views typecheck + lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): use a valid anchor in the handoff count-exclusion test
CountNewCommentsSince filters id <> @anchor_id; SQL id <> NULL is NULL and
excludes every row, so an empty anchor made the control assertion read 0. The
production caller always passes a real anchor — mirror that with a non-matching
sentinel uuid.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(issues): RunConfirmModal apply logic (start/suppress/note-gate/batch)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* test(core): preview schema malformed/missing/null fallback coverage
Cover IssueTriggerPreviewSchema via parseWithFallback (MUL-3375): well-formed
parse, top-level + item default fills (empty/older backend), and fallback to
{ triggers: [], total_count: 0 } for malformed shapes, a dropped required
issue_id, a wrong-typed total_count, and null/non-object bodies — so the four
entry points degrade to "nothing will start" instead of throwing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): remove display-only handoff timeline record (留痕)
The handoff "留痕" timeline record (type='handoff' comment written on run
start) was judged superfluous and dropped per product call. This removes
only the display-only trace; the handoff NOTE injection into the run's
opening prompt + issue_context.md is untouched.
- backend: drop RecordHandoff + its call in dispatchIssueRun
- db: drop the `type <> 'handoff'` exclusion in CountNewCommentsSince and
migration 123 (comment_type_check reverts to the 4-type set from 001);
no production data exists for this unreleased feature
- frontend: drop the "handoff" CommentType, HandoffCard, and handoff_card
i18n (all locales)
- tests: drop handoff_count_test.go and the record-write assertions in
issue_trigger_preview_test.go (note-injection tests retained)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): dismissable run-confirm modal + team-handoff copy
Two fixes to the pre-trigger confirm modal (MUL-3375).
1. Dismissable: switch RunConfirmModal from AlertDialog to the standard
shadcn Dialog so it has the close (X) button + Esc + click-outside.
Previously the only choices were "start" / "don't start now" with no
way to abort the action entirely; dismissing now cancels with no write.
2. Copy: rework the action-surface wording away from the backend term
"run" toward team-handoff voice — 指派 / 开始 / 交接 (run stays only on
record surfaces). Unifies the note's three names to "交接说明", and
parallels the rewrite across en/ja/ko.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* chore(agent): bump handoff note min CLI version to 0.3.28
The daemon release that renders handoff notes ships in 0.3.28 (0.3.27
was the prior tag), so move the soft-gate threshold up. Below this the
note is silently dropped and the frontend grays the note box — assignment
is never blocked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(issues): skip run-confirm when batch-moving issues to backlog
A move into backlog never starts a run (service/issue_trigger.go), so the
pre-trigger confirm modal degenerated to an empty "won't start" box with a
single Apply button — pure friction. Apply directly instead, matching the
single-issue status path. Other target statuses still route through the
modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(issues): refine pre-trigger preview hint and copy
- Move the create-issue run hint to a reveal band (grid 0fr→1fr) above the
property toolbar. It was sharing the footer button row and, lacking a
width constraint, reflowed the submit buttons whenever it appeared.
Restyle to a borderless, comment-style avatar+caption that is purely a
caption (non-interactive avatar).
- Distinguish squad from agent in the pre-trigger copy: a squad's leader
evaluates and delegates rather than "starting work" itself. Add
will_start_named_squad / will_start_squad / create_will_start_squad across
en/zh/ja/ko (reusing the squad_leader_* evaluate→arrange vocabulary) and
branch run-confirm + the create hint on squad assignees.
- Bold the assignee name in the run-confirm headline via a language-safe
sentinel split (no per-language prefix/suffix keys).
- Align zh "开始处理" → "开始工作" on the single-assign copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(issues): stub ActorAvatar in create-issue suite
CreateRunHint now renders an ActorAvatar for agent/squad assignees, which
pulls in getActorInitials/getActorAvatarUrl + the workspace/presence/navigation
hook tree. This form-focused suite only stubbed getActorName, so the
squad-forwarding test crashed with "getActorInitials is not a function". Stub
the avatar inert — its own behavior is covered elsewhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Walt <walt@multica.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): stop kanban card snapping back on drag
A cross-column drag on a non-position-sorted board left the card in its
origin column for the whole request, then jumped to the target only when
the mutation settled — the "snaps back, then moves" glitch. Root cause was
three coupled choices in the optimistic path:
- board-view never updated local columns on drop for sortBy != "position"
(onDragOver is a no-op there), so the card relied on the settle refetch
to move across.
- useUpdateIssue invalidated the whole list on settle, replacing the column
and re-landing the card even on success.
- patchIssueInBuckets appended a moved card to the column tail instead of
its position slot, so any later cache refresh teleported it to the end.
Fixes:
- board-view: optimistically move the card into the target column on drop
for the non-position path (insertIdByPosition), and reconcile local
columns from the cache on settle for both paths (revert on error now that
the list is no longer refetched).
- mutations: reconcile via onSuccess surgical patch of the returned entity;
drop the list/detail invalidation from onSettled (aggregates still flush).
- cache-helpers: patchIssueInBuckets inserts the moved/reordered card at its
position slot; a plain field update still keeps its slot.
Adds cache-helpers and drag-utils unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): patch My-Issues / Project board caches on move too
The drag fix made the board reconcile local columns from its feeding cache
on settle. The workspace board rides issueKeys.list (patched by onMutate),
but the My-Issues and Project boards ride the myList cache, which the
mutation did not patch — so a successful move snapped back on those boards.
useUpdateIssue now patches/snapshots/rolls back every bucketed list cache
(workspace list + myList), selected by the ListIssuesCache `byStatus` shape
so grouped (assignee) and flat (gantt) caches are skipped. Adds renderHook
regression tests covering both-cache optimistic move, both-cache rollback,
and no-list-invalidation-on-settle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): drop redundant WS position->list invalidate
onIssueUpdated already surgically patches the non-filtered workspace board
via patchIssueInBuckets (cross-status move + same-column reorder). The extra
`if (position) invalidateQueries(list)` re-pulled the whole board on top of
that, re-introducing drag flicker through the echoed-back WS event. Removed.
Filtered myAll lists still invalidate (membership can change there) — the
client-side membership reconciliation for those is a separate follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): batch update patches myList + stops list refetch on settle
- onMutate now patches both issueKeys.list and the filtered issueKeys.myAll
bucketed caches, so a batch edit on a My-Issues / Project board is
optimistic too. Previously only the workspace board was patched, so batch
edits on those boards relied entirely on the settle refetch.
- onSettled no longer invalidates issueKeys.list: the optimistic patch is a
complete reconcile for these bucketed boards (batch changes status /
priority / project, never a server-computed value), so a full-board
refetch only re-introduced the flicker the single-issue path removed.
Aggregate / grouped caches still refresh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): list view optimistically moves row on non-position drag
The sortBy != "position" branch called onMoveIssue without moving the row in
local columns, so the row sat in its origin group for the whole request and
only jumped across on settle -- the same snap-back the board view had before
its fix. Now mirrors board-view: setColumns(insertIdByPosition) on drop so
the settle rebuild is a visual no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): keep My-Issues/Project boards in place on non-membership WS change
onIssueUpdated now surgically patches the filtered myList (myAll) caches and
only invalidates them when the change can actually move an issue in/out of the
filter: an assignee change (covers My-Issues direct-assignee + the involves leg
+ actor panels) or a project change (Project board). A pure status / position /
priority / label change reconciles in place -- no refetch -- removing the last
drag flicker on filtered boards.
Uses the assignee_changed flag the server already sends on issue:updated
(surfaced on IssueUpdatedPayload + forwarded by the realtime dispatch); project
change is diffed client-side against the cached value. No predicate replication,
no backend change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): add settle-lock to swimlane drag (no clobber mid-flight)
The swimlane drag had no settle window: the resync useEffect (and the issueMap
freeze) guarded only isDraggingRef, so a cache change landing after drop but
before the move settled could rebuild localCells out from under the optimistic
move. Adds isSettlingRef + settleVersion (mirroring board-view / list-view): the
lock is held from drop until onMoveIssue settles, then released, forcing a
single resync from the reconciled cache.
onMoveIssue now accepts the same optional onSettled callback board/list already
use; the parent handleMoveIssue supplies it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* refactor(issues): extract shared useDragSettle hook for board + list
board-view and list-view carried byte-identical drag/settle scaffolding (the
local columns mirror, the dragging/settling locks, the post-move animation-frame
throttle, and the settle callback). That duplication is exactly what let
list-view silently drift earlier (it had lost the optimistic-move half of the
fix, and its position-branch settle callback omitted the settleVersion bump).
Extract the primitive into useDragSettle so both surfaces share one
implementation and can't drift again.
Behavior-preserving for board-view. For list-view the one intended alignment:
its position-branch failed move now reverts, gaining the settleVersion bump
board-view already had.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Agents creating sub-issues only saw the runtime brief's Sub-issue
Creation section, which taught the manual todo/backlog serial chain and
never mentioned stages — the `--stage` flow was documented only in the
multica-working-on-issues skill, which an agent reads only if it opens
it. So agents defaulted to hand-managed backlog chains and rarely reached
for stages.
- Add an "Ordering with stages" paragraph to the brief's Sub-issue
Creation section nudging agents to group ordered/waiting sub-issues
with --stage instead of hand-promoting a backlog chain.
- List --stage on the brief's issue create / update command lines and
add multica issue children to the Core command list for discoverability.
- Extend the brief test with the new stage assertions.
The Sub-issue Creation section stays gated to issue-bound runs (skipped
for chat/quick-create/autopilot), unconditional on parent_issue_id, and
free of parent-notification guidance — all existing canaries still pass.
MUL-3508
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Replace the # (Hash) icon for the Stage property with the Milestone icon
across the picker trigger, dropdown option rows, and the Add-property menu.
Shrink the Stage dropdown option font to text-xs (scoped to the Stage
picker only; the shared PickerItem keeps text-sm so other property
dropdowns are unaffected).
* 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 <github@multica.ai>
* 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 <github@multica.ai>
* 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 <github@multica.ai>
* 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 <github@multica.ai>
* 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 <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Fix GitHub pull_request and check_suite webhook routing so events are attributed to the workspace that registered the repository, with fallback to the installation workspace. Includes host-qualified repo matching, account-gated registry routing, deterministic matching, and regression coverage.
* feat(daemon): inject project description into the agent brief
Issues bound to a project only surfaced the project title in the runtime
brief; the project description (durable, project-wide context the owner
sets) was loaded but dropped. Carry it end-to-end:
- claim handler reads proj.Description onto the response (issue-bound and
quick-create paths)
- new ProjectDescription field on AgentTaskResponse, daemon Task, and
TaskContextForEnv
- rendered in the brief's `## Project Context` section and written to
.multica/project/resources.json as project_description
Empty descriptions render nothing (no extra heading). Updated the
projects-and-resources built-in skill docs in the same change.
MUL-3465
Co-authored-by: multica-agent <github@multica.ai>
* feat(projects): clarify project description is injected as agent context
The project description is now durable context injected into every task's
brief, but the UI still presented it as a plain "Description" field, so
existing descriptions could silently become agent input. Add a hint under
the description editor on the project detail page and in the create-project
modal, in all four locales, stating it is shared with agents as context for
every task in the project. No data-semantics change.
Addresses review feedback on PR #4395. MUL-3465
Co-authored-by: multica-agent <github@multica.ai>
* test(handler): assert project description flows through task claim
The execenv tests cover brief rendering, but nothing pinned the claim
handler boundary where proj.Description is read onto the response. Add
two tests — issue-bound and quick-create paths — so a regression in that
assignment fails loudly instead of silently dropping the description.
Addresses review feedback on PR #4395. MUL-3465
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(agent): Qoder ACP runtime, chat reconnect recovery, and task linkage
- Add Qoder CLI backend (ACP transport, model discovery, blocked-args policy)
- Wire daemon/runtime config, docs, and UI provider assets
- Retry terminal task reports; add backoff unit tests
- Chat: SQL attach user message to task; handler + optimistic cache reconcile
- Invalidate chat/task-messages caches on WS reconnect; extract helper + tests
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: drop non-Qoder changes (chat reconnect, task link, terminal report retries)
Keep only Qoder runtime, docs, daemon config/execenv, and UI provider assets.
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(agent): harden Qoder ACP drain and wire project skills path
- Stop streaming to msgCh after reader wait so grace timeout cannot race close
- Resolve injected skills to .qoder/skills per Qoder CLI discovery
- Update AGENTS.md skill copy and add execenv tests
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(qoder): add provider logo and wire MCP config into ACP sessions
- Add inline SVG QoderLogo component to provider-logo.tsx, replacing
the generic Monitor icon placeholder
- Add convertMcpConfigForACP helper to convert Claude-style MCP server
config (object map) into ACP array format for session/new and
session/resume
- Add unit tests for convertMcpConfigForACP covering stdio, SSE,
empty/nil, and multi-server cases
Co-authored-by: Orca <help@stably.ai>
* fix(test): capture both return values from InjectRuntimeConfig in Qoder test
Co-authored-by: Orca <help@stably.ai>
* fix(qoder): preserve remote MCP headers and promote provider errors
Addresses review feedback on #2461 (Bohan-J): two runtime-correctness
issues in the Qoder ACP backend.
1. Remote MCP headers were dropped. The bespoke convertMcpConfigForACP
only forwarded url/type, so an authenticated remote MCP server looked
configured in Multica but failed inside the Qoder session. Replace it
with the shared buildACPMcpServers helper (same path Hermes/Kimi/Kiro
use), which preserves headers as [{name, value}], sorts for
deterministic output, and handles remote transport aliases. Fail
closed on malformed mcp_config instead of silently dropping servers.
2. Provider failures could report as completed tasks. stderr was wired
via io.MultiWriter and the result was only promoted to failed when
output was empty, so a terminal upstream error (HTTP 429 / expired
token) racing a stopReason=end_turn with text still became
"completed". Switch to StderrPipe + an explicit copier, drain it
(bounded by the existing grace window, since qodercli can leave a
child holding the inherited fds) before the decision, and run the
shared promoteACPResultOnProviderError.
Tests: replace the convertMcpConfigForACP unit tests with two
end-to-end Qoder tests — one asserts the Authorization header reaches
the session/new payload as {name, value}, the other asserts a terminal
stderr error with non-empty output reports failed.
Co-authored-by: Orca <help@stably.ai>
* fix(qoder): align ACP session handling
Co-authored-by: Orca <help@stably.ai>
* fix(agent): guard qoder late output after drain
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The batch action toolbar hardcoded status="todo", priority="none", and a
null assignee, so the status/priority/assignee pickers always checked a
fixed row regardless of the selected issues. The batch write itself worked,
but the picker mis-reported the current value, surfacing as "status always
defaults to todo" (MUL-3510). The same defect applied to priority and
assignee, across all five toolbar mount points.
Derive the shared status/priority/assignee of the selected issues via a new
commonIssueFields helper and feed it to the pickers; when the selection is
mixed, pass an empty value so no row is checked. Pickers now accept a
nullable current value, and AssigneePicker gains a `mixed` flag to
distinguish an all-unassigned selection (check "No assignee") from a mixed
one (check nothing). Each call site passes its issue universe, mirroring the
skill list's selected-rows approach.
Adds unit tests for commonIssueFields and a toolbar picker-wiring test.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Extend the clean target beyond server binaries/temp files to also
remove Next.js/source/Expo/Electron build outputs, Turbo caches, and
tsbuildinfo files across apps and packages.
The header 'agent is working' chip previously required a click to reveal the
activity card. Open it on hover instead so the live signal reads as a
glanceable status surface. The hover config lives on Base UI's Popover.Trigger
(openOnHover + delay/closeDelay), and the trigger stays a real button so
click/keyboard access is retained for touch and a11y.
Add a regression test asserting openOnHover is wired on the trigger so a
click-only implementation can no longer pass.
MUL-3507
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Add a Join Discord promo card pinned to the bottom of the left sidebar
(above the help launcher). Dismiss state persists per-user in
localStorage so it stays hidden once closed.
Extract the shared DiscordIcon + invite URL into layout/discord.tsx so
the help launcher and the card reuse one source. i18n copy added for
en / zh-Hans / ja / ko.
MUL-3505
Co-authored-by: multica-agent <github@multica.ai>
Add a Discord invite (https://discord.gg/W8gYBn226t) in three places:
- Website footer: social icon + link in the Resources group (en/zh/ja/ko)
- In-app help menu: Discord item in the help launcher (all 4 locales)
- GitHub repo README: badge + link (README.md and README.zh-CN.md)
MUL-3492
Co-authored-by: multica-agent <github@multica.ai>
When a sub-issue transitions to done, notifyParentOfChildDone posts a
system comment on the parent and wakes the parent's assignee. A parent
deliberately parked in backlog should stay inert: waking it lets the
assignee promote sibling backlog sub-issues into todo, which is the
unwanted auto-activation reported in GitHub Discussion #4320 (MUL-3497).
Add a backlog guard alongside the existing done/cancelled guard so the
whole notification (comment + mention + trigger) is skipped until the
user explicitly moves the parent out of backlog.
MUL-3497
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
The daemon auto-grants Codex item/permissions/requestApproval requests by
echoing back the network / fileSystem profile scoped to the current turn.
Previously a malformed params payload and any permission key outside
network / fileSystem were dropped silently, so a future app-server
protocol that adds a new permission shape would be narrowed away with no
trace in daemon logs.
Log both cases (parse failure and dropped keys) without changing the
granted response. Addresses review nits on #4346 / MUL-3451.
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
* feat(lark): reply inside the originating thread (话题) instead of the group
When a user @-mentions the bot inside a Lark topic/thread, the bot now
replies back into that thread rather than posting a fresh message at the
chat level. Behavior is automatic and scoped: only triggers that were
themselves inside a thread get a threaded reply, so normal group/p2p
chats are unchanged.
The outbound path is event-driven and decoupled from the inbound
message, so the trigger message_id + thread_id are persisted on
lark_chat_session_binding (migration 122) at ingest time. The patcher
then routes the agent reply (text / markdown card / error card) and the
OutcomeReplier notices (/issue confirmation, offline/archived) through
Lark's reply endpoint with reply_in_thread=true when a thread is present,
falling back to a chat-level send if the threaded reply fails.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(lark): classify thread-reply failures before chat-level fallback
Only retry a threaded reply at the chat level when Lark returns an
explicit "this message/topic cannot receive a threaded reply" error
(recalled trigger, topic gone, topics disabled, aggregated message,
etc.). Transport errors, 5xx, timeouts, rate limits, and ambiguous
failures are now logged and returned as failures instead of being
retried, so we never duplicate a reply or leak a thread-only reply
into the main group chat.
The three reply-capable send methods now return a structured *APIError
carrying the Lark business code, and isThreadReplyUnsupported drives
the fallback via an allowlist. sendWithThreadFallback is promoted to a
package-level function so the immediate OutcomeReplier sends (/issue
confirmation, offline/archived notices) share the same classified
fallback path instead of silently swallowing thread-reply failures.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: kun <kuen@micous.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Skill import builds raw.githubusercontent.com URLs by hand and fetches them
via fetchRawFile, which sent no Authorization header. GitHub API calls were
authenticated by #2215 (doGitHubAPIGet/addGitHubAuthHeader) but the raw
content download path was missed, so importing a skill from a private/internal
GitHub repo listed the directory fine and then 404'd on the actual file
download, surfacing as a generic 502.
Attach the existing GITHUB_TOKEN bearer header in fetchRawFile, but only when
the URL host is raw.githubusercontent.com. fetchRawFile is shared with
clawhub.ai / skills.sh downloads, so the token must not leak to those hosts.
The host gate is extracted into newRawFileRequest so it is unit-testable
without a live round-trip.
MUL-3496
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
When an issue is assigned to a squad, only the leader is triggered. The
leader briefing's Squad Roster listed each member's name, type, role, and
mention link — but not the member's assigned skills, so the leader had to
infer capability from the free-text role label when deciding who to
delegate to.
renderMemberRow now loads each agent member's assigned skills via
ListAgentSkillSummaries and formatRosterRow renders them as
"skills: a, b" (or "no skills assigned" when the agent has none). Builtin
multica-* skills are excluded (they live outside agent_skill); human
members carry no skills segment; a skill-lookup error degrades to the
prior name+role row rather than asserting a misleading "no skills".
Operating-protocol step 1 now tells the leader to match the task to each
member's listed skills.
Updates the multica-squads builtin skill and its source map to document
the new roster content, and adds
TestBuildSquadLeaderBriefing_MemberSkillsInRoster.
Co-authored-by: hal9000botagent <hal9000botagent@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): autofocus OTP input on verification step
The email-verification step renders the OTP input without focus, so
users must click the field before typing the code. This is friction on
every login, especially when switching accounts.
Add `autoFocus` to the InputOTP so the cursor lands in the field as
soon as the step mounts. Mirrors the existing email-step input and the
mobile OTP component, both of which already autofocus.
* test(web): polyfill document.elementFromPoint for input-otp in jsdom
Autofocusing the OTP input makes input-otp run its focus-time DOM
measurement, which calls document.elementFromPoint. jsdom doesn't
implement it, so the web login test threw an unhandled error.
packages/views/test/setup.ts already stubs this for the same reason;
mirror the stub in the web test setup (which already stubs
ResizeObserver for input-otp).
* test(auth): assert OTP input autofocuses on verification step
Guards the autofocus behavior: the test fails if the autoFocus prop is
removed from the verification-step InputOTP. Lives in packages/views
since it covers shared component behavior, mocking @multica/core.
* feat(web): add opt-in react-grab dev element inspector
Loads the react-grab overlay (hold ⌘C / Ctrl+C + click to copy an
element's source path + component stack) only when REACT_GRAB is set in
a local, gitignored apps/web/.env.local. Both the NODE_ENV and REACT_GRAB
guards are evaluated server-side in the root layout, so the <Script> tag
is omitted from the HTML for anyone who hasn't opted in — no effect on
other developers or production.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(desktop): add opt-in react-grab dev element inspector
Mirrors the web wiring for the Electron renderer: injects the react-grab
overlay (hold ⌘C / Ctrl+C + click to copy an element's source path +
component stack) only when VITE_REACT_GRAB is set in a local, gitignored
apps/desktop/.env.development.local. Guarded by import.meta.env.DEV so the
branch is tree-shaken out of production builds; never activates for other
developers. No CSP/sandbox blocks the unpkg script (webSecurity is off).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(web): unify react-grab opt-in var to VITE_REACT_GRAB
Use the same env var name as the desktop renderer so one variable name
controls both apps. The desktop renderer is bundled by Vite, which only
exposes VITE_-prefixed vars to client code, so the shared name must carry
the VITE_ prefix; web reads it server-side where the name is unconstrained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- expose `--callback-host` on `multica setup` and `multica setup cloud`, reusing the existing login callback override
- print an SSH tunnel hint when browser login runs in an SSH session with a loopback callback
- show local flags in parent-command help so `multica setup --help` documents the callback option
Fixes#4357
## Tests
- `go test ./cmd/multica -run 'TestCallbackHostFlagValueReadsParentSetupFlag|TestSetupHelpShowsCallbackHostFlag|TestSetupCallbackHostFlagWiring|TestBrowserLoginInstructionsSSHRemoteHint'`
- `go run ./cmd/multica setup --help`
- `go run ./cmd/multica setup cloud --help`
- `git diff --check`
- `go test ./...`
* fix(projects): require admin for project deletion
* test(projects): clean up orphaned member rows in delete-permission helper
The schema uses no foreign keys or cascades, so deleting the test user
left its member row behind in the shared test workspace, polluting later
tests in the package. Delete the member row before the user in both the
pre-seed cleanup and t.Cleanup.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>