* feat(comments): thread-internal pagination via --tail + reply cursor (MUL-2421)
Long threads inside a single issue still forced agents to read every reply
once they used --thread, even after MUL-2387 fixed cross-thread noise. This
adds reply-level paging so a 200-reply thread can be navigated tail-first
without dragging the whole conversation into prompt context.
- New SQL query ListThreadCommentsForIssuePaged: same recursive root walk
as the legacy thread query, but caps reply count and supports an
(created_at, id) composite cursor. Root is unconditional — even tail=0
emits it so the reader keeps the "what is this thread about" context.
- Handler ListComments: parses `tail` (non-negative, ThreadTailSet flag
preserves the tail=0 intent), threads it through to the paged query,
and re-uses X-Multica-Next-Before / X-Multica-Next-Before-Id for the
reply cursor. Cursor's meaning is now context-dependent: thread cursor
under --recent, reply cursor under --thread + --tail.
- CLI: new --tail flag (only valid with --thread; mutually exclusive
with --recent), reply-cursor semantics for --before / --before-id when
paired with --thread + --tail, stderr label flips to "Next reply cursor"
so an operator copy-pasting the cursor knows which scope it scrolls.
- Tests cover the new contract: tail=N keeps newest N + root, tail=0 is
root-only, anchor on a nested reply still walks up, reply cursor
scrolls older replies page-by-page, since combined with tail filters
after the cut, and the negative-flag-combination matrix.
Out of scope: prompt template update to hint at `--thread <id> --tail 30`
on long threads — separate follow-up per the issue.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): only emit reply cursor when older reply exists (MUL-2421)
The thread-tail path emitted `X-Multica-Next-Before` whenever the page
filled to exactly the requested reply count, even when there was nothing
older to scroll to. So `--thread <root> --tail 3` on a thread with
exactly 3 replies sent a cursor that, when followed, returned just the
root — a wasted round-trip that surfaced as a phantom "older replies"
affordance in the agent prompt.
Switch to a `reply_limit + 1` probe: ask the SQL for one extra row, trim
the oldest overflow before responding, and only emit the cursor when an
older reply actually existed. The exact-boundary case (replyCount ==
tail with no overflow) now returns no cursor.
Also documents `--thread/--tail/--recent/--before` and the cursor
semantics in CLI_AND_DAEMON.md, which was the second must-fix in the
MUL-2421 review.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): suppress reply cursor when --since covers older replies (MUL-2421)
In the thread + tail + since path the server still emitted a reply cursor
whenever there was an older reply on disk, regardless of `since`. If the
oldest retained reply on the page was already `<= since`, every older
reply was guaranteed to be filtered out too, so the next page only ever
returned the root — wasting round-trips until the agent walked the whole
pre-`since` history. Mirror the recent + since suppression: when
`replies[0].CreatedAt <= since`, drop the cursor.
Test covers the exact case from Elon's review: tail=2 overflow, body
keeps a fresher reply, but the cursor target (oldest retained reply) is
already past `since` — header must be empty.
Co-authored-by: multica-agent <github@multica.ai>
* feat(prompt): default comment-trigger reads to --thread --tail 30 (MUL-2421)
Comment-triggered agents previously defaulted the trigger-thread read to
the unbounded `--thread <id> --output json`, which dumps the full thread
into the prompt — exactly the kind of context bloat MUL-2387 fixed at the
cross-thread layer but never bounded inside a single thread.
Use the new `--tail` flag landed earlier in this PR (server + CLI) as the
default for both the per-turn prompt and the runtime-config Workflow:
- `--thread <trigger-id> --tail 30 --output json` is the new default.
Root is always included so "what is this about" context survives.
- If 30 replies aren't enough, the prompt now spells out the reply
cursor: re-feed the stderr `Next reply cursor: --before <ts>
--before-id <reply-id>` pair back to walk older replies.
- `--recent 20` stays as the cross-thread background fallback, with an
explicit callout that the same `--before` / `--before-id` flags walk
*threads* (not replies) in that mode.
- Available Commands core line now surfaces `--tail N` and both stderr
cursor labels so non-workflow callers also discover the flag.
- `--since` callouts reflect the post-MUL-2421 combinable mode names
(`--thread --tail` / `--recent`).
Tests (`prompt_test.go`, `execenv_test.go`) pin the new defaults and add
a regression guard against the unbounded `--thread` recipe sneaking back
in.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
- Drop `workspace current`; `workspace get` (no args) already prints the
current default workspace, so the two were doing the same thing.
- Rename `workspace members` to `workspace member list` to free up the
`member` namespace for future `add` / `remove` subcommands and align
with the rest of the CLI's `<resource> <verb>` shape.
- Add `--full-id` to `workspace list`, matching `project list`,
`autopilot list`, and friends.
Docs and the daemon prompt are updated to match.
Co-authored-by: multica-agent <github@multica.ai>
`multica workspace switch <id|slug>` is the product-semantic entry point for
changing the default workspace on the current profile. It looks the target up
in the user's accessible workspace list (an access check by construction —
the server only returns workspaces the user is a member of), persists the
chosen UUID via the existing CLI config layer, and prints the resolved name.
`config set workspace_id` stays as the low-level escape hatch.
`multica workspace switch` resolves the workspace before saving, so an
unknown id or slug fails fast and leaves the previous default intact.
`multica workspace current` and a `*` marker in `multica workspace list`
expose which workspace commands without --workspace-id/MULTICA_WORKSPACE_ID
will target. `multica login` reuses the same marker when listing discovered
workspaces and points multi-workspace users at switch.
Docs gain a "Working with multiple workspaces" section spelling out the
resolution priority (--workspace-id flag > env > profile default) and
calling out config set workspace_id as low-level.
Addresses GitHub#2750.
Co-authored-by: multica-agent <github@multica.ai>
* 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>
Copilot's backend (server/pkg/agent/copilot.go) and the public docs
site (apps/docs/) already treat it as one of the 11 supported agents,
but the root README, CLI guide, and self-host docs still listed only
10. Bring those to parity. Also brings README.zh-CN.md up to current
English content (was missing Copilot, Kimi, and Kiro CLI).
* fix(cli): make `multica login --token` accept the PAT as a value
The flag was registered as a Bool, so `multica login --token <PAT>` parsed
`--token` as `true` and dropped the supplied value as an unused positional
argument, then unconditionally prompted "Enter your personal access token:".
This contradicted the user-facing docs (`cli.mdx`, `CLI_AND_DAEMON.md`,
the in-app `connect-remote-dialog`) which show `--token <mul_...>`.
Switch `--token` to a String flag. Both `--token mul_...` and
`--token=mul_...` now bind the value and skip the prompt. Passing
`--token=` with an empty value (or `multica login --token=""`) still
falls through to the interactive prompt for users who don't want the
token in shell history. Updates the few internal docs that showed the
no-value form.
Fixes#1994
Co-authored-by: multica-agent <github@multica.ai>
* fix(cli): preserve `multica login --token` (no value) prompt path and tighten regression test
Addresses review feedback on #2017:
1. Restore the legacy no-value form. After the prior commit, `multica
login --token` (no value) errored with `flag needs an argument:
--token`, which broke the CLI_INSTALL.md / CLI_AND_DAEMON.md flow for
headless users. Set `NoOptDefVal` on the `--token` flag to a sentinel
that runAuthLoginToken treats as "prompt me," so:
- `--token mul_xxx` and `--token=mul_xxx` consume the value (the
#1994 fix is preserved),
- `--token` alone falls through to the interactive prompt,
- `--token=""` (explicit empty) also prompts.
pflag with `NoOptDefVal` won't bind the next positional as the flag's
value, so runAuthLogin recovers `--token mul_xxx` (the form from
#1994) by promoting a single positional arg into the token. loginCmd
gains `Args: cobra.MaximumNArgs(1)` so multi-positional typos still
error fast.
2. Tighten regression coverage. Split into TestLoginTokenFlagWiring
(asserts the production loginCmd.Flags().Lookup("token") is a String
flag with the prompt-mode NoOptDefVal — would fail if anyone reverts
the flag to Bool) and TestLoginTokenFlagParsing (drives all five
documented invocation forms through the same flag wiring + the
runAuthLogin space-form recovery). The synthetic-only test that the
reviewer flagged is gone.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): reclaim disk on long-open issues + correct cancelled-status check
Two related fixes for GitHub #1890 (self-hosted disk space growth):
- The GC's done/cancelled branch compared `status.Status` against `"canceled"`
(single l), but the issue schema and the rest of the daemon use `"cancelled"`
(double l). Cancelled issues therefore never matched and only fell out via the
72h orphan TTL, which itself doesn't fire because cancelled issues are still
reachable. Aligning the spelling lets cancelled-issue task dirs be reclaimed
on the normal TTL path.
- Add a third GC mode, artifact-only cleanup, for the common case the report
flagged: an issue stays open for days while many tasks complete on it, so
per-task `node_modules`, `.next` and `.turbo` directories accumulate without
ever becoming GC-eligible. The new branch fires when `.gc_meta.completed_at`
is older than `MULTICA_GC_ARTIFACT_TTL` (default 12h), the env root is not
currently in use by an active task, and the issue is still alive. It removes
only directories whose basename matches `MULTICA_GC_ARTIFACT_PATTERNS`
(default narrow: `node_modules,.next,.turbo`); source, `.git`, `output/`,
`logs/` and the meta file are preserved so subsequent tasks can still resume
the workdir. Patterns containing path separators are dropped, `.git` subtrees
are never descended into, symlinked matches are not followed, and every
removal target is verified to live inside the task dir.
Bookkeeping: `Daemon` now tracks active env roots with a refcounted set so the
GC loop never reclaims a directory that is mid-execution; `runTask` claims the
predicted root early plus the prior workdir on reuse paths. The cycle log is
extended with bytes reclaimed and per-pattern counts so self-hosted operators
can see what was freed.
Docs: extend the daemon configuration table in CLI_AND_DAEMON.md with the new
GC env vars and add a Workspace garbage collection section explaining the
three modes and the artifact-pattern contract.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon): protect active env root from full GC removal too
Address GPT-Boy's PR #1931 review: the active-root guard only fired in the
artifact-cleanup branch, leaving a real race on the full-removal paths. A
follow-up comment on a long-done issue dispatches a task that reuses the prior
workdir, but `CreateComment` does not bump issue.updated_at — so the issue
still satisfies the done+stale GCTTL window and `gcActionClean` would
`RemoveAll` the directory mid-execution. The orphan-404 path is similarly
exposed when a token's workspace access is in flux.
Move the `isActiveEnvRoot` check to the top of `shouldCleanTaskDir` so all
three delete actions (clean, orphan, artifact) skip an in-use env root in one
place, and drop the now-redundant guard from the artifact branch.
Add tests covering the three at-risk paths: active root + done/stale issue,
active root + 404 issue past orphan TTL, active root + no-meta orphan past
TTL.
Also align two stale comments noted in the same review: cleanTaskArtifacts now
documents that symlinks are skipped entirely (the previous note implied the
link itself was removed), and GCOrphanTTL no longer claims that 404s are
cleaned immediately — the implementation gates them on the same TTL.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
* feat(cli): add `issue subscriber` commands
Wrap the existing /subscribers, /subscribe, and /unsubscribe endpoints as
`multica issue subscriber list|add|remove`, mirroring the comment subcommand
shape. `--user <name>` reuses resolveAssignee to resolve a member or agent;
without the flag, the action targets the caller.
* fix(issues): default subscribe target to resolveActor, not X-User-ID
When no user_id is posted, subscribe/unsubscribe hardcoded the target as
("member", X-User-ID). A CLI caller running as an agent (X-Agent-ID set)
then subscribed the underlying member rather than the agent itself,
which contradicts the "defaults to the caller" contract.
Derive the default via resolveActor so the endpoint mirrors caller
identity consistently — agent caller → agent row, member caller →
member row. Adds a regression test covering the agent caller path.
These trigger kinds exist in the DB schema but nothing on the server
fires them:
- autopilot_scheduler.ClaimDueScheduleTriggers filters kind='schedule'
(pkg/db/queries/autopilot.sql:150)
- DispatchAutopilot is reached only from the scheduler (source:schedule)
or POST /api/autopilots/{id}/trigger (source:manual); no inbound
webhook or api endpoint exists
- The UI only surfaces schedule creation
Exposing them in the CLI lets users create triggers that sit in the DB
doing nothing. Drop --kind from trigger-add, require --cron, always
send kind=schedule. Re-add the flag when the server grows a dispatch
path for the other kinds.
* feat(cli): add autopilot commands
Expose the existing autopilot REST API through the multica CLI so
users and agents can list, get, create, update, delete, trigger, and
inspect autopilots, plus manage their triggers (schedule/webhook/api).
Also surface the read + core write commands in the agent meta skill
prompt so agents discover them without needing --help.
- new cmd_autopilot.go (+ test) wiring /api/autopilots endpoints
- add APIClient.PatchJSON (autopilot update uses PATCH)
- expose autopilot in CORE COMMANDS group
- extend runtime_config.go meta skill with autopilot entries
- document autopilot command group in CLI_AND_DAEMON.md
* fix(autopilot): address code review — restrict run_only, validate workspace on update
Code review caught two issues with the initial CLI PR:
1. run_only mode is broken end-to-end. The daemon-side
resolveTaskWorkspaceID() in internal/handler/daemon.go only resolves
workspace from issue/chat, so run_only tasks (which have neither)
return 404 from /start. BuildPrompt() would also emit an empty issue
ID. The service-level resolver in internal/service/task.go already
handles AutopilotRunID, but the daemon endpoint uses the handler
copy. Fixing that path is out of scope for the CLI PR; drop
run_only from the CLI and docs so we don't recommend a mode that
cannot complete. Server continues to accept it for the existing UI.
2. UpdateAutopilot did not verify that a new assignee_id belongs to
the workspace, unlike CreateAutopilot. This let a PATCH swap in an
agent from a different workspace. Mirror the same
GetAgentInWorkspace check.
The project CRUD commands (list, get, create, update, delete, status)
and the `--project` flag on issue commands have been implemented in
the CLI but were not yet documented. Add them to both the docs site
reference and the repo-level CLI_AND_DAEMON.md so the feature is
discoverable.
Closes MUL-867
Co-authored-by: Eve <eve@multica.ai>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Pi and Gemini runtimes to supported-agent references
CLI_AND_DAEMON.md, SELF_HOSTING.md, and SELF_HOSTING_ADVANCED.md listed
claude/codex/opencode/openclaw/hermes as supported runtimes in their agent
tables and env-var overrides but omitted the pi and gemini entries that
the daemon already registers (server/internal/daemon/config.go).
* docs(readme): list all supported runtimes (add Hermes, Gemini, Pi)
* docs: add Cursor runtime, fix Pi URL, clarify daemon ASCII diagram
- Add Cursor Agent (cursor-agent CLI, MULTICA_CURSOR_PATH/MODEL) to the
supported-runtime tables, env-var lists, and prose across README,
CLI_AND_DAEMON, CLI_INSTALL, SELF_HOSTING, and SELF_HOSTING_ADVANCED.
- Fix Pi's canonical URL from github.com/paperclipai/paperclip to
https://pi.dev/.
- Rework the Agent Daemon box in both READMEs so provider names live in
an annotation outside the box instead of being wrapped mid-word
(`OpenClaw/Code`), which read as a phantom "Code" runtime.
Decouple install.sh from environment configuration — install.sh now only
installs the CLI binary (and optionally Docker via --with-server), while
all environment configuration moves to `multica setup` subcommands.
Key changes:
- install.sh: remove config writes, rename --local to --with-server
- multica setup: add cloud/self-host subcommands with --server-url,
--app-url, --port, --frontend-port flags and --profile support
- Add config overwrite protection with interactive prompt
- Remove redundant commands: `config local`, `auth login` alias
- Replace silent multica.ai fallbacks with explicit errors
- Onboarding wizard: dynamically show correct setup command for
Cloud vs Self-host environments
- Update all docs, landing page, and install scripts for consistency
* fix(docker): remove COPY for non-existent tsconfig/node_modules
The @multica/tsconfig package has zero dependencies, so pnpm install
never creates a node_modules directory for it. The COPY --from=deps
instruction fails with "not found" during docker compose build.
Closes#658
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(docker): add dotenv as explicit dependency for web app
next.config.ts imports dotenv to load .env for REMOTE_API_URL, but
dotenv was never declared as a dependency. It worked locally as a
hoisted transitive dep but fails in Docker's stricter module resolution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: fix daemon setup instructions for local Docker deployments
The daemon setup section in SELF_HOSTING.md had production URLs as the
active example and local Docker URLs commented out. Since this is a
self-hosting guide, local Docker should be the primary example.
Key changes:
- Make local Docker URLs the default in daemon setup examples
- Add explicit warning that CLI defaults to hosted service
- Add 'multica config set' instructions for persistent setup
- Add link from Quick Start to daemon setup section
- Clarify that daemon runs on host machine, not inside Docker
- Update CLI_AND_DAEMON.md self-hosted section similarly
Closes#660
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a structured installation guide (CLI_INSTALL.md) designed for AI agents
to fetch and execute step-by-step: install CLI, authenticate, and start the
daemon. Update README and README.zh-CN CLI sections with an agent-friendly
paste option alongside the existing manual instructions.
Also fix brew formula name in CLI_AND_DAEMON.md (multica-cli → multica) to
match .goreleaser.yml.
- Add CLI_AND_DAEMON.md with full command reference, daemon configuration,
profiles, and self-hosted setup instructions
- Rename LOCAL_DEVELOPMENT.md → CONTRIBUTING.md for conventional naming
- Update README.md references and link to the new CLI guide