* docs(plans): chat attachment & image support implementation plan
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* feat(db): add chat_session_id/chat_message_id to attachment
Co-authored-by: multica-agent <github@multica.ai>
* feat(db): sqlc — chat_session_id on CreateAttachment + LinkAttachmentsToChatMessage
Co-authored-by: multica-agent <github@multica.ai>
* feat(file): upload-file accepts chat_session_id form field
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): SendChatMessage links uploaded attachments to the new message
Co-authored-by: multica-agent <github@multica.ai>
* feat(api): uploadFile accepts chatSessionId; sendChatMessage accepts attachmentIds
Co-authored-by: multica-agent <github@multica.ai>
* feat(core): useFileUpload supports chatSessionId context
Co-authored-by: multica-agent <github@multica.ai>
* feat(chat): support paste/drag/upload attachments in chat input
Co-authored-by: multica-agent <github@multica.ai>
* test(e2e): chat input attachment upload + send round-trip
Co-authored-by: multica-agent <github@multica.ai>
* chore(chat): keep lazy-created session title empty so untitled fallback localizes
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): address review — dedupe ensureSession + parse upload response
- chat-window: cache in-flight createSession promise in a ref so a file drop
followed by a quick send no longer spawns two sessions (and orphans the
attachment on the losing one).
- Attachment type + EMPTY_ATTACHMENT + AttachmentResponseSchema: include the
new chat_session_id / chat_message_id fields the server now returns.
- uploadFile: route the response through parseWithFallback so a malformed
body returns EMPTY_ATTACHMENT instead of an undefined-keyed Attachment,
matching the API boundary rule.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): address PR #2445 review — test ctx, send gating, attachment surface
1. Backend test was 400ing because the handler reads workspace from
middleware-injected ctx, and `newRequest` only sets the header. Helper
`withChatTestWorkspaceCtx` mirrors the agent-access-test pattern and
loads the member row + SetMemberContext before invoking the handler.
2. Attachment metadata now flows end-to-end:
- new sqlc `ListAttachmentsByChatMessageIDs` (batch lookup, mirrors the
comment-side query)
- `chatMessageToResponse` takes `attachments` and `ChatMessageResponse`
surfaces them — same shape as CommentResponse
- `ListChatMessages` loads them via a new `groupChatMessageAttachments`
helper so the chat bubble can render file cards
- daemon claim path pulls `ListAttachmentsByChatMessage` for the latest
user message and ships `ChatMessageAttachments` to the daemon
- `buildChatPrompt` lists id+filename+content_type and instructs the
agent to `multica attachment download <id>` — fixes the private-CDN
expiring-URL problem where the markdown URL would have expired by
the time the agent acts
- TS `ChatMessage` gains an optional `attachments` field
3. Chat composer now blocks send while uploads are in flight:
- `pendingUploads` counter increments in handleUpload, SubmitButton
uses it to disable
- handleSend also gates on `editorRef.current.hasActiveUploads()` to
catch the Mod+Enter path that bypasses the button
- new vitest covers the "drop large file → immediate send" scenario
where attachment id would otherwise be silently dropped
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
* chore: drop implementation plan doc
Process artefact, not something the repo needs to keep.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Three user reports converge on the same Windows-shell encoding bug:
- #2198 / #2236 — Chinese, Codex on Win11. Comments / descriptions
generated by the agent arrive as `?`.
- #2376 — Cyrillic, non-Codex agent ("Ops Lead") on Win11 Desktop.
Title preserved (argv → CreateProcessW UTF-16), description / agent
reply garbled (stdin → shell-codepage re-encoding).
woodcoal's independent diagnosis on #2198 confirms the root cause:
Windows PowerShell 5.1's `$OutputEncoding` defaults to ASCIIEncoding
when piping to a native command, so non-ASCII bytes are silently
replaced with `?` before they reach `multica.exe`. The CLI's stdin
parsing is fine; the bytes are corrupted upstream, in the agent's
shell layer.
This PR ships the fix that supersedes the codex-only attempt in
PR #2265 (which is closed in favour of this one):
## CLI
Add `--content-file <path>` to `multica issue comment add` and
`--description-file <path>` to `multica issue {create,update}`. The
CLI reads bytes off disk via `os.ReadFile` and skips the shell
entirely; UTF-8 survives end-to-end regardless of `$OutputEncoding`
or `chcp`. The three input modes (`--content`, `--content-stdin`,
`--content-file`) are mutually exclusive.
## Runtime config
`buildMetaSkillContent`'s Available Commands section is rewritten as a
neutral three-mode menu. The previous unconditional "MUST pipe via
stdin" / `--description-stdin` mandate (over-spread from #1795 /
#1851's Codex-multi-line fix) is gone for non-Codex providers; the
strong directive now lives only in the Codex-Specific section, which
branches on host:
- Codex / Linux+macOS: `--content-stdin` + HEREDOC (preserves MUL-1467
fix against codex's literal `\n` habit).
- Codex / Windows: `--content-file` (PowerShell ASCII pipe is the
exact bug we're patching).
## Per-turn reply template
`BuildCommentReplyInstructions` now takes a provider arg and branches
provider × OS:
- Windows + any provider → `--content-file` (the bug is shell-layer,
not provider-layer; #2376 shows non-Codex agents on Windows also
hit it). All providers write a UTF-8 file with their file-write tool
and post via `--content-file ./reply.md`.
- Linux/macOS + Codex → stdin/HEREDOC (MUL-1467 protection).
- Linux/macOS + non-Codex → lightweight pre-#1795 inline
`--content "..."`. The CLI server-side decodes `\n`, so escaped
multi-line works; the agent retains stdin / file as escape hatches
for richer formatting.
`BuildPrompt` and `buildCommentPrompt` gain a `provider` arg;
`daemon.runTask` already has it in scope.
## Tests
- `TestResolveTextFlag` — file-source verbatim with non-ASCII
(`标题 / Заголовок / 中文段落`), missing-file error, empty-file
rejection, three-way mutual exclusion.
- `TestInjectRuntimeConfigAvailableCommandsIsNeutral` — every
non-Codex provider × {linux, darwin, windows} pins the three-mode
menu present + over-spread "MUST stdin" substrings absent.
- `TestInjectRuntimeConfigCodexLinuxEmphasizesStdin` +
`TestInjectRuntimeConfigCodexWindowsUsesContentFile` — Codex
section's per-OS branch.
- `TestBuildCommentReplyInstructionsCodexLinux` +
`TestBuildCommentReplyInstructionsNonCodexLinux` +
`TestBuildCommentReplyInstructionsWindowsUsesContentFile` — the
reply-template provider × OS matrix.
- `TestInjectRuntimeConfigWindowsCommentTriggerHasNoStdin` — end-to-end
AGENTS.md / CLAUDE.md on Windows has no prescriptive stdin
directive, for claude / codex / opencode.
`go test ./...` and `go vet ./...` clean.
Closes#2198, #2236, #2376.
Co-authored-by: multica-agent <github@multica.ai>
* feat(quick-create): add project picker that remembers last pick
Quick-create users targeting one project repeatedly had to restate "in
project X" in every prompt. The modal now exposes a project picker beside
the agent picker, persists the selection per-workspace, and pins the
agent's `multica issue create` invocation to that project so the prompt
text doesn't have to.
The picked project also flows to the daemon as ProjectID/ProjectTitle and
its github_repo resources override the workspace repo fallback — same
treatment issue-bound tasks already get.
Co-authored-by: multica-agent <github@multica.ai>
* fix(quick-create): move project picker into property pill row
Reviewer feedback: the picker felt out of place wedged next to the agent
header. Move it into a property toolbar row above the footer, reusing the
shared `ProjectPicker` + `PillButton` so its placement and styling line up
exactly with the manual create panel.
This also drops the bespoke dropdown / aria / label strings that were only
needed while the picker rendered inline beside "Created by".
Co-authored-by: multica-agent <github@multica.ai>
* fix(quick-create): clear stale persisted project + carry across mode switch
Two review-blocking bugs in PR #2321:
1. The stale-id sweep in AgentCreatePanel only fired when projects.length > 0
and only cleared local state, leaving lastProjectId pointing at a deleted
project. The next open re-seeded the dead UUID and submit hit the server's
`project not found` rejection. Gate on the query's `isSuccess` so we can
tell "loading" apart from "loaded as empty", and clear both local state
and the persisted preference when the selection isn't in the resolved list.
2. ManualCreatePanel's switchToAgent dropped the picked project from the carry
payload, so flipping manual → agent silently fell back to the agent panel's
own lastProjectId — potentially routing the issue to a different project
than the one shown in manual mode. Forward project_id alongside prompt /
agent_id, and add a regression test.
Co-authored-by: multica-agent <github@multica.ai>
* test(quick-create): pass new isExpanded props in stale-project tests
Main got an expand button on AgentCreatePanel via #2320 while this branch
was open, adding `isExpanded` / `setIsExpanded` to the panel's required
props. The two new stale-project tests still passed `{ onClose }` only,
which CI's typecheck (run on the main+branch merge) caught while my
local run did not.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929)
The cursor-paginated /timeline and /comments endpoints were sized for a
problem the data shape doesn't have: prod p99 is ~30 comments per issue
and the all-time max is ~1.1k. Time-based pagination also splits reply
threads across page boundaries (orphan replies), which the frontend was
papering over with an "orphan rescue" that promoted disconnected replies
to top-level — confusing UX with no real benefit.
Replace both endpoints with a single full-issue fetch, capped server-side
at 2000 rows as a defensive safety net (never hit in practice).
Server
- /api/issues/:id/timeline now returns a flat ASC TimelineEntry[]
(matches the legacy desktop contract — older Multica.app builds keep
working because the wrapped TimelineResponse + cursors are gone, and
the raw array shape was always what they consumed).
- /api/issues/:id/comments drops limit/offset; only ?since is honoured
for the CLI agent-polling flow.
- Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest
and the timelineCursor encoding.
- Replace with ListCommentsForIssue / ListCommentsSinceForIssue /
ListActivitiesForIssue (capped by argument).
CLI
- multica issue comment list drops --limit / --offset and the X-Total-Count
reporting; --since is preserved for incremental polling.
Frontend
- Replace useInfiniteQuery with useQuery in useIssueTimeline; drop
fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount.
- Remove timeline-cache helpers (mapAllEntries / filterAllEntries /
prependToLatestPage) and the TimelinePage / TimelinePageParam types.
- WS event handlers update the single flat-array cache directly.
- Drop the orphan-reply rescue in issue-detail — every reply's parent
is now guaranteed to be in the same array.
- Strip the "show older / show newer / jump to latest" buttons and their
i18n strings.
Co-authored-by: multica-agent <github@multica.ai>
* fix(timeline): address review feedback on pagination removal
Three issues caught in PR #2322 review:
1. /timeline broke for stale clients between #2128 and this PR. They send
?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema;
the new flat-array response was failing schema validation and falling back
to an empty timeline. Restore the wrapped shape on those query params
(DESC entries, null cursors, has_more_*=false), keeping the flat ASC array
for bare requests. Around-mode now also fills target_index from the merged
slice so legacy clients can still scroll-to-anchor without a follow-up.
2. The agent prompts in runtime_config.go and prompt.go still told agents
that `multica issue comment list` accepts --limit/--offset and to use
`--limit 30` on truncated output. With those flags removed in this PR,
new agent runs would hit "unknown flag" or skip context. Update the
prompt copy to "returns all comments, capped at 2000; --since for
incremental polling".
3. useCreateComment's onSuccess was a bare append to the timeline cache
with no id-dedupe, so a fast comment:created WS event firing before
onSuccess produced a transient duplicate. Restore the id guard the old
prependToLatestPage helper used to provide.
Adds two new boundary tests:
- TestListTimeline_LegacyWrappedShape_OnPaginationParams
- TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex
Co-authored-by: multica-agent <github@multica.ai>
* test(handler): fix timeline test assertions for handler-package isolation
The TestListTimeline_* assertions assumed CreateIssue would seed an
"issue_created" activity_log row, but the activity listener that publishes
those rows is registered in cmd/server/main.go — handler-package tests
don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the
test expected ≥6.
Drop the auto-activity assumption: assert exactly 5 entries in
TestListTimeline_MergesCommentsAndActivities, and tighten
TestListTimeline_EmptyIssue to assert a fully-empty timeline.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): tighten quick-create prompt to drop meta-instructions and apologetic Context
The quick-create prompt was producing descriptions that:
1. Echoed routing meta-instructions ("create an issue for me", "cc @X") into
the User request body, even though those phrases are handled by separate
CLI flags and are not spec content.
2. Emitted a Context section to apologize for resources it could not fetch
(e.g. an image attachment not piped through to the run), instead of
staying silent and letting the executing agent ask the user.
3. Preserved pure conversational fillers ("对吧?", "嗯", "那个…") because the
model treated removing them as forbidden paraphrasing.
Updates the prompt to call out each of these as explicit non-spec material
to strip before writing the description, while keeping the "high fidelity /
no paraphrasing of substantive content" invariant. Adds a regression test
that locks in the new rules at the substring level.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): preserve cc mention links in quick-create description
Stripping "cc @Y" wholesale would have lost the mentioned member's only
routing channel: `multica issue create` has no --subscriber/--cc flag, and
the platform auto-subscribes members by parsing `[@Name](mention://member/<uuid>)`
links from the description body. Without the mention link in the body, a
cc'd member would never get subscribed or notified.
Updates the prompt to:
- Strip only the verbal "cc" wrapper from the User request body.
- Append a trailing `CC: <mention links>` line to the description so the
platform's auto-subscribe logic still picks the mentions up.
- Spell out the contrast for assignee mentions, where --assignee-id is
the routing channel and the body should not double-encode the mention.
Also adds a substring assertion for the "Pure conversational fillers" rule
that was missing from the original regression test.
Co-authored-by: multica-agent <github@multica.ai>
* refactor(daemon): trim quick-create prompt rules to general principles
Reviewers pointed out the previous rewrite traded one prompt smell (over-
permissive verbatim quoting) for another (too many specific rules and
exhaustive bilingual example tables). Rewrites the description block as
general principles with a single representative example each, trusting the
model to generalize:
- "Strip non-spec material before writing" replaces the multi-bullet list
of routing-meta-instruction and conversational-filler enumerations.
- "Include Context only when references were fetched and produced facts;
never use it as an apology log" replaces the three "Do NOT emit a
Context section to" sub-bullets.
- The CC exception (the only operationally non-obvious rule, since
`multica issue create` has no --subscriber flag) is kept inline as a
single sentence and is still locked in by the regression test.
Net: ~16 fewer lines of prompt text without losing any of the rules the
test asserts.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968)
Opening an issue from Inbox with thousands of timeline entries used to
hard-freeze the browser tab on a synchronous render of every comment +
activity. The whole pipeline was unbounded: the API returned every row,
TanStack Query cached the full array, and IssueDetail mounted N
CommentCards (each running a full react-markdown + lowlight pipeline)
in one frame.
This swaps the timeline endpoint to keyset cursor pagination and rewires
the frontend to useInfiniteQuery so a long issue costs the same as a
short one on first paint.
API:
- GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex)
+ ?limit (default 50, max 100); response wraps entries with next/prev
cursors and has_more flags. Cursors are opaque base64 (created_at, id).
- ?around=<entry_id> anchors a window on the target so Inbox notifications
pointing at an old comment never trigger the freeze.
- New composite indexes on (issue_id, created_at DESC, id DESC) replace
the redundant single-column ones so keyset queries are index-only scans.
- /issues/:id/comments default branch now caps at 50 instead of returning
every row unbounded; the unbounded ListComments / ListActivities sqlc
queries are deleted.
Frontend:
- useIssueTimeline switches to useInfiniteQuery, exposes
fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount.
- WS handlers respect the at-latest invariant: comment/activity:created
prepends to pages[0] only when the user is reading the live tail;
otherwise it just bumps a counter so the UI offers a "Jump to latest"
affordance without yanking scroll.
- Optimistic mutations adapted to the InfiniteData shape via shared
helpers (mapAllEntries / filterAllEntries / prependToLatestPage in
core/issues/timeline-cache.ts) and use setQueriesData so all open
windows of the same issue stay in sync.
- IssueDetail Activity section gets a TimelineSkeleton placeholder
during the brief load window plus subtle text-link load-more buttons
matching the existing Subscribe affordance (no Button chrome). Top
uses a divider for boundary clarity; bottom shows
"Jump to latest · N new" weighted slightly heavier when there's
unread state.
- highlightCommentId now flows into the hook's around parameter so
Inbox jumps fetch the surrounding 50 entries directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(agent): default comment list to 50 + prompt hint about long issues
The CLI's "multica issue comment list" used to default to --limit 0
(meaning "fetch every comment"), which lets an agent on a long issue
fill its context window with thousands of rows. The default is now 50;
agents that need older history can pass --limit or --since explicitly.
The local-coding-agent prompt also gains a single-line note about this
in both the comment-triggered and on-assign flows so the agent knows to
scope its fetches when issue size is unknown. Autopilot run-only mode
is intentionally unchanged — it has no issue context to query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): add --assignee-id / --to-id / --user-id for unambiguous targeting
`multica issue {create,update,list}`, `issue assign`, and `issue subscriber
{add,remove}` accepted only fuzzy name matching, which fails in workspaces
where one user's name is a substring of another (e.g. agent "J" vs
"Cursor - J" / member "Jiayuan"). #1642 added UUID acceptance through the
existing flags, but there was still no explicit path that signals "this is a
UUID, not a name" — important for scripts that read IDs from
`multica workspace members --output json`.
Adds an `-id`-suffixed counterpart for every assignee-taking flag:
- `issue list` : --assignee-id
- `issue create` : --assignee-id
- `issue update` : --assignee-id
- `issue assign` : --to-id
- `issue subscriber {add,remove}` : --user-id
The new flags route through `resolveAssigneeByID`, a strict resolver that
requires a canonical UUID and fails with a clear error when the entity is
not in the workspace (no name fallback). A shared `pickAssigneeFromFlags`
helper enforces mutual exclusion between the name and id flags so a script
that accidentally sets both never silently applies one over the other.
Refs MUL-1254.
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): detect assignee flag presence via Changed, not value-emptiness
`pickAssigneeFromFlags` previously branched on `flag value != ""`, so
explicitly passing an empty UUID silently routed through the "no flag set"
path:
multica issue list --assignee-id "" # listed every issue
multica issue create --assignee-id "" # created an unassigned issue
multica issue subscriber add --user-id "" # subscribed the caller
This is exactly the failure mode the strict-UUID flag was added to prevent —
a script interpolating `--assignee-id "$MAYBE_UUID"` against a missing env
var should fail loudly, not silently degrade to a different operation.
Switch the picker (and the assign-command top-level guard) to use
`Flags().Changed`, so an explicit empty value reaches `resolveAssigneeByID`
/ `resolveAssignee` and surfaces a clear "expected a canonical UUID" /
"no member or agent found matching" error.
Co-authored-by: multica-agent <github@multica.ai>
* docs(cli): cover --assignee-id / --to-id in user docs and quick-create prompt
Follow-up to the --*-id flag rollout: surface the new flags everywhere the
old ones are documented so users (and agents) can discover them.
- assigning-issues.{mdx,zh.mdx}: the page explicitly calls out the
duplicate-name footgun ("first one listed wins, so rename before
assigning") — replace that workaround with a --to-id <uuid> example
- cloud-quickstart.{mdx,zh.mdx}: add a --to-id hint after the substring-
match callout so first-time users learn about the strict path
- internal/daemon/prompt.go (quick-create injected prompt):
- default-to-self: pass --assignee-id <task.Agent.ID> instead of
--assignee <name>; the picker agent's UUID is already in scope and
UUID matching is unambiguous in workspaces with overlapping agent
names (J / Cursor - J / Pi - J etc.)
- user-named: tell the agent to prefer --assignee-id <uuid> using the
user_id/id from the JSON it already fetched; --assignee <name> stays
a fallback for unambiguous workspaces
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
The previous description rule ("stay faithful + keep it concise") caused
agents to over-compress user input into vague single-sentence summaries,
losing context that the executing agent needs.
Key changes:
- Replace "keep it concise" with structured two-section format:
User request (faithful restate) + Context (verifiable external facts)
- Add hard rules against information compression and semantic downgrading
- Remove "one-line description" phrasing (UI supports richer input)
- Strip redundant behavioral rules from issue_context.md (already
covered by AGENTS.md guardrails and per-turn prompt)
Co-authored-by: multica-agent <github@multica.ai>
The description rule in buildQuickCreatePrompt() instructed the agent to
"always provide a rich, self-contained description" and "spell out what
needs to be done", which caused the agent to fabricate detailed product
specs, implementation phases, and design decisions from a one-line input.
Replace with a faithfulness-first rule: enrich with factual context
(fetched PR details, linked resources) but never invent requirements,
design decisions, or constraints the user did not express.
Fixes MUL-1605
* feat(quick-create): enrich issue title and description with URL context
Update the quick-create agent prompt to fetch context from URLs in user
input (GitHub PRs, issues, web pages) before creating the issue. The
agent now produces semantically rich titles (e.g. "Review PR #123:
Refactor auth to OAuth2" instead of "review PR #123") and includes
summarized link content in the description so issues are self-contained.
* refactor(quick-create): let agent decide when to fetch URL context
Replace prescriptive URL enrichment instructions (hardcoded gh/WebFetch
commands) with goal-oriented guidance. The agent now uses its own
judgment to decide whether fetching referenced URLs would produce a
meaningfully better title/description, rather than being told exactly
which tools to use.
* fix(quick-create): always generate rich description for agent execution
The description was previously optional ("omit if simple request"). Since
quick-create issues are executed by agents, richer context leads to
better execution — update the prompt to always produce a substantive
description with actionable context.
* fix(quick-create): remove Chinese text from prompt, use English only
Replace Chinese examples in priority mapping and assignee matching with
language-agnostic English equivalents, per project coding rules.
* fix(quick-create): remove language-related hints from prompt
Agent doesn't need to be told about language handling — remove
"(in any language)" and "or equivalent in any language" qualifiers.
Keep prompt purely in English with no language-related content.
Two coordinated fixes for a quick-create case where the agent ended up
creating duplicate issues. Repro: user pasted an image into the
quick-create prompt; the front-end uploaded it and embedded the URL as
markdown in the user input; the agent saw the URL, assumed it was an
attachment, and ran `multica issue create … --attachment "https://…"`.
The CLI POSTed the issue first, then failed to read the URL as a file
(`os.ReadFile("https://…")`) and exited 1. The agent treated exit 1 as
"create failed" and retried — but the first issue already existed, so
the workspace ended up with two of them.
CLI (`server/cmd/multica/cmd_issue.go`):
- `runIssueCreate` pre-validates `--attachment` BEFORE POSTing. URLs are
warned about and skipped (they are never local files); local-path
read errors fail before the issue is created so no half-baked issue
lands. Once the POST succeeds, post-create upload failures only
print a stderr warning and the issue metadata is still emitted —
never a non-zero exit, so callers cannot mistake "attachment upload
hiccup" for "create failed" and retry.
- `runIssueCommentAdd` already uploads attachments BEFORE the comment
is created, so its failure mode is fine; it just gets the same
URL-skip behaviour for consistency.
Quick-create prompt (`buildQuickCreatePrompt`):
- Tells the agent NOT to pass `--attachment` for prompt-embedded image
URLs (they are already part of the description as markdown).
- Hardens the "no retry" rule: even on a non-zero exit, do not retry
`issue create` — the issue may already exist.
* feat(quick-create): default assignee to picker agent when user didn't name one
The quick-create prompt previously told the agent to OMIT --assignee
when the user's input didn't name a person. That left almost every
quick-created issue unassigned, which doesn't match user intent — the
user opened quick-create with a specific agent picked, so that agent
is the obvious owner.
Both prompt surfaces (BuildPrompt for the dispatched message, plus
the workflow block in injected CLAUDE.md / AGENTS.md) now instruct
the agent: if the input doesn't name an assignee, pass
`--assignee "<your name>"`. The picker agent's name is interpolated
into the prompt at task-build time so the agent has a literal value
to use rather than guessing its own name. The "explicitly named
assignee → resolve via members" branch is unchanged.
* refactor(execenv): drop duplicated quick-create field rules from CLAUDE.md/AGENTS.md
The quick-create field rules (title / description / priority / assignee
fallback / project / status) lived in two places — the per-turn user
message built by BuildPrompt, and the workflow block injected into
CLAUDE.md / AGENTS.md by buildMetaSkillContent. Same content, two
sources, easy to update one and forget the other (the assignee-default
change in this PR had to touch both).
Quick-create is one-shot, so the per-turn user message is always
present and is the natural single source of truth. The injected
file's quick-create section now keeps only the hard guardrails:
"do exactly one issue create, no issue get / status / comment add,
exit on CLI error". Those guardrails stay in BOTH surfaces because
they're the safety net for providers that don't propagate the user
message into resumed-session context.
renderQuickCreateContext (issue_context.md) was already
guardrails-only — no change needed there.
* 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.
Follow-ups to #1765 review nits:
- Tighten the per-turn prompt and AGENTS.md workflow instructions so
that "exit with no output" only applies when the trigger is from
another agent AND no actual work was produced this turn. If the
agent did real work, the standard "post results as a comment" rule
still applies — a result reply is not a noise comment.
- Add TestAgentExplicitMentionStillTriggers as a positive control
documenting the boundary the structural fix preserves: suppressing
implicit parent-mention inheritance for agent authors does NOT
block deliberate handoffs. An agent that explicitly @mentions
another agent in its own content still enqueues a task for the
mentioned agent and does not self-trigger.
When an agent replied in a thread whose root mentioned another agent,
the reply inherited the parent mention and re-triggered the other agent.
This caused 'No reply needed' ping-pong loops between co-assigned agents.
Structural fix:
- In enqueueMentionedAgentTasks, suppress parent-mention inheritance
when authorType == 'agent'. Explicit @mentions in the agent's own
comment still work for deliberate handoffs.
Defense-in-depth (prompt):
- Strengthen per-turn prompt and AGENTS.md workflow instructions to
explicitly forbid posting 'No reply needed' noise comments.
Regression test:
- TestAgentReplyDoesNotInheritParentMentions covers both the fix
(agent reply does not re-trigger) and the positive control
(member reply still inherits mentions).
Also updates TestBuildPromptCommentTriggeredByAgent to match the
new prompt wording.
* feat(daemon): harden agent mention-loop instructions
Two agents that mention each other via `mention://agent/<id>` can fall into
an infinite reply loop — each says "I'm done" in prose but keeps
`@mentioning` the other, which re-enqueues their run. Adding hard caps on
agent-to-agent turns conflicts with Multica's design principle of giving
agents the same authorship freedom as humans, so this change hardens the
instructions that the harness injects instead.
- Replace the terse "mentions are actions" blurb with a full Mentions
protocol: `side-effecting` warning, explicit "when NOT to mention"
(replying to another agent, sign-offs, thanks) and "when a mention IS
appropriate" (human escalation, first-time delegation, user asked).
- Add a pre-workflow decision step for comment-triggered runs: decide
whether a reply is warranted at all, decide whether to include any
`@mention`, and clarify that the post-a-comment rule is mandatory *if*
you reply — silence is a valid exit for agent-to-agent threads.
- Thread the triggering comment's author kind + display name
(`TriggerAuthorType` / `TriggerAuthorName`) from the claim endpoint
through the daemon task type, per-turn prompt, and CLAUDE.md workflow.
When the author is another agent, both surfaces now name that agent
and warn against sign-off mentions.
- Soften the old closing line that told agents to `always` use the
mention format — the word generalized to member/agent mentions and
encouraged the very behavior that causes loops.
Refs GH#1576, MUL-1323.
* fix(daemon): remove MUST-respond conflict and sanitize trigger author name
Addresses two blocking points on PR #1581:
1. buildCommentPrompt told the agent "You MUST respond to THIS comment"
and unconditionally appended the reply command — directly conflicting
with the new agent-to-agent silence-as-valid-exit workflow. Models
were likely to keep following the older must-reply rule and fall back
into the loop this PR is trying to close.
Rewrite the header as "Focus on THIS comment — do not confuse it
with previous ones" (keeps the anti-stale-comment signal) and change
BuildCommentReplyInstructions to open with "If you decide to reply,
post it by running exactly this command" so the reply command is
available but conditional across both prompt surfaces.
2. Raw agent/user display names were being embedded directly into the
high-priority prompt and CLAUDE.md via TriggerAuthorName. Agent and
member names are only validated as non-empty at write time, so a
name containing newlines, backticks, or fake mention markup would
turn the field into a cross-agent prompt-injection surface.
Add execenv.SanitizePromptField — strip control runes, collapse
whitespace, drop markdown structural characters (backtick, asterisk,
brackets, pipe, angle brackets, hash, backslash), truncate to 64
runes — and apply it at both embed sites (per-turn prompt and
CLAUDE.md). Defense-in-depth at the consumption layer so this works
for already-stored names without a migration.
Tests: TestSanitizePromptField covers the policy; TestBuildPromptSanitizesAgentName
plants an attack payload in TriggerAuthorName and checks the rendered prompt
does not leak the newline-anchored injection or the fake mention markup.
TestBuildPromptCommentTriggered*{,ByMember} updated to lock in the
conditional reply-command framing.
* refactor(daemon): trim redundant CLAUDE.md preamble and drop name sanitizer
Per PR #1581 feedback:
1. Remove the `if ctx.TriggerAuthorType == "agent"` preamble block in
runtime_config.go. It duplicated what workflow steps 4 and 5 already
say ("Decide whether a reply is warranted", "Never @mention the
agent you are replying to as a thank-you or sign-off"), so the
signal lands the same without the extra ~7 lines of CLAUDE.md. The
per-turn prompt preamble in prompt.go stays — that surface has no
numbered workflow below it and would otherwise lose the
silence-as-exit signal.
2. Delete execenv.SanitizePromptField + its test. Workspace agents are
created by trusted team members, so the cross-agent name-injection
surface it defended isn't realistic in the current trust model.
3. Drop TriggerAuthorType/Name from execenv.TaskContextForEnv and stop
populating them in daemon.go — they're no longer read by the
execenv package. The same fields on daemon.Task stay because
prompt.go still needs them to label the triggering author in the
per-turn prompt.
Tests simplified to match the leaner shape: CLAUDE.md regression
guards now assert that the anti-loop phrases live in the numbered
workflow, and the sanitizer-specific tests are removed.
* fix(agent/comments): re-emit trigger comment id every turn + server-side parent_id guard
Resumed Claude sessions keep prior turns' tool calls in context, so a
comment-triggered task could reuse the PREVIOUS turn's --parent UUID
instead of the current trigger's. The reply landed in the wrong thread
(MUL-1125): backend stored exactly what the agent sent, but the agent
pulled a stale UUID from its own conversation memory.
Two layers of defense:
1. Extract BuildCommentReplyInstructions so daemon.buildCommentPrompt
and execenv.InjectRuntimeConfig emit the same "use this exact
--parent, do not reuse values from previous turns" block. The
per-turn prompt now carries the current TriggerCommentID, which it
previously relied on CLAUDE.md for (and CLAUDE.md isn't re-read
mid-session).
2. Handler-side guard in CreateComment: when an agent posts from inside
a comment-triggered task (X-Agent-ID + X-Task-ID, task has
TriggerCommentID), require parent_id == task.TriggerCommentID or
return 409. Assignment-triggered tasks are untouched.
* fix(agent/comments): scope parent_id guard to the task's own issue
Two issues from CI + GPT-Boy's review:
1. Guard was too broad: the CLI stamps X-Task-ID on every request, so an
agent legitimately commenting on a different issue while its current
task was comment-triggered would get 409'd with the wrong issue's
trigger comment id. Narrow the guard to fire only when the request's
issue matches the task's own issue — cross-issue agent activity
stays unblocked.
2. The integration test tried to insert a second queued task for the
same (agent, issue), which hits the idx_one_pending_task_per_issue_agent
unique index. Replace the assignment-triggered-task sub-case with a
cross-issue regression test (the scenario we now need to cover anyway):
post on issue B while X-Task-ID points at a comment-triggered task on
issue A, expect 201.
When a comment-triggered task resumes an existing session, the agent
may mistake the new comment for a previous one and skip it. Add [NEW
COMMENT] tag to the prompt and reinforce in AGENTS.md workflow that
the agent must respond to THIS specific comment, not prior ones.
When a task is triggered by a comment, the agent prompt now includes
the comment content directly. This prevents the agent from ignoring
the comment when stale output files exist in a reused workdir.
Closes#805
Implement the Master Agent chat feature allowing users to chat with agents
directly from a floating window, separate from the issue-based workflow.
Backend:
- New chat_session and chat_message tables (migration 033)
- Make issue_id nullable on agent_task_queue for chat tasks
- REST API: create/list/get/archive sessions, send/list messages
- EnqueueChatTask in TaskService with session_id persistence
- WS events: chat:message, chat:done
- Daemon: chat task type with separate prompt builder
- ClaimTaskByRuntime populates chat context (session, message, repos)
Frontend:
- ChatSession/ChatMessage types + API client methods
- core/chat: TanStack Query options, mutations with optimistic updates, WS updaters
- features/chat: Zustand store, ChatFab (floating button), ChatWindow with
real-time streaming via task:message events
- Mounted in dashboard layout (bottom-right corner)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: decouple task lifecycle from issue status, add daemon health server
- Remove automatic issue status changes from StartTask (in_progress),
CompleteTask (in_review), and FailTask (blocked) in task service.
Issue status is now fully managed by the agent via `multica issue status`.
- Update agent prompt and meta skill to instruct agents to manage issue
status themselves (in_progress → done/in_review/blocked).
- Add daemon health HTTP server on 127.0.0.1:19514 with /health endpoint
exposing pid, uptime, agents, and workspaces. Fail fast if port is taken
(another daemon already running).
- Update `multica status` to check both server and daemon health.
- Add Save button to repos section in workspace settings UI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(daemon): simplify prompt, fix runtime config path, improve task error logging
- Slim down BuildPrompt to a minimal hint; detailed workflow now lives in CLAUDE.md/AGENTS.md
- Write CLAUDE.md to workDir root instead of .claude/CLAUDE.md
- Fix git-exclude pattern (.claude → CLAUDE.md)
- Decouple task queue reconciliation from issue status changes (agents manage status via CLI)
- Add diagnostic logging when CompleteTask/FailTask fail due to unexpected task state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(task): use task_completed/task_failed inbox notification types
FailTask was sending "agent_blocked" which conflates agent crash with
issue-level blocked status. Align notification types with the new
decoupled model: task_completed and task_failed. Update frontend types
and labels accordingly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the frozen context snapshot pattern with a CLI-driven approach:
agents now use `multica` CLI commands to fetch issue details, comments,
and workspace context on demand, always getting the latest data.
- Remove buildContextSnapshot and snapshot generation from enqueue
- Claim endpoint now returns fresh agent name + skills from DB
- Daemon resolves provider from local runtimeIndex, not snapshot
- Prompt instructs agent to use `multica issue get` / `comment list`
- Meta skill (CLAUDE.md/AGENTS.md) documents all available CLI commands
- Skills still injected as filesystem files (static agent config)
- Simplify daemon types: remove TaskContext/IssueContext/RuntimeContext
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These fields were unused in practice. Removed from frontend types,
issue detail UI, backend handlers, daemon prompt/context, protocol
messages, SQL queries, and tests. DB columns retained with defaults.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace agent.skills TEXT field with structured skill/skill_file/agent_skill
tables. Skills are workspace-level entities with supporting files, reusable
across agents via many-to-many bindings.
Backend: migration 008, sqlc queries, CRUD handler, agent-skill junction,
structured skill loading in task context snapshot.
Daemon: meta skill injection via runtime-native config (.claude/CLAUDE.md
for Claude, AGENTS.md for Codex) so agents discover .agent_context/ skills
through their native mechanism. Lean prompt without inlined skill content.
Frontend: Skills management page, agent Skills tab picker, SDK methods,
TypeScript types, workspace store integration.
Also removes auto-creation of init issues when creating agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce the `execenv` package that creates isolated working directories
for each agent task. Supports git worktree mode (code tasks) and plain
directory mode (non-code tasks), with `.agent_context/issue_context.md`
injected into the workdir for Claude Code to discover.
Key changes:
- New `server/internal/daemon/execenv/` package (Prepare/Cleanup)
- `runTask()` now creates isolated env instead of using shared reposRoot
- Prompt updated to reference `.agent_context/` files
- Add `WorkspacesRoot` config (default ~/multica_workspaces)
- Add `KeepEnvAfterTask` config for debugging
- Default agent timeout increased from 20min to 2h
- `CompleteTask` now forwards branch name to server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The repository JSONB column on the issue table is unused. This removes
it end-to-end: migration to drop the column, sqlc queries, Go handler/
service/daemon/protocol structs, TypeScript types, and the
RepositoryEditor UI component.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a new agent is created, automatically create an initialization issue
assigned to it so the agent starts working immediately. The issue
description incorporates the workspace context field, giving the agent
background info to set up its environment (clone repos, etc.).
Also adds workspace context to the task context snapshot for all tasks,
and renders it in the daemon prompt so agents always have workspace
background when executing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract daemon logic from cmd/daemon/ into internal/daemon/ package and
create a new unified CLI entry point at cmd/multica/ using cobra. The CLI
supports `daemon` as a long-running subcommand plus ctrl subcommands for
agent/runtime management, config, status, and version.
Server, migrate, and seed binaries remain unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>