mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 22:09:44 +02:00
v0.3.6
445 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a55c03a0b3 |
fix(agent): inject Workspace Context into agent brief (MUL-2542) (#3078)
* fix(agent): inject Workspace Context into agent brief (MUL-2542) The per-workspace `workspace.context` field (Settings → General) was stored in the DB but never reached the agent prompt. Plumb it from the workspace row through the claim response, the daemon's Task struct and TaskContextForEnv, and render it as `## Workspace Context` in the meta brief above `## Available Commands`. Heading is skipped when the field is empty so workspaces that haven't set a context don't see a bare header. Applies to every task kind — issue, comment, chat, autopilot, quick-create — so the shared system prompt is consistent regardless of trigger source. Co-authored-by: multica-agent <github@multica.ai> * chore(server): gofmt files touched by workspace-context injection Run gofmt on the files that buildWorkspaceContext injection touched. Cleans up composite-literal alignment in execenv task context and struct-tag alignment in Task / AgentTaskResponse / RegisterRequest. No behavior change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <agent-j@multica.ai> |
||
|
|
0bb51ccd0e |
feat(issues): mention parent assignee in child-done system comment (MUL-2538) (#3065)
* feat(issues): mention parent assignee in child-done system comment (MUL-2538)
Per Bohan's product call on MUL-2538 ("方案 C"), the platform's child-done
system comment now @mentions the parent assignee — member, squad, or
agent — and the platform fires the matching side effect explicitly:
- agent → mention task via TaskService.EnqueueTaskForMention
- squad → leader task via TaskService.EnqueueTaskForSquadLeader
- member → 'mentioned' inbox row + EventInboxNew broadcast
The generic comment listener still short-circuits on author_type='system'
(see notification_listeners.go) so smuggled mention links in the child
title can never light up unrelated members; the parent assignee mention
is the only side effect, and it is fired from the handler with explicit
guards rather than the listener path.
Guards retained / added:
- Comment-fire gates from prior PR unchanged (status transition, parent
state, no parent).
- Loop guard: skip trigger when child and parent share the same assignee
(same agent / same squad / same member). The comment + mention still
render so the timeline tells the full story; the second task does not
fire.
- Idempotency: HasPendingTaskForIssueAndAgent dedupes rapid-fire enqueues
for the same parent (back-to-back child completions).
- Readiness: archived agents / missing runtimes are silently skipped.
Tests:
- TestChildDoneMentionsParentAssignee_{Agent,Member,Squad} verify the
mention link + the matching trigger / inbox row.
- TestChildDoneSelfTriggerGuard_SameAgent asserts that an agent assigned
to both the child and the parent gets the comment + mention but no
second task — the documented loop break.
- TestChildDoneNotifiesParent updated: when the parent has no assignee
(its existing fixture), no routing mention should appear; the assigned
branches are exercised by the new cases above.
Co-authored-by: multica-agent <github@multica.ai>
* feat(issues): skip child-done parent notification for human assignees (MUL-2538)
Humans read their own timeline manually — an automated system comment
is pure noise for member-assigned parents, and there is no agent task
to trigger. Skipping the notification entirely also removes the mention
question (no comment → no mention → no inbox row).
The agent / squad / unassigned branches stay unchanged.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): close cross-squad shared-leader loop in child-done dispatch (MUL-2538)
Elon's review of PR #3065 flagged that triggerChildDoneAgent and
triggerChildDoneSquad only compared the child's direct assignee, so a
child-done event could still wake the same agent when:
- parent assigned to agent A, child assigned to a squad whose leader is A;
- parent and child assigned to two different squads sharing the same
leader agent.
Replace the per-side checks with a single effectiveChildAgentOwner helper
that reduces the child to "the agent that would actually act on it" (the
agent assignee, or the squad's leader) and lets both trigger paths compare
apples to apples. Add coverage for both newly-blocked cases, and tighten
the documented side-effect semantics (squad triggers leader only — no
member fan-out; notification_preference is not consulted, downstream
agent_task / inbox pipeline still respects mutes).
Also fix the member-skip test fixture to write user_id, matching the
production invariant that issue.assignee_id for assignee_type='member'
references user_id (validateAssigneePair, server/internal/handler/issue.go).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
c967ae0e0e |
feat(issues): platform-owned parent notify on child done (MUL-2538) (#3055)
* feat(issues): platform-owned parent notify on child done (MUL-2538)
When a child issue transitions from a non-done status into `done` and has
an open parent, the server now posts a top-level platform-generated
comment on the parent itself. Replaces the agent-prompt rule shipped in
PR #2918, which produced self-mention loops, planner ping-pong, and
accidental `MUL-` prefix hardcoding because the agent did not always know
the workspace prefix.
- Migration 107 widens `comment.author_type` to allow `system`; the
zero UUID is used as the sentinel `author_id` (the column stays NOT
NULL, callers branch on `author_type === 'system'`).
- `Handler.notifyParentOfChildDone` fires from both `UpdateIssue` and
`BatchUpdateIssues`. Guards: prev status != done, new status == done,
parent set, parent not in `done`/`cancelled`. Bypasses the
CreateComment HTTP path so the assignee on_comment trigger and the
mention-trigger paths do not fire — the comment content carries only
the safe issue mention for the child, no `mention://agent/...` /
`mention://member/...` / `mention://squad/...` links.
- `runtime_config.go` downgrades the Parent/Sub-issue Protocol rule 1
to an explicit "do NOT post one yourself" guardrail; rule 2 (sub-issue
creation `--status todo` vs `backlog`) is unchanged.
- New handler test exercises the happy path, idempotency, reopen+done,
parent done/cancelled guards, and the no-parent case. Runtime-config
tests reassert the new wording and the banned strings from the prior
revision.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): isolate system comments + wire GH merge path (MUL-2538)
Addresses the two must-fix items from the PR #3055 second review:
1. The platform-generated `comment:created` event (author_type='system')
was running through the generic comment listeners, which (a) tried to
subscribe the zero-UUID author and (b) parsed @mentions from the body
for inbox notifications. Both subscriber_listeners and
notification_listeners now early-return on author_type='system' so the
event becomes a pure WS broadcast for the timeline — no inbox rows,
no transcluded-mention attack surface.
2. advanceIssueToDone (the GitHub merge auto-done path) only published
issue:updated and skipped notifyParentOfChildDone, so a child closed
via merged PR — the dominant completion path — left the parent
silent. The helper is now invoked on the same prev/updated pair, with
the existing guards (transition + parent state) protecting double-fire.
Tests:
- New cmd/server/notification_listeners_test:
TestNotification_SystemCommentSkipsInboxAndMentions (parent subscribers
and smuggled @mention targets stay quiet),
TestSubscriberSystemCommentDoesNotSubscribe (zero-UUID never reaches
AddIssueSubscriber).
- New internal/handler/github_test:
TestWebhook_MergedPR_ChildWithParent_NotifiesParent fires a real
pull_request closed-merged webhook against a child and asserts the
parent receives exactly one safe system comment with the workspace's
real identifier (no `mention://agent|member|squad` links).
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtime): drop parent-notification guidance from agent brief (MUL-2538)
Per Bohan's product call on PR #3055: the platform now owns the
child-done parent notification, so the runtime brief should not mention
the parent-comment path at all — not as an instruction, not as a "do
not do it" guardrail. The previous revision kept rule 1 of the Parent /
Sub-issue Protocol as a "Do NOT post your own parent-notification
comment." sentence; that still puts the concept in front of the agent
every run, which is exactly what we are trying to avoid.
What changes:
- Delete the "Parent / Sub-issue Protocol" preamble and rule 1 from
buildMetaSkillContent. The remaining content — the `--status todo`
vs `--status backlog` rule for creating sub-issues — now lives in a
dedicated `## Sub-issue Creation` section, since the parent/child
framing it previously sat under is gone.
- The system comment on the parent stays exactly as in
|
||
|
|
1c91c2a3b2 |
security(db): scope DELETE/UpdateIssueStatus by workspace_id (defense-in-depth) (#3027)
* fix(security): scope DELETE/UpdateIssueStatus by workspace_id Add workspace_id to the WHERE clause of DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, and UpdateIssueStatus as SQL-layer defense-in-depth. Handler loaders (loadIssueForUser / loadSkillForUser / etc.) already enforce workspace membership today, so this is not patching a known live vuln. But the tenant invariant is currently a handler-layer guarantee — a future loader bypass or a new caller skipping the loader would be silently catastrophic. Making workspace_id part of the SQL identity collapses the trust surface to the schema itself: forging a sibling-workspace UUID becomes ErrNoRows instead of a cross-tenant write. Reference: incident #1661 (util.ParseUUID silent zero UUID returning 204 on a DELETE that matched zero rows) — same class of failure, prevented at a different layer. Scope: - 5 DELETE queries: issue, comment, project, skill, chat_session - 1 simple UPDATE: UpdateIssueStatus (2 narg, no SET ordering risk) - All callers updated (handlers, service, runtime sweeper fallback) Multi-narg UPDATE queries (UpdateIssue, UpdateProject, UpdateSkill, UpdateComment, UpdateChatSession*) are deferred to a follow-up to keep this change reviewable: each needs its narg pinning shifted and per-caller verification. sqlc was regenerated by hand (no local sqlc toolchain); CI's backend job is the authoritative compile check. * test(security): add workspace_scope_guard regression test Locks in the SQL-layer tenant guard added in this PR. For each of the 6 scoped queries (DeleteIssue, DeleteComment, DeleteProject, DeleteSkill, DeleteChatSession, UpdateIssueStatus), creates the resource in workspace A, invokes the query with a foreign workspace UUID, and asserts the row is untouched (0 rows affected with no error for :exec; pgx.ErrNoRows for :one). A future refactor that drops the workspace_id arg from any of these queries will now fail loudly instead of silently regressing. Includes a sanity sub-test that the in-workspace path still mutates, so a buggy guard that returns no-op for every call would not pass. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com> --------- Co-authored-by: Tom Qiao <tomqiaozc@users.noreply.github.com> Co-authored-by: Claude Opus 4 <noreply@anthropic.com> |
||
|
|
eefc6cebaa |
feat(server): add workspace-level always_redact_env setting (MUL-2495) (#2367)
* feat(server): add workspace-level always_redact_env setting When a workspace opts into always_redact_env (via workspace settings JSON), all agent GET/LIST responses will have custom_env values masked and mcp_config nulled regardless of the caller's role. This provides a stricter security posture for single-tenant self-hosts or environments where screen-sharing or pairing makes plaintext secrets a risk. The setting is opt-in and defaults to false (preserving existing behavior). Owners can still write secrets via the update path; they just cannot read them back through the API when this setting is enabled. Closes #2352 * fix(server): fail-closed on GetWorkspace, add HTTP tests, distinguish redaction reason Address review feedback on #2367: 1. GetWorkspace failure now returns 500 instead of silently defaulting to alwaysRedact=false (fail-open → fail-closed). 2. Add HTTP-level regression tests for always_redact_env: - GetAgent with flag on → owner sees redacted env - ListAgents with flag on → owner sees redacted env - GetAgent with default settings → owner sees plaintext env 3. Add custom_env_redacted_reason field ('policy' | 'role') to distinguish workspace-policy redaction from role-based redaction. UI now only sets readOnly when reason is 'role', allowing owners to edit env even when always_redact_env is enabled. 4. Write-back footgun tracked in #2999. Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> * fix(test): clear workspace settings before DefaultNoRedactForOwner Guard against test-order leakage: if a preceding test enabled always_redact_env on the shared workspace and its cleanup didn't run (e.g. due to -shuffle or parallel execution), this test would incorrectly see policy-level redaction. Explicitly reset settings to NULL before assertions. Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> * fix(ui): make EnvTab read-only when env is redacted by any policy Previously the readOnly guard only checked for 'role' redaction, leaving the tab editable under 'policy' redaction. This meant a user could save the form with '****' placeholder values, permanently overwriting the actual secrets. Use the boolean custom_env_redacted flag instead so the tab is locked regardless of the redaction reason. Fixes the regression flagged in the third-pass review. Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> * fix: reset workspace settings to empty JSON instead of NULL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: gofmt AgentResponse struct alignment Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> --------- Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
46a29b1ebb |
fix(squads): warn leader against double-triggering an agent (#3053)
Squad coordinators were both @mentioning an agent in the parent issue and creating a todo child issue assigned to the same agent, causing the agent to be triggered twice in parallel (mention dispatch + assignment dispatch). The server has no cross-issue dedupe for this case — and adding one would make @mention semantics context-dependent and unpredictable. Fix is at the prompt level: tell the squad leader that a `todo` child issue with an agent assignee already fires that agent, so they must pick exactly one delegation path for any given piece of work — comment-based @mention or todo child-issue assignment, never both. Adds a focused regression test that locks in the new rule via narrow substring checks (so harmless rewording stays free). Fixes #3033 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7984606eed |
feat(landing): add Contact Sales page and inquiry endpoint (MUL-2493) (#2988)
* feat(landing): add Contact Sales page and inquiry endpoint (MUL-2493) Adds a public `/contact-sales` marketing page with a needs-discovery form modelled on the design reference attached to MUL-2493 — first/last name, business email (with free-provider rejection), company name + size, country/region, intended use case, and a free-text goals field, plus the two consent checkboxes from the reference. Submissions hit a new public `POST /api/contact-sales` endpoint with per-IP rate limiting (Redis-backed via the existing RateLimit middleware, configurable through `RATE_LIMIT_CONTACT_SALES`) and a per-email hourly cap so a single business address can't be used as a flood channel after one valid pass. The inquiry is stored in a new `contact_sales_inquiry` table; analytics fires a `contact_sales_submitted` PostHog event with only the closed-enum dimensions (size, country, use case) — the free-text goals stay in the DB and are never broadcast. The page is linked from the landing header (md+) and the footer's Company column, in both English and Simplified Chinese. The reserved-slug list is updated so a workspace named `contact-sales` can't shadow the route. Co-authored-by: multica-agent <github@multica.ai> * fix(landing): canonicalize business email and tighten contact-sales form (MUL-2493) - Parse the submitted email with net/mail and run the free-email block-list against the canonical addr.Address, so a display-name form like `Ada <ada@gmail.com>` can no longer slip past the gate (the raw string had domain `gmail.com>`, which wasn't blocked). Adds regression tests covering the display-name bypass and the canonicalization helper. - Drop noValidate from the contact-sales form so the browser's native required / email / select checks fire before submit; the JS-side free-email warning still runs as a UX guard. - Update success copy ("respond within three business days") in EN and ZH plus the page metadata. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b9602adabe |
fix(handler): validate skill id UUID at request boundary (#3025)
loadSkillForUser was passing chi.URLParam(r, "id") directly into
parseUUID, the panic-on-invalid helper reserved for trusted UUID
round-trips. A malformed `/api/skills/{notuuid}` request panicked
in util.MustParseUUID; chi's middleware.Recoverer turned it into a
500 instead of a 400.
This violates the documented convention (CLAUDE.md → "Backend Handler
UUID Parsing Convention"): pure-UUID request inputs must use
parseUUIDOrBadRequest, which writes a 400 and short-circuits.
Switch loadSkillForUser to parseUUIDOrBadRequest. Behaviour for valid
UUIDs is unchanged; malformed input now returns 400 with a clear
"invalid skill id" message.
Test:
- TestGetSkill_MalformedUUIDReturns400 asserts GET /api/skills/not-a-uuid
returns 400.
Co-authored-by: Tom Qiao <tomqiaozc@users.noreply.github.com>
|
||
|
|
fbd965e5bf |
feat(onboarding): v3 — thin server, frontend-orchestrated welcome (#3008)
* feat(onboarding): Multica Helper as general workspace assistant + blocking modal
Reshape Multica Helper from an onboarding-only guide into the workspace's
general-purpose AI assistant. The agent's permanent identity (injected as
`## Agent Identity` into every task's CLAUDE.md / AGENTS.md / GEMINI.md
via execenv.InjectRuntimeConfig) is rewritten to three sections that don't
overlap with what the brief already provides:
- Who I am (built-in workspace assistant, not onboarding-only)
- What Multica is + docs/source/issues URLs as knowledge sources
- What I can do (CLI = manifest, `multica --help` is the source of truth)
- Tone (concise, like a colleague, match user's language)
Bootstrap moves out of the in-flow Step 4. Runtime step now exits the
onboarding shell with no bootstrap call; a blocking OnboardingHelperModal
mounts inside the workspace layout (web + desktop) and gates purely on
`me.onboarded_at == null`. The user picks one of three starter prompts
(intro / assign / second_agent) and the modal calls
BootstrapOnboardingRuntime with a new optional `starter_prompt` field that
becomes the seeded onboarding issue's description.
Side effects required to make `onboarded_at == null` an honest signal:
- CreateWorkspace no longer marks onboarded (was atomic with CreateMember).
The "member exists ⟹ onboarded_at != null" invariant is intentionally
broken; guards (useDashboardGuard / desktop App.tsx) already tolerate
this — comments updated to reflect the new contract.
- AcceptInvitation still marks (invitee skips the modal in someone
else's workspace). Code comment added warning future removers.
- resolvePostAuthDestination flips to workspace-presence-first: a user
with a workspace lands in it regardless of `onboarded_at`, so the
modal can pick up an interrupted setup on relogin.
Other backend changes:
- `onboardingAssistantDescription` rewritten ("Built-in workspace assistant…")
- `onboardingAssistantInstructions` rewritten to the 3-section identity
- `bootstrapOnboardingRuntimeRequest.StarterPrompt` (optional, 2 KiB rune
cap, empty-falls-back-to onboardingIssueDescription)
Frontend changes:
- Delete `packages/views/onboarding/steps/step-teammate.tsx` (no longer a
persisted step)
- `ONBOARDING_STEP_ORDER` and `OnboardingStep` type drop `"teammate"`
- `handleRuntimeNext` exits via `onComplete(workspace, undefined)` — no
bootstrap, `onboarded_at` stays NULL so the modal fires
- Runtime step next-button copy → "Start exploring" / "开始探索"
- New `packages/views/workspace/onboarding-helper-modal.tsx`:
Base UI Dialog, dismissible=false, three localized cards, mutation
invalidates agents + issues queries then navigates to the seeded issue
- Mounted in both `apps/web/app/[workspaceSlug]/layout.tsx` and
`apps/desktop/src/renderer/src/components/workspace-route-layout.tsx`
Tests:
- Backend: TestBootstrapOnboardingRuntime_{With,No}StarterPrompt and
TestCreateWorkspace_DoesNotMarkOnboarded
- Frontend: onboarding-helper-modal.test.tsx covers all four gating
conditions, three-card behavior, mutation pending state, and the
"no close button" invariant
Compatibility:
- Already-onboarded users: zero impact (modal can't fire)
- Invitees: AcceptInvitation still marks → modal can't fire
- Skip-runtime path: BootstrapOnboardingNoRuntime still marks → modal can't fire
- Old desktop / web clients: legacy teammate-step path keeps working
(bootstrap accepts missing starter_prompt) — the new modal only fires
on the new frontend bundle
- Avatar SVG kept (asterisk variant) — no migration of existing Helper
agents, only newly-created Helpers pick up the new instructions/description
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(desktop): suppress OnboardingHelperModal while a WindowOverlay is open
On desktop, App.tsx auto-creates a tab pointing at the user's first
workspace as soon as workspaces.length flips from 0 → 1 (during onboarding
Step 2). The new tab mounts WorkspaceRouteLayout under the overlay,
which mounts OnboardingHelperModal. The modal's Portal renders to
document.body — appearing AFTER the WindowOverlay in DOM order, so its
z-50 wins and the modal floats in front of the still-active onboarding
Step 3 (runtime).
Suppress the modal whenever any WindowOverlay is active. When the overlay
closes (onComplete fires after the user finishes onboarding), the modal
re-evaluates `me.onboarded_at == null` and pops on its own.
Web is unaffected (onboarding flow lives at /onboarding, not under
/[workspaceSlug]/, so WorkspaceRouteLayout never mounts during the
onboarding flow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(onboarding): add v2 refactor plan
Captures the design + 8-step implementation order for collapsing the
onboarding state machine: single mark-onboarded entry point, persisted
Step 3 user choice, dumb Modal, single install-runtime seed call site.
Includes old-user compatibility analysis (4 existing gates) and per-PR
risk/rollback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(db): persist Step 3 runtime choice on user record (MUL-onboarding-v2)
Adds onboarding_runtime_id UUID NULL + onboarding_runtime_skipped BOOLEAN
columns to "user" and the CHECK constraint enforcing the 3-state machine
(unset / picked-runtime / explicit-skip; the fourth combination is
forbidden). ON DELETE SET NULL on the FK so a deleted runtime degrades
to "unset" rather than dangling.
PatchUserOnboarding gains the two narg fields plus CASE expressions that
collapse the runtime/skipped pair atomically — a follow-up PATCH that
flips one side now clears the other in the same statement, instead of
preserving it via per-field COALESCE and tripping the CHECK constraint.
Backwards compatible for existing users: both new fields default to
(NULL, false), which is the "unset" leaf of the state machine, and four
upstream gates on me.onboarded_at != null already short-circuit the
new fields' readers for everyone who's already onboarded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(server): collapse onboarding side effects to service layer
Introduces OnboardingService.MarkComplete and
WorkspaceContentService.{Ensure,Seed}InstallRuntimeIssue as the single
authorities for the two onboarding side effects that used to be
duplicated across four handlers:
- MarkUserOnboarded + claim starter_content_state +
optional install-runtime fallback seed: was inline in
BootstrapOnboardingRuntime, BootstrapOnboardingNoRuntime,
AcceptInvitation, and CompleteOnboarding.
- install-runtime issue seeding: was inline in CreateWorkspace and
AcceptInvitation as a "no runtime yet" fallback.
After this refactor:
- MarkUserOnboarded is called from exactly one place (the service).
- install-runtime issue is seeded from exactly one place (the service).
- CreateWorkspace deliberately does not seed — the new
/ensure-onboarding-content endpoint (also added here) lets the
workspace-entry init component request the seed on first mount, so
workspaces created but never opened don't accumulate stale issues.
- The PatchOnboarding handler now accepts the new runtime_id /
runtime_skipped fields and rejects (uuid, skipped=true) up front.
- UserResponse exposes the two new persisted fields so the frontend
can read them off `me` without an extra round-trip.
Handler-side tests added: TestPatchOnboarding_RuntimeChoiceSwitch (the
explicit cross-request switch path that the original COALESCE design
would have 500'd on) + TestPatchOnboarding_PreserveUntouched.
Old handler-local file no_runtime_issue.go is deleted; its content
moved to service/workspace_content.go with the helpers exported.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): API + types for persisted onboarding runtime choice
User type / Zod schema gain onboarding_runtime_id (string | null) and
onboarding_runtime_skipped (boolean); EMPTY_USER + test fixture updated
to match. api.patchOnboarding accepts the new optional fields and the
new api.ensureOnboardingContent endpoint is wired so the workspace
shell can request the fallback seed.
Two new store helpers — recordOnboardingRuntimeChoice(runtimeId) and
recordOnboardingRuntimeSkipped() — replace the prior pattern of
Step 3 calling bootstrap directly. They PATCH the user's choice, sync
the auth store, and return. Mutually exclusive on the server side via
the CHECK constraint; the client just ships one intent at a time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(workspace): WorkspaceOnboardingInit single decision point + dumb Modal
Replaces OnboardingHelperModal's self-gating render path with a 4-branch
dispatcher that runs once on workspace-shell mount:
branch 0 me.onboarded_at != null → ensure install-runtime issue
fallback, render nothing
branch 1 me.onboarding_runtime_skipped → SkipBootstrapping component:
loading veil → bootstrap →
navigate. On failure shows
a Retry UI instead of
silently freezing the veil
branch 2 me.onboarding_runtime_id → render Modal with the
runtime id from `me` (no
internal list query)
branch 3 (none of the above) → useEffect navigate back to
/onboarding so the user
walks Step 3 again
The Modal itself is now a dumb component — receives `workspace` and
`runtimeId` as props, no internal gates, no runtimeListOptions query.
Tests rewritten to cover the props-driven render + pick-card paths;
the prior gating tests move into the new
workspace-onboarding-init.test.tsx alongside the M2 retry-on-failure
behaviour.
Mounted in both apps/web/app/[workspaceSlug]/layout.tsx and the desktop
workspace-route-layout. Desktop keeps its `!overlayActive` suppression
guard so the init doesn't portal-jump in front of an active
WindowOverlay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): Step 3 records user choice instead of calling bootstrap
handleRuntimeNext now PATCHes the user's pick (recordOnboardingRuntime
{Choice,Skipped}) and navigates straight into the workspace shell. The
workspace-entry WorkspaceOnboardingInit reads the persisted choice off
`me` and runs the appropriate branch — Step 3 is pure intent capture
with zero side effects on its own.
PATCH must succeed before navigation: if it fails the user stays on
Step 3 with a toast, because navigating with no persisted intent would
land them in WorkspaceOnboardingInit's branch 3 "no decision yet" rescue
and trigger a redirect loop back to /onboarding.
The prior asymmetry (Connect deferred bootstrap to the workspace, Skip
ran bootstrap inline) is gone — both paths defer to the workspace
shell now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): v3 — thin server, frontend-orchestrated welcome
Collapse v2's persisted runtime-choice fields + 4-branch dispatcher +
OnboardingService/WorkspaceContentService stack down to a single rule:
`onboarded_at` is the only state field, layout hard-gates on it, and the
welcome experience after Step 3 is owned entirely by the frontend.
V3 flow
- Step 3 button: await POST /api/me/onboarding/complete (mark only) +
park a transient signal in `useWelcomeStore` + navigate
- Workspace layout: hard gate `onboarded_at == null` -> /onboarding
- `<WelcomeAfterOnboarding />` reads the welcome-store signal:
- runtime path: find-or-create Multica Helper via generic createAgent
with bilingual instructions from `templates/helper-instructions.ts`,
blocking modal with 3 starter cards, pick -> createIssue + navigate
- skip path: provision install-runtime (in_progress) -> agent-guide
(todo, body embeds install-runtime mention chip) -> follow-up comment
on install-runtime mentioning agent-guide; then pop celebration
modal with 🎉 emoji pop animation, 2 read-only preview cards, single
[Got it] CTA that navigates to install-runtime
Server cleanup
- Drop OnboardingService, WorkspaceContentService, v2 runtime-choice
columns/CHECK on user, EnsureOnboardingContent endpoint
- CompleteOnboarding/AcceptInvitation call qtx.MarkUserOnboarded
directly (no service indirection)
- BootstrapOnboardingRuntime / BootstrapOnboardingNoRuntime kept as a
deprecation shim in onboarding_shim.go for desktop < v3 during the
rollout window — handlers inlined to qtx.* calls, no service layer
Localization
- Persisted strings (issue titles/bodies, Helper instructions/
description, comment prefix) live as TS const `{en, zh}` maps in
`packages/views/onboarding/templates/` — i18n bundle staleness can no
longer write raw key paths into DB
- UI-rendered strings (modal copy, status chips, buttons) stay in
`packages/views/locales/{en,zh-Hans}/onboarding.json`
- Language picked from live `i18n.language` (not `me.language`, which is
null for new users until they pick a preference)
Race protection
- Module-level promise dedupe (`findOrCreateHelper`, `seedIssueDeduped`,
`postCommentDeduped`) so React StrictMode double-mount can't fire two
parallel API calls that the server would then 409
Cross-references between the two skip-path issues render via Multica's
mention-chip protocol `[<identifier>](mention://issue/<uuid>)` so they
match the styled IssueChip pills used elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(onboarding): welcome-after-onboarding modal redesign + cross-user safety
Welcome modal polish (the post-Step-3 surface this branch already
introduced):
Runtime path
- Helper avatar replaces the bouncy 🎉 hero; tone-down animation to
fade. New copy: "Hi, welcome to Multica / I'm your first Agent
assistant" + capability hint sentence so users discover assignment +
chat from the first screen.
- Cards changed from "click = submit" to multi-select with the existing
border-primary + ring selection pattern used by compact-runtime-row;
bottom CTA "Assign N tasks to me →" appears only with N>0.
- New starter cards: intro / tour / welcome_page (the last one tells
Helper to paste an HTML welcome page into the issue comment — works
on any runtime regardless of fs access).
- Success state added between createIssue and navigation: 🎉 +
"All set!" + "Sit tight ☕ — your {agentName} is on it" + inbox/chat
hints, single [Got it] button.
- Title/prompt for starter cards now live in TS const
HELPER_STARTER_PROMPTS (persisted to DB — must not depend on i18n
bundle being loaded); subtitle stays in onboarding.json.
Skip path
- Body restructured into three independent ```md blocks (Name /
Description / Instructions) so each picks up the markdown renderer's
per-block copy button — no manual extraction.
- ZH body now embeds the ZH Helper Description + Instructions (was
Chinese-around-English-block).
- Follow-up comment uses Multica's mention-chip protocol
[identifier](mention://issue/uuid) so it renders as the styled
IssueChip pill.
- Issue titles bilingual with "Step 1 / Step 2" prefix.
Cross-user / cross-workspace safety (code review feedback)
- web onLogout + desktop handleDaemonLogout now call
useWelcomeStore.reset() so user B logging into the same browser
doesn't inherit user A's signal.
- WelcomeAfterOnboarding gates on
currentWorkspace.id === signal.workspaceId — prevents firing the
modal in workspace B when the signal was parked for workspace A
(desktop multi-tab, back/forward, deep-link).
- Module-level promise dedupes (pendingHelperSetup,
pendingIssueSeed, pendingCommentSeed) for the three API calls so
React 18+ StrictMode dev double-mount can't race-create duplicates.
Other small fixes carried in this commit
- Helper instructions / agent description / starter card titles all
read i18n.language (not me.language, which is null for new users
who haven't picked a UI language preference yet).
- Reverted welcome-emoji-pop animation to a small fade for the runtime
avatar (kept the bouncy variant for the skip 🎉 hero where the
celebration is the whole point).
- Removed the duplicate 🎉 from the skip modal title (kept the hero
one only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(views): i18n hardcoded "Close" in welcome FullScreenError
CI lint (i18next/no-literal-string) blocked on a literal "Close" string
inside `FullScreenError` — surfaced as a nit in the original code
review but missed in the merge. Add `error_close` to onboarding.json
(EN: "Close" / ZH: "关闭") and thread it through as a `closeLabel`
prop, matching the existing `retryLabel` plumbing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e288eff2c5 |
feat: server auto-generates PAT for cloud runtime bootstrap (#3002)
When bootstrap is enabled and no PAT is available from the request header or Authorization bearer token, the server now generates a new PAT automatically and forwards it to the cloud service. This removes the need for the frontend to pass X-User-PAT — the server handles it entirely. |
||
|
|
29c2a5d18f |
fix(daemon): reclaim stale dispatched claims (MUL-2485) (#2872)
* fix(daemon): reclaim stale dispatched claims * fix(daemon): widen stale claim reclaim window |
||
|
|
0c767c0052 |
feat(issues): per-issue metadata KV (MUL-2017) (#2845)
* feat(issues): per-issue metadata KV (MUL-2017)
Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).
All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.
GET /api/issues/:id/metadata
PUT /api/issues/:id/metadata/:key body: { "value": <primitive> }
DELETE /api/issues/:id/metadata/:key
Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.
CLI: multica issue metadata {list,get,set,delete}
multica issue list --metadata key=value (repeatable, AND)
set has --type to override the default value-sniffing
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)
- Fix two failing handler tests blocking backend CI:
- reset decode target after delete so map merge does not mask removal
- url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
the issue has no keys so it stays quiet in the common case.
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtime): teach agents to read/write issue metadata (MUL-2017)
Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)
main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.
Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
614dfae884 |
MUL-2488 feat(timezone): Scheduling / Viewing two-layer timezone architecture (#2968)
* docs(timezone): add scheduling/viewing timezone architecture RFC * feat(db): replace daily rollups with task_usage_hourly, add user.timezone Migrations 100-104: add "user".timezone (Viewing tz), build the UTC hourly task_usage_hourly rollup with its pipeline, drop the legacy task_usage_daily / task_usage_dashboard_daily pipelines, and drop the agent_runtime.timezone column. Report queries now slice day boundaries at read time by the caller-supplied @tz instead of materialising in a fixed tz. Regenerate sqlc. * feat(server): add task_usage_hourly backfill command Replace the two legacy backfill commands (daily / dashboard_daily) with a single backfill_task_usage_hourly that loads historical task_usage into the new UTC hourly rollup, sliced per workspace. * refactor(server): resolve viewing timezone in report handlers Report handlers resolve the Viewing tz per request (?tz query param, then user.timezone, then UTC) and pass it to the hourly-rollup queries. Drop the UseDailyRollup feature flags and the old raw-scan/daily-rollup dual paths, remove the /api/usage endpoints, and stop the daemon from reporting and the runtime handler from accepting host timezone. * refactor(core): switch report queries to viewing timezone API client and dashboard/runtime queries send ?tz with each report request, the user schema/types carry the new timezone field, and the runtime timezone field/mutation is removed. * feat(views): add viewing timezone preference and UI Add the useViewingTimezone hook and a Timezone setting in Preferences; report charts and the dashboard week boundary follow the viewer tz. Remove the runtime detail timezone editor and its locale strings. * fix(test): update fixtures and stabilize tests for timezone refactor The timezone architecture refactor changed several types without updating dependent test code: - RuntimeDevice no longer has a timezone field — drop it from the create-agent-dialog runtime fixture. - User now requires a timezone field — add it to the apps/web mockUser fixture. - The PreferencesTab timezone tests asserted on the async save handler (PATCH then store update) with a bare expect, racing the mutation's settle callback, and timed out querying the Select's ~600-option IANA list on a loaded CI runner. Wrap the assertions in waitFor and extend the timeout for those three tests. * docs(timezone): document self-host migration order and trigger invariant Add a SELF-HOST UPGRADE ORDER runbook to the backfill command's package comment: applying migrations 100-104 in a single migrate-up drops the legacy daily rollups before the hourly backfill runs, leaving dashboards empty until cron catches up. Add an INVARIANT comment on trg_atq_dirty_hourly noting that agent_id must be added to the trigger's OF list if it ever becomes mutable, otherwise dirty buckets for the old agent_id are silently missed. * style(runtimes): drop trailing blank line in runtime-detail |
||
|
|
41cb91abd9 |
feat: add cloud runtime fleet proxy API (MUL-2453) (#2986)
* feat: add cloud runtime fleet proxy API Co-authored-by: multica-agent <github@multica.ai> * test: cover cloud runtime handler nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7f9e4e829d |
feat(comments): thread-internal --tail pagination + reply cursor (MUL-2421) (#2846)
* 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> |
||
|
|
2f1f90c11a | fix(agent): retry codex semantic inactivity fresh (#2593) | ||
|
|
8d4f4caf4a |
MUL-2338 fix(comments): allow agent self-mention to enqueue cross-issue handoff (#2928)
* fix(comments): allow agent self-mention to enqueue cross-issue handoff
The @mention path in CreateComment unconditionally skipped any
self-mention. That dropped the child→parent handoff between issues
assigned to the same agent: the child run posted `@J` on the parent
issue, the guard tripped, and the parent's J was never woken — the chain
silently broke.
Drop the self-trigger `continue` in the agent mention branch. Runtime
ready / private-agent gate / HasPendingTaskForIssueAndAgent dedup all
remain, so a same-issue self-mention while a queued or dispatched task
exists is still deduped; a running task no longer pre-empts a new
follow-up (the existing queue coalescing handles that).
Three regression tests:
- cross-issue self-mention enqueues a task on the target issue
- same-issue self-mention while running queues a follow-up
- same-issue self-mention with a pre-existing queued/dispatched task
is deduped
MUL-2338
Co-authored-by: multica-agent <github@multica.ai>
* test(handler): assign per-workspace issue number in self-mention fixture
The fixture inserts two issues in the same test workspace; without an
explicit number both default to 0 and the second insert violates
uq_issue_workspace_number, taking the backend CI job down on PR #2928.
Mirror the workspace-counter advancement pattern from
issue_scheduled_test.go so each fixture issue gets a unique number.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
1f978bf1ec |
feat(autopilot): link created issues to projects (#2908)
* feat(autopilot): link created issues to projects * test(autopilot): cover project flag |
||
|
|
b7082a01f1 |
fix(issues): retry button targets the row's agent (MUL-2457) (#2921)
* fix(issues): retry button targets the row's agent, not the assignee (MUL-2457)
The execution log retry button used to re-fire the issue's current
assignee instead of the agent that actually ran the clicked row. After
a reassignment, or for squad workers / @-mention agents, the rerun
landed on the wrong agent.
POST /api/issues/{id}/rerun now accepts an optional task_id: when set,
the rerun targets that task's agent (and reuses its leader/worker
role). An empty body keeps the assignee-driven CLI/API contract.
The execution-log retry button passes task.id, so per-row retry always
fires the correct agent. enqueueMentionTask gained a forceFreshSession
parameter so the new mention-path rerun keeps the same fresh-session
contract as the assignee path.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): inherit trigger provenance + fix cross-issue test (MUL-2457)
Address review feedback on PR #2921:
1. RerunIssue now inherits TriggerCommentID from the source task when
sourceTaskID is valid. Without this, a per-row rerun of a comment-
or mention-triggered task degrades into a generic issue run because
the daemon's buildCommentPrompt path keys on TriggerCommentID. The
inherited summary is rebuilt naturally inside the enqueue helpers
(buildCommentTriggerSummary derives it from the comment ID).
2. The new cross-issue rejection test inserted a second issue without
`number`, hitting uq_issue_workspace_number on a same-workspace
collision with the fixture's issue. Both inserts now claim the next
available per-workspace number (MAX(number)+1) — matching the
pattern used by notification_listeners_test.
Added TestRerunIssueInheritsTriggerCommentFromSourceTask to lock the
trigger provenance contract.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
314e91fa6d | fix(chat): guard optimistic task message ids (#2901) | ||
|
|
2bec2221d2 |
feat(agent): per-agent thinking_level for claude + codex (MUL-2339) (#2865)
* feat(agent): persist thinking_level per agent (MUL-2339) Adds a nullable `thinking_level` column to the `agent` table so the backend can route a runtime-native reasoning/effort token (e.g. Claude's `xhigh`, Codex's `minimal`) through to the agent CLI on every dispatch. The column is intentionally TEXT rather than an enum — Claude and Codex publish overlapping but distinct vocabularies and we want the persisted value to round-trip exactly through whichever CLI receives it. NULL is the "use runtime default" sentinel that every downstream consumer reads as "do not inject --effort / reasoning_effort". This commit is just the storage layer (migration + sqlc); subsequent commits wire it through the API, daemon, and agent backends. Co-authored-by: multica-agent <github@multica.ai> * feat(agent-backend): inject reasoning effort for claude + codex (MUL-2339) Extends ExecOptions with a runtime-native ThinkingLevel string and wires it into the Claude and Codex backends. Discovery is driven by the local CLI so the daemon advertises whatever the host install supports rather than a hand-maintained list that goes stale. Per Elon's PR1 review: - Claude: parses `claude --help` to learn the `--effort` superset and projects through a per-model allow-list (xhigh is Opus-only; max is session-only on the smaller models). Falls back to a conservative static list when the binary is missing or help drift hides the line. - Codex: drives `codex debug models --output json` so per-model reasoning subsets and the documented default come directly from the CLI. The older config-error probe trick is gone — the JSON path is stable and doesn't pollute stderr with an intentional misconfig. - Cache key includes (provider, executablePath, cliVersion) so a CLI upgrade invalidates entries that referenced the older help / catalog. Per Trump's PR1 constraint, all three Codex injection points (thread/start.config, thread/resume.config, turn/start.effort) flow through one helper (`applyCodexReasoningEffort`) so they cannot drift independently. The shared `codexReasoningCases` fixture in `thinking_test.go` asserts the same value→{shape, key} contract at each site for every level the runtimes know about. Claude's `--effort` is also added to `claudeBlockedArgs` so a user custom_args entry can't silently outvote the daemon-injected value. Co-authored-by: multica-agent <github@multica.ai> * feat(api): wire thinking_level through API + daemon contract (MUL-2339) End-to-end plumbing for the per-agent reasoning/effort setting: - AgentResponse / TaskAgentData now carry `thinking_level`; the daemon's claim response includes it and the daemon's executor passes it through to agent.ExecOptions, where the Claude and Codex backends already know what to do with it. - ModelEntry on the runtime-models wire format gains a `thinking` block carrying `supported_levels` + `default_level` per model so the UI can render a runtime-aware picker without the server having to know about the local CLI install. `handleModelList` projects the agent-package catalog (including the new Thinking field) into the wire shape. - CreateAgent / UpdateAgent gate the field with a synchronous provider enum check (claude / codex only today). UpdateAgent is tri-state: field omitted = no change, "" = explicit clear (new `ClearAgentThinkingLevel` query, mirrors the existing mcp_config null pattern), non-empty = validate then set. Per Trump's PR1 review, the API NEVER auto-clears on a runtime/model swap and ALWAYS returns 400 on an unknown literal value — same shape across CreateAgent, UpdateAgent, and combined patches that move runtime + level in one request. Per-model combination failures (e.g. `xhigh` against a model that only supports up to `high`) surface as a daemon-side task error, not a silent server-side rewrite. TS types follow the same shape: `Agent.thinking_level`, `CreateAgentRequest`/`UpdateAgentRequest` add the field, `RuntimeModel` grows a `thinking` block. Older backends omit the field, which the front-end treats as "no picker for this model" — installed desktop builds keep working. Co-authored-by: multica-agent <github@multica.ai> * fix(agent): correct codex debug models argv + pin via runner test (MUL-2339) `codex debug models --output json` is rejected by codex-cli 0.131.0 — the subcommand emits JSON on stdout by default and has no `--output` flag. Drop the flag and add `--bundled` to skip the network refresh discovery doesn't need. Move the argv to a package-level var and add a test that runs a fake `codex` to assert the binary actually receives exactly `debug models --bundled`, so the contract can't silently drift on the next refactor. Also teach ValidateThinkingLevel to resolve an empty model to the provider's default model entry. Without this, every default-model task with a persisted thinking_level would be misjudged "unknown model" by the daemon guard. Co-authored-by: multica-agent <github@multica.ai> * fix(api): reject runtime switch that would leave invalid thinking_level (MUL-2339) A PATCH that changed `runtime_id` without touching `thinking_level` used to silently keep the existing value, so a Claude agent storing `max` could land on a Codex runtime where `max` is not a recognised token at all, and the daemon would receive a literal-invalid level. Hold the same "always 400 on literal-invalid, never silent coerce" rule on this implicit path. When runtime_id changes and the existing value is not in the new provider's enum, return 400 with the recovery options (clear via `thinking_level=""` or re-set in the same PATCH). Add coverage for both the kept-when-still-valid and the rejected cases, plus the two recovery paths (clear and replace). Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): guard runTask with per-model thinking_level validator (MUL-2339) ValidateThinkingLevel existed but had no call site — `task.Agent. ThinkingLevel` flowed straight into ExecOptions, so `xhigh` configured on a non-Opus Claude model, or API-side stale values that escaped the provider enum gate, would be injected anyway. Run the validator before building ExecOptions. Invalid combinations log a warning and drop the level instead of failing the task: the agent still runs, just at the runtime's default reasoning effort. Discovery errors fail open (keep the level, let the CLI surface any objection) so a transient `claude --help` failure can't strand work. Empty model is forwarded as-is; the validator resolves it to the provider's default model internally per the cross-package contract. Co-authored-by: multica-agent <github@multica.ai> * chore(agent): drop stale `--output json` comments + unused scanner (MUL-2339) Codex CLI's `debug models` subcommand emits JSON without an `--output` flag, and `parseCodexDebugModels` never read from the bufio.Scanner. Sync the comments with the actual invocation and remove the dead init. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fc8528d64d |
feat(autopilot): support assigning to a squad (MUL-2429) (#2888)
* feat(autopilot): support assigning autopilot to a squad (MUL-2429) Path A (Squad-as-Leader) from the RFC: when an autopilot's assignee is a squad, dispatch resolves to squad.leader_id and executes against the leader's runtime — semantics match a human manually assigning the issue to that squad, no fan-out. Backend scope only; frontend picker change is a follow-up PR. Changes: - 096_autopilot_squad_assignee migration: drop agent FK on autopilot.assignee_id, add assignee_type column (default 'agent'), add autopilot_run.squad_id attribution column. - service.AgentReadiness: single source of truth for archived / runtime-bound / runtime-online checks. Shared by autopilot admission gate, run_only dispatch, and isSquadLeaderReady. - service.resolveAutopilotLeader: translates assignee_type/id to the agent that actually runs the work. - dispatchCreateIssue: stamps issue with assignee_type='squad' for squad autopilots and enqueues via EnqueueTaskForSquadLeader. - dispatchRunOnly: belt-and-braces readiness re-check after resolving squad → leader so a leader that went offline between admission and dispatch produces a clean failure instead of a doomed task. - handler.CreateAutopilot / UpdateAutopilot: accept assignee_type with squad/agent existence + leader-archived validation. Backward-compatible default of "agent" preserves the contract for older clients. - Analytics: AutopilotRunStarted/Completed/Failed events carry assignee_type and squad_id; PostHog can now group autopilot runs by squad without joining back to the autopilot row. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): reject archived squads, route post-admission skips, cleanup dangling-agent autopilots (MUL-2429) Addresses three review findings on PR #2888: 1. Archived squad handling: validateAutopilotAssignee now rejects squads with archived_at set; resolveAutopilotLeader returns errSquadArchived so the admission gate fails closed; DeleteSquad now mirrors the issue transfer for autopilot rows (TransferSquadAutopilotsToLeader) so surviving autopilots flip to assignee_type='agent' (leader) instead of dangling at the archived squad. 2. dispatchRunOnly post-admission readiness: introduces errDispatchSkipped sentinel, recognised by DispatchAutopilot via handleDispatchSkip so the run is recorded as `skipped` (not `failed`). Manual triggers no longer 500 when the leader's runtime goes offline between admission and task creation. New TestManualTriggerDoesNotErrorOnPostAdmissionSkip locks the behaviour in. 3. Dangling agent assignee after migration 096 dropped the FK: shouldSkipDispatch now distinguishes pgx.ErrNoRows / errSquadArchived (hard skip — retrying won't help) from transient DB errors (fail-open). DeleteAgentRuntime pauses autopilots that target agents about to be hard-deleted (ListArchivedAgentIDsByRuntime + PauseAutopilotsByAgentAssignees) so the breakage surfaces as a paused row in the UI instead of a quiet skip-burning loop. Unit tests cover the sentinel unwrap contract and errSquadArchived errors.Is behaviour. Integration test TestAutopilotDispatchSkipsWhenRuntimeOffline re-verified against a fresh DB with migration 096 applied. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): bump last_run_at on post-admission skip (MUL-2429) Match recordSkippedRun (pre-flight skip) and the success path so the scheduler / "last seen" UI both reflect that this tick evaluated the trigger, even when the post-admission readiness gate caught a late regression. Addresses Emacs review caveat #1 on PR #2888. Co-authored-by: multica-agent <github@multica.ai> * feat(autopilot): mixed agent/squad assignee picker in dialog (MUL-2429) End-to-end UI for assigning an autopilot to a squad. Closes the PR #2888 backend gap: the squad-as-assignee feature was already wired in Go (Path A, RFC §4) but the desktop dialog never offered the choice. - core/types/autopilot: add `AutopilotAssigneeType`, surface `assignee_type` on `Autopilot` + Create/Update request payloads. - views/autopilots/pickers/agent-picker: switch to a polymorphic AssigneeSelection (`{type, id}`); render agents and squads as two grouped sections with shared pinyin search. - views/autopilots/autopilot-dialog: maintain `assigneeType` state, send it on create/update, render the trigger avatar / hover dot with `assignee.type`. - views/autopilots/autopilots-page + autopilot-detail-page: render the assignee row using `autopilot.assignee_type` so squad-typed autopilots show the squad avatar + name, not a broken agent lookup. - locales: add `agents_group` / `squads_group` / `select_assignee` keys (en + zh-Hans), keep legacy `select_agent` for callers that still reference it. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e48f6a84d6 |
feat(github): expose read-only installation list to workspace members (MUL-2413) (#2886)
* feat(github): expose read-only installation list to workspace members (MUL-2413)
Relax `GET /api/workspaces/{id}/github/installations` from owner/admin-only
to any workspace member so the Settings → Integrations tab no longer renders
blank for non-admins (the original symptom of MUL-2413).
The handler now reads the caller's role from the workspace middleware:
- owner / admin keep the full row including the numeric `installation_id`
(the connect / disconnect handle) and receive `can_manage: true`.
- every other role (member / guest) receives rows with `installation_id`
omitted and `can_manage: false`, giving them visibility into "is GitHub
wired up?" without the management handle.
`GET /github/connect` and `DELETE /github/installations/{id}` stay under
the admin/owner middleware group — this PR only relaxes the read path.
Tests: `TestListGitHubInstallations_RoleGating` exercises admin, owner,
member, and guest paths against the real DB-backed handler fixture and
asserts the field stripping + `can_manage` contract.
Refs: MUL-2413
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): redact installation_id from realtime broadcasts (MUL-2413)
GET /github/installations strips the numeric installation_id for non-admin
members, but the github_installation:created / uninstall / suspend WS
events were still publishing it, so the same handle was reachable from
any workspace client subscribed to the workspace scope. Broadcast both
payload variants without it — the frontend uses these events only to
invalidate the installations query, so admins re-query the list endpoint
to recover the management handle.
Also adds a router-level test that mounts the production middleware split
(member-visible list vs. owner/admin connect+delete) so a future routing
change can't silently widen the write surface.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
2ad1cd8ff8 |
feat(profile): user profile description injected into agent brief (MUL-2406)
## Summary Adds per-user `profile_description` so coding agents have cheap, durable context about who is asking. v1 per the brief Xeon locked in on [MUL-2406](mention://issue/63a7247c-4f6a-42cf-90d1-7c746e77158a): - **DB** — `user.profile_description TEXT NOT NULL DEFAULT ''` (migration 096). 2000-rune cap enforced server-side. No nullable / privacy state to manage. - **API** — `PATCH /api/me` accepts the field; `UserResponse` always emits it. Client wraps `updateMe` in a lenient `UserSchema` + `EMPTY_USER` fallback per CLAUDE.md API Response Compatibility. - **UI** — Settings → Account gains an "About you" textarea with live `n/2000` counter, `maxLength` guard, and a localized too-long error (EN + zh-Hans). - **CLI** — `multica user profile get` / `multica user profile update` with `--description / --description-stdin / --description-file / --clear`, mirroring the existing `issue comment add` input-mode menu. - **Daemon injection** — claim handler resolves the runtime owner and stamps `requesting_user_name` + `requesting_user_profile_description` on the task. `buildMetaSkillContent` emits `## Requesting User` between `## Agent Identity` and `## Available Commands`, blockquoted and framed as background context. The block is omitted entirely when the description is empty (no token cost when unused). Brief is written **once per task** via `CLAUDE.md` / `AGENTS.md`, not the per-turn prompt — same path the agent already reads for identity, so no extra per-turn cost. ## Test plan - [x] `go build ./...`, `go vet ./...`, `go test ./internal/cli/ ./internal/daemon/ ./internal/daemon/execenv/ ./cmd/multica/` - [x] New brief tests: `TestBuildMetaSkillContentEmitsRequestingUser`, `TestBuildMetaSkillContentOmitsRequestingUserWhenEmpty` - [x] `pnpm typecheck`, `pnpm lint`, `pnpm test` (74 files, 644 tests pass) - [ ] Handler DB tests (`TestUpdateMe*`) require a migrated test DB — not runnable in this sandbox - [ ] Manual: open Settings → Account, set a description, confirm the next daemon-run agent's `CLAUDE.md` shows `## Requesting User` |
||
|
|
591e47842d |
refactor(onboarding): remove starter-content kit; unify install-runtime issue across mark-onboarded paths (MUL-2438) (#2884)
* refactor(onboarding): remove starter-content kit, unify install-runtime issue across mark-onboarded paths (MUL-2438) Drops the post-onboarding ImportStarterContent / DismissStarterContent flow (handler + routes + StarterContentPrompt + templates + locale strings + analytics event). The bug — web onboarding seeding 6+ starter issues without a runtime — only existed through that path; with it gone the source disappears. The "install a runtime" issue from BootstrapOnboardingNoRuntime is now the canonical no-runtime onboarding seed. The title/description and a LockAndFindActiveDuplicate-deduped seeder move to handler/no_runtime_issue.go, and CompleteOnboarding / CreateWorkspace / AcceptInvitation seed it whenever the workspace has no runtime yet, so every mark-onboarded entry point lands the user on a concrete next step. starter_content_state column is kept and continues to be claimed as 'imported' in all five entry points so older desktop builds (which still render the legacy dialog on NULL) don't surface it to accounts created after this change. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): backfill starter_content_state for in-window NULL users (MUL-2438) 054 only covered pre-feature users. Anyone onboarded between then and the starter-content kit removal could still sit at NULL, and old desktop clients gate the legacy StarterContentPrompt on `starter_content_state IS NULL`. The import/dismiss routes are gone, so leaving these rows NULL would surface a dialog whose buttons 404. Mark them 'imported' to match the new helper's claim semantics. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
cd37b4e3d6 | feat(settings): consolidate GitHub options under a dedicated Settings tab (MUL-2414) | ||
|
|
54368fd826 |
feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) (#2856)
* feat(projects): scheduled-only Gantt data source + WS reactivity (MUL-1881) Project Gantt now fetches its own scheduled-only data instead of riding the Board/List pagination cache. The Unscheduled drawer and pagination warning banner are gone, and any WS-driven issue change (create / update / delete) invalidates the new cache so the timeline stays live. - Backend: `GET /api/issues?scheduled=true` adds an `(i.start_date IS NOT NULL OR i.due_date IS NOT NULL)` predicate on both ListIssues and CountIssues. New SQL filter is plumbed through sqlc + handler. - Frontend: new `projectGanttIssuesOptions(wsId, projectId)` issues a single fetch and lives under its own cache key. WS handlers and mutations invalidate the prefix on create/update/delete so the bar reacts to start_date / due_date changes from other tabs and from this tab without waiting on the WS round-trip. - GanttView: drops the Unscheduled section, the pagination warning banner, and the load-all button; renders only scheduled rows. - Removes now-dead `useLoadAllRemaining`, `myIssueListPaginationOptions`, `summarizeIssueListPagination`, and the gantt locale strings that supported the old plumbing. Co-authored-by: multica-agent <github@multica.ai> * fix(projects): page through Gantt fetch and isolate per-view data sources - Walk paginated `scheduled=true` issues until total is reached so projects with more than 500 scheduled bars no longer silently truncate. - Gantt mode disables the bucketed Board/List query and reads its own scheduled cache for the project empty-state check, so the page never short-circuits Gantt with a Board-derived "no issues" CTA. - `onIssueLabelsChanged` patches matching rows in the Project Gantt cache in-place, keeping label filters consistent after attach/detach from other tabs or agents. MUL-1881 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
59617f376e |
feat(auth): make auth token TTL configurable via AUTH_TOKEN_TTL env var (MUL-2371) (#2713)
* feat(auth): make auth token TTL configurable via AUTH_TOKEN_TTL env var Add AUTH_TOKEN_TTL environment variable (in seconds) to override the hardcoded 30-day auth token lifetime. Self-hosted deployments on trusted networks can set a longer value to avoid frequent magic-link re-authentication. The value is read once at startup and cached. Invalid or missing values fall back to the 30-day default with a warning log. Closes #2685 * refactor(auth): extract parseAuthTokenTTL for testability Address review feedback: extract pure parse function from sync.Once wrapper so the parsing logic can be unit-tested independently. Add TestParseAuthTokenTTL with table-driven cases. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> * refactor(auth): accept Go duration strings + hoist shared TTL in SetAuthCookies Address nice-to-have review feedback from Bohan-J: - parseAuthTokenTTL now tries time.ParseDuration first (e.g. '8760h'), falling back to ParseInt for integer seconds - Warn on unreasonable values (>10 years) but still accept them - Hoist AuthTokenTTL() and time.Now() in SetAuthCookies so both cookies share the exact same expiry - Add security trade-off note in .env.example - Add 5 new test cases for duration strings Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com> Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> * fix: use AuthTokenTTL() in CloudFront middleware, guard ParseInt overflow Address review feedback from Bohan-J (round 2): 1. CloudFront refresh middleware (cloudfront.go:21) was hardcoding 30*24*time.Hour instead of using auth.AuthTokenTTL(). Now calls AuthTokenTTL() so the middleware respects AUTH_TOKEN_TTL env var. 2. parseAuthTokenTTL integer-seconds branch: very large values like 9999999999 would silently overflow int64 when multiplied by time.Second. Added overflow guard comparing against math.MaxInt64/int64(time.Second) before the multiplication. 3. Updated AuthTokenTTL() doc comment to reflect that it accepts Go duration strings or integer seconds (not just seconds). 4. Added middleware test (cloudfront_test.go) verifying short AUTH_TOKEN_TTL produces short cookie expiry, not 30-day hardcode. Also covers nil signer and existing-cookie-skip cases. 5. Added integer overflow test case to cookie_test.go. * style: run gofmt on cookie.go and cookie_test.go --------- Signed-off-by: kagura-agent <kagura.agent.ai@gmail.com> Co-authored-by: Claude Opus 4 (1M context) <noreply@anthropic.com> |
||
|
|
6f21cb8f3e |
[codex] Simplify onboarding runtime bootstrap (#2836)
* feat(onboarding): simplify runtime bootstrap * fix(onboarding): close private-helper reuse hole and guide-issue nav race - server: when bootstrap looks for an existing Multica Helper, require Visibility="workspace" so a private helper owned by another member can't be auto-assigned to the onboarding issue (and trigger a task as that private agent), which would have bypassed canAccessPrivateAgent. - web onboarding page: refreshMe() inside bootstrap flips hasOnboarded before onComplete fires, letting the guard's router.replace overtake onComplete's router.push to the new guide issue. Mark the page as "completing" right before navigating so the guard stays silent during the in-flight transition. Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): escape daemon command literals to satisfy i18next/no-literal-string Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Lambda <lambda@multica.ai> |
||
|
|
e19f7967b9 |
feat(prompt): thread-first comment reads for agent runs (MUL-2387) (#2816)
* feat(prompt): thread-first comment reads for agent runs (MUL-2387) PR #2787 added --thread / --recent / --before / --before-id to the ListComments API and CLI but kept the agent prompt steering at the legacy "dump everything" recipe. On a long-running issue the flat dump burns context on chatter unrelated to the trigger; agents acting on the trigger want the trigger's thread first. Prompt updates: - Comment-triggered Workflow (runtime_config.go) now anchors step 2 on `multica issue comment list <issue-id> --thread <trigger-comment-id> --output json`. Fallback offers `--recent 20 --output json` with the stderr `Next thread cursor: --before <ts> --before-id <root-id>` line feeding the next-page cursor. `--since` is preserved and explicitly marked combinable with --thread / --recent. - Per-turn buildCommentPrompt (prompt.go) carries the same thread-first guidance so a Codex-style runtime that re-reads the per-turn message every iteration gets the same steering, even if it ignores the injected runtime config. - Assignment-triggered Workflow keeps the mandatory full-history rule (MUL-1124) but now also points at `--recent 20` as the long-issue alternative — this is the place that previously had no thread-aware guidance at all. - Default fallback prompt (no trigger comment, no chat, no autopilot, no quick-create) gains the same --recent hint without --thread (no comment to anchor on). - Available Commands core line surfaces the new flags so the discovery path matches the workflow guidance. Default CLI/API semantics are unchanged: the unparameterized list still returns the full chronological dump capped at 2000, --since still works on its own, and the desktop UI is untouched. Tests: - prompt_test.go: TestBuildPromptCommentTriggerPromotesThreadReads pins --thread <triggerID>, --recent 20, the stderr cursor phrasing, and the absence of the legacy "returns all comments" prose. - prompt_test.go: TestBuildPromptDefaultMentionsRecent guards the no-trigger fallback (mentions --recent, must NOT mention --thread). - execenv_test.go: TestInjectRuntimeConfigCommentTriggerThreadFirstReads asserts the comment-triggered Workflow steers at --thread/--recent, the Available Commands line surfaces the new flags, and the legacy "read the conversation (returns all comments...)" string is gone. - execenv_test.go: TestInjectRuntimeConfigAssignmentTriggerMentionsRecent keeps the mandatory full-history rule pinned AND asserts --recent is offered as the long-issue alternative. Also fixes the recent+since cursor nit Elon flagged in #2787's second review: when `since` empties the page, the `len(seenRoot) >= recentN` check used to emit a cursor anyway. Pagination walks threads in strictly decreasing last_activity_at — if every comment in this page is <= since, every older thread's last_activity is also <= since by transitivity, so the cursor would only invite the caller into a guaranteed-empty walk. Now suppressed; new tests pin both branches (suppressed when empty, retained when at least one row passes since). MUL-2387 Co-authored-by: multica-agent <github@multica.ai> * fix(comments): suppress recent+since cursor when head thread past since (MUL-2387) Previous suppression only tripped when the `since` filter emptied the page. That missed the mixed case Elon flagged in #2787's second review: the page keeps rows from fresher threads but the head (oldest-active) thread already sits at or before `since`, so every older page is guaranteed empty too. Predicating on `headLast <= since` covers both cases. Add a recent=2 + since fixture that pins the mixed scenario: root1 (last_activity = base+3m) is filtered out, root2 stays, and the cursor is suppressed even though the body is non-empty. Co-authored-by: multica-agent <github@multica.ai> * fix(prompt): clarify --recent is paging, not a replacement (MUL-2387) Address Elon's second-pass nit on #2816: the assignment-trigger workflow in runtime_config.go used "you may switch to --recent 20", which reads as a replacement for the mandatory full-history rule. Rephrase --recent as a paging strategy ("read the full history page-by-page, not a shortcut that replaces it") so it cannot conflict with the rule it lives next to. The default per-turn prompt in prompt.go opened with "If you need comment history" — that soft conditional contradicts the runtime workflow's mandatory read. Move it to a neutral "For comment history, follow the rule in your runtime workflow file" framing that defers to whatever the workflow says (mandatory for assignment, optional elsewhere) instead of encoding its own policy. Keep the runtime/prompt dual-layer fallback intact — different runtimes propagate the config file vs. the per-turn user prompt with varying fidelity, so both surfaces need the guidance. Tests pin the new phrasing against regression: - TestBuildPromptDefaultMentionsRecent now also forbids "If you need comment history" from sneaking back in. - TestInjectRuntimeConfigAssignmentTriggerMentionsRecent now also forbids "you may switch to" / "switch to `--recent" replacement phrasing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8d30d76300 |
feat(dashboard): add 1d range to workspace Usage tab (#2837)
* feat(dashboard): add 1d time range to workspace Usage tab 1d means "today" — the natural calendar day from 00:00 UTC, matching the rollup's bucket_date axis — not the trailing 24 hours. The client-side dailyCutoffIso filter is now applied in daily dim too so 1d collapses strictly to today even at the midnight UTC edge where the server's wall-clock since cutoff would otherwise include yesterday. Co-authored-by: multica-agent <github@multica.ai> * fix(dashboard): scope `1d` to today only on aggregate endpoints The pre-aggregated `byAgent` / `runTime` dashboard endpoints leaked yesterday into the agent leaderboard and KPI cards for the `1d` time range because `parseSinceParam(days=1)` returned `now-24h` (wall clock) and the downstream SQL then applied `DATE_TRUNC('day', @since)`, which landed on yesterday 00:00 UTC. The PR's client-side `dailyCutoffIso` filter could only fix the date-bearing daily endpoints; aggregate responses are already collapsed across dates. Anchor `parseSinceParam` at UTC start-of-today instead, so `days=N` covers N natural calendar days (today + N-1 prior). This matches the frontend `dailyCutoffIso = today - (days-1)` semantic that the workspace dashboard already assumes, and removes the off-by-one that previously made `30d` return 31 buckets. The runtime-detail page uses `parseSinceParamInTZ` (timezone-aware), which is unchanged — it has no `1d` option. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
c577a29c10 |
feat(onboarding): v2 per-question questionnaire (source/role/use_case) (#2814)
* feat(onboarding): per-question v2 questionnaire (source/role/use_case) Replaces the 3-questions-on-one-screen gate with three lightweight, individually-skippable steps. New step order: welcome → source → role → use_case → workspace → runtime → agent → first_issue - New v2 questionnaire schema: source/role/use_case + per-slot `*_skipped` markers. `team_size` removed. - Click-to-advance card grid with lucide + emoji icons (RFC Option B). - Skip is a footer text button; Other expands a free-text input. - Recommendation table updated for new role × use_case vocabulary, with use_case-only fallback when role is skipped. - DB migration v1 → v2 maps existing role/use_case answers and drops team_size; historical nulls stay null (not retroactively skipped). - Re-entry treats skipped slots as fresh; analytics record kept in DB. - onboarding_questionnaire_submitted event payload updated: source replaces team_size, per-slot skip booleans added. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): tighten question UX (Continue, layout, brand icons) Address review feedback on Source/Role/Use-case: - Replace auto-advance with an explicit Continue button so selections are reviewable. Continue is disabled until something is picked (and, for Other, until the free-text input is non-empty). - Move Back/Skip/Continue inline under the option grid; drop the duplicate Back from the top header — the page now has a single, anchored action row. - Swap the placeholder lucide marks for real brand SVGs on Source: Google, X, LinkedIn, YouTube, and an OpenAI mark for the AI-assistant option. Generic options stay on lucide. - Replace the awkward expanded underline input on the Other card with an inline borderless input that swaps in for the label slot, so the Other state has the same height and weight as the other cards. E2E smoke test updated to click Continue between question steps. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): unify step nav, rename Runtime step around "where agents run" - Refactor the Source/Role/Use case questionnaire steps to use the same 3-region chrome (header with Back + step indicator, scrolling main, sticky footer with Skip + Continue) that Workspace/Runtime/Agent already use, so the Back/Skip/Continue affordances stay in the same on-screen position across the whole flow. - Reframe the Runtime step around the user-visible question — "Where will your agents run?" — instead of the internal "runtime" concept. The aside panel keeps the educational "What's a runtime?" copy for users who want to learn. - Drop the hard-coded "Step 3 · Runtime" eyebrow on the web fork step: Runtime is now step 5 of 7 after the per-question split, and the step indicator already shows the correct count. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): tighten Skip/Continue spacing in step footer Group Skip and Continue inside a sub-flex with gap-2 so they read as a single action cluster on the right, while the status hint still anchors left via mr-auto. Applied to both the questionnaire steps and the runtime step so the footer layout stays consistent across onboarding. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): move Skip/Continue inline below form, drop sticky footer The sticky bottom footer left a large dead zone between the form content and the action buttons — most onboarding steps only fill the top third of the viewport. Move the hint + Skip + Continue inline, directly below the form/options grid, so the buttons sit where the eye already is after picking an option. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): match Skip button size to Continue (size="lg") Skip used the default button size (h-8) while Continue used size="lg" (h-9), so the two adjacent action buttons rendered visibly different heights. Promote Skip to size="lg" in step-question and step-runtime-connect so they line up. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): reframe step 3 as 'connect a computer' / 'pick an agent runtime' Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): replace cloud waitlist with "Coming soon", reword CLI intro - Web Step 3 cloud card: remove "Join waitlist" CTA + dialog and render a static "Coming soon" badge instead. Drops CloudWaitlistDialog, the cloud DialogState, waitlistSubmitted local state, and the onWaitlistSubmitted prop on StepPlatformFork (desktop's StepRuntimeConnect still owns its own waitlist path). - Tighten cloud_subtitle to drop the "join the waitlist" half now that the action is gone. - cli_install.intro: "AI coding tool" → "agent runtime", EN + zh-Hans. Tests updated to match: asserts the Coming soon badge is non-actionable and drops the four cloud-dialog scenarios (now unreachable). Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): refresh button, "agent runtime" wording, coming-soon card Three fixes on the desktop Step 3 empty state per review: 1. Empty headline + hints now say "agent runtime", matching the picker-context terminology established earlier in this PR. 2. Add a Refresh button (header pill in Found, inline with the headline in Empty). Desktop wires it to restart the bundled daemon so a freshly-installed Claude/Codex/Cursor CLI is picked up — the daemon's PATH probe runs once at boot, so without a restart the install would only take effect on next launch. 3. "Use a cloud computer" loses the waitlist dialog and renders as a disabled "Coming soon" badge, aligning with the web fork. Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): address review follow-ups (i18n, step-order, version, tests) - runtime-aside-panel: point "Learn more" to /docs/install-agent-runtime, branching by language so zh users land on /docs/zh/... - zh-Hans: unify Cloud "Coming soon" wording to "即将推出"; translate step_workspace.preview.more_meta ("and more" -> "等等") - onboarding-flow: derive forward navigation from ONBOARDING_STEP_ORDER via advanceFrom(curr) so inserting/reordering a step only requires editing the canonical array; runtime → agent/first_issue branch keeps its bespoke routing with a comment explaining why - onboarding handler: gate questionnaireAnswers.complete() on Version == 2 so a future schema bump can't be silently mis-counted against v2 funnel semantics - add unit tests for step-source / step-role / step-use-case (option click, Skip patch, Other free-text) and step-question shell (canContinue + pendingOther state machine) Co-authored-by: multica-agent <github@multica.ai> * fix(onboarding): rename useCaseFallback to fallbackFromUseCase ESLint's react-hooks/rules-of-hooks treats any function starting with "use" as a React hook. The helper is a pure switch — give it a name that doesn't trip the rule. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
93153d08b7 |
feat(my-issues): cover squad assignees via involves_user_id (MUL-2397) (#2829)
Re-introduces the `involves_user_id` filter on the issues list / open-list / count / grouped paths, but with the semantics nailed down for the second time around: tab 3 surfaces issues whose assignee is an *indirect* extension of the user (owned agent, or a squad they're a human member of / lead via owned agent / have an owned agent inside) — and explicitly NOT direct member assignment, which is tab 1's meaning. - server/pkg/db/queries/issue.sql: 4-branch filter on ListIssues / ListOpenIssues / CountIssues. Each subquery clamps workspace_id because issue.assignee_id is polymorphic with no FK. Leader resolution reads squad.leader_id directly, not the squad_member copy row (squad.go ignores errors when seeding that copy, so it can be missing). FindActiveDuplicateIssue switched from positional $2/$3/$4 to named sqlc.arg() — pure hygiene so the generated struct field names don't drift when new nargs are added. - server/internal/handler/issue.go: parse involves_user_id and plumb it into the three sqlc params; ListGroupedIssues (hand-written dynamic SQL) gets a mirrored 4-branch fragment, no shortcut. - packages/core: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter / api.listIssues / api.listGroupedIssues all carry the new param through. - packages/views/my-issues: tab 3 switches from client-side agent-fanout to involves_user_id=user.id. agentListOptions import and the myAgentIds memo go away. - server/internal/handler/issue_involves_test.go: 13 integration tests cover every branch (positive + cross-workspace negatives) plus the critical ExcludesDirectMemberAssignee negative on BOTH the sqlc and the grouped paths, locking tab 3 ∩ tab 1 = ∅. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5476e7678d |
Revert "feat(my-issues): cover squad assignees via involves_user_id (MUL-2364…" (#2828)
This reverts commit
|
||
|
|
3c510c31ed |
feat(my-issues): cover squad assignees via involves_user_id (MUL-2364) (#2801)
* feat(my-issues): cover squad assignees via involves_user_id (MUL-2364) The "My Agents" tab on /my-issues only resolved agents owned by the caller, so issues assigned to squads (member, leader, or agent-member of mine) never surfaced. This added a UNION-based involves_user_id filter that the backend expands to "me + agents I own + squads I relate to" in a single query. - SQL: ListIssues / ListOpenIssues / CountIssues accept narg involves_user_id and OR a workspace-scoped 3-branch UNION on the squad assignee subquery. Leader is sourced from canonical squad.leader_id (not the best-effort squad_member copy row whose AddSquadMember error is dropped in squad.go:177-188 and :259-263). - Handler: parses involves_user_id via parseUUIDOrBadRequest, plumbs into all three list params, and mirrors the same UNION fragment into the grouped dynamic SQL path. - Frontend: ListIssuesParams / ListGroupedIssuesParams / MyIssuesFilter gain involves_user_id; api client forwards it to the querystring. - My Issues page: "agents" scope now passes involves_user_id instead of fanning out owned-agent IDs client-side. Tab label widens to "我的智能体 / 小队" / "My Agents / Squads". - Tests: Go suite covers all three squad relations including the canonical-leader-without-squad_member-copy variant, cross-workspace isolation for agent / leader / squad_member branches, combination with creator_id, and the malformed-UUID 400 path. Client test pins the involves_user_id querystring wiring for both list endpoints. The FindActiveDuplicateIssue query gets explicit sqlc.arg() names so sqlc regeneration keeps the existing struct field names regardless of the local sqlc version (no behavior change). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(my-issues): tighten cross-workspace negatives for involves_user_id UNION Cross-workspace negative tests previously put both the foreign actor and the foreign issue in the foreign workspace, so the outer i.workspace_id = $1 already excluded the row before the UNION branches were exercised. Stripping a.workspace_id = $1 / s.workspace_id = $1 from any of the UNION subqueries would not have failed the tests. Rewrite the three existing negative cases to seed the issue in testWorkspaceID with a polymorphic assignee_id pointing at a foreign-workspace agent or squad (issue.assignee_id has no FK per migrations/001_init.up.sql:61). Now each UNION branch must enforce its own workspace scoping for the issue to stay out of the result. Also add ExcludesOtherWorkspaceSquadAgentMember: the squad_member.agent UNION branch had only positive coverage; this test pins that s.workspace_id = $1 and a.workspace_id = $1 must both hold there too. Verified by mutation: stripping the workspace clause from each branch makes the corresponding test fail. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6f5fbb7813 |
feat(comments): thread-aware list with composite cursor (MUL-2340) (#2787)
* feat(comments): thread-aware list with composite cursor (MUL-2340)
Adds three optional query params to GET /api/issues/{id}/comments and the
matching `multica issue comment list` flags:
- `thread=<comment-uuid>` resolves the anchor to the thread root via a
recursive CTE (defends against any future nested replies) and returns
root + all descendants chronologically. Anchor can be any comment in
the thread, root or reply.
- `recent=<N>` returns the newest N comments for the issue, ordered
chronologically in the response.
- `before=<RFC3339>` + `before-id=<uuid>` form a composite cursor for
stable pagination of `recent`. Both must be set together; a
timestamp-only cursor is rejected because ties on `created_at` would
let the existing `(created_at ASC, id ASC)` total order skip or
duplicate rows across pages.
Flag combination rules: `thread` is exclusive with `recent` and the
cursor; both may combine with `since`. Server and CLI enforce the same
matrix; the CLI fails fast locally so callers don't pay for a 400
round-trip.
Default behaviour (no params) is unchanged — full chronological dump
capped at commentHardCap — so the desktop UI and existing `--since`
polling are untouched. Agent prompt updates land in a follow-up PR so
the new CLI capabilities ship and bake first.
Co-authored-by: multica-agent <github@multica.ai>
* fix(comments): reject cursor without recent and align CLI/server on invalid --recent (MUL-2340)
Elon's PR #2787 second review flagged two gaps in the flag combination
matrix:
- server: GET /comments?before=...&before_id=... without `recent` was
silently dropped by fetchCommentsForList (RecentN=0 fell through to
the default / since path), so callers got the full timeline instead
of the documented "before X" semantics. Now returns 400.
- CLI: --recent 0 / --recent -3 were collapsed with "flag not passed"
by `recent > 0`, so an explicit invalid value silently fell back to
the default list. Switched to Flags().Changed("recent") so explicit
non-positive values fail loudly. Also enforces that --before /
--before-id only appear with explicit --recent (mirrors the new
server-side rule).
Tests:
- server flag matrix gains `before + before_id without recent → 400`.
- CLI gains TestRunIssueCommentListFlagGuards covering `--recent 0`,
`--recent -3`, cursor-without-recent, and the thread/recent
exclusivity path under the new Changed()-based check. The mock
server fatals if a request reaches /comments, proving the guards
fire before any HTTP round-trip.
Co-authored-by: multica-agent <github@multica.ai>
* feat(comments): make `recent` thread-grouped with a thread cursor (MUL-2340)
Bohan pushed back on the row-based `recent=N` shape: comments form a tree,
not a list, and the newest N rows can come from N unrelated threads, giving
the agent N disjoint conversational tails. Replace the row-based query with
a thread-grouped one before #2787 merges so we never ship the wrong shape:
- `recent=N` now returns the N most recently active threads (root + every
descendant per thread). A thread's recency is MAX(created_at) across its
whole subtree, so a stale-but-recently-replied thread outranks an old
quiet one — exactly the property row-recent loses.
- The cursor is now a *thread* cursor: `before` = a thread's
last_activity_at, `before_id` = its root comment id. The pair walks
threads strictly less recent than the page's oldest-active thread. The
cursor surfaces via `X-Multica-Next-Before` / `X-Multica-Next-Before-Id`
response headers (empty when there are no older threads); the CLI
forwards the same pair to stderr after listing.
- Row-based `recent` is gone — there is no internal caller and the prompt
update has not shipped yet, so there is no compat surface to preserve.
- Response body shape unchanged (flat JSON array, chronological). Default
and `--since` paths untouched. Desktop UI keeps working.
Tests:
- recent=1 returns the freshest-active thread fully; recent=2 returns both
with the older-active thread first (oldest-active → freshest tail).
- Stale-but-fresh: a thread whose root is older but has a fresh reply
outranks a thread whose root is newer but quiet.
- Cursor headers emitted only on full pages; empty on the final page.
- Pagination walks threads root2 → root1 → empty, no skips/duplicates.
- Tie-break: three threads sharing last_activity_at paginate one-at-a-time
using (last_activity_at, root_id) ordering — verifies the timestamp-only
cursor failure mode is fixed for the thread case too.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
eabfb8f3d1 |
fix(autopilots): reject unknown {{...}} tokens in issue title template (MUL-2370) (#2799)
* fix(autopilots): reject unknown {{...}} tokens in issue title template (MUL-2370)
`--issue-title-template` (and the matching `issue_title_template` API
field) silently kept any placeholder other than `{{date}}` as a literal
string in the rendered issue title — `{{.TriggeredAt}}`, `{{trigger_id}}`,
`${date}`, etc. would all slip through `strings.ReplaceAll` unchanged
because the renderer only knew one token. The flag name and help text
("Template for issue titles (create_issue mode)") and the docs phrasing
("the title supports interpolation like `{{date}}`") both implied a
richer placeholder set existed.
Tightens the contract on three fronts:
- Reject any `{{...}}` token other than `{{date}}` at create/update time
with `unknown template variable %q; supported: {{date}}` — turns the
silent-on-trigger surprise into an explicit 400 the moment the user
sets the template.
- Update CLI flag help on `autopilot create --issue-title-template` and
`autopilot update --issue-title-template` to spell out that only
`{{date}}` (UTC, YYYY-MM-DD) is interpolated.
- Update `apps/docs/content/docs/autopilots{,.zh}.mdx` to drop the
"like `{{date}}`" phrasing for the single supported placeholder.
Adds service-layer tests covering `interpolateTemplate` (substitution,
empty-template fallback, no-placeholder verbatim) and
`ValidateIssueTitleTemplate` (accepts empty / plain / `{{date}}` /
`{{ date }}`; rejects Go-template, Mustache-style, future placeholders
like `{{datetime}}`, and templates that mix one valid and one invalid
token).
Expanding the placeholder set (`{{datetime}}`, `{{trigger_id}}`,
`{{trigger_source}}`) is tracked as a separate enhancement — those
need run/trigger context plumbed into the renderer, which is out of
scope for this bug fix.
Closes #2732
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): render {{ date }} whitespace form too (MUL-2370)
Validator permitted {{ date }} but interpolateTemplate only matched the
exact string {{date}}, so a template that passed create/update could
still emit a literal {{ date }} at trigger time — re-introducing the
silent-literal behaviour the validator was meant to remove.
Route rendering through the same regex as validation so every accepted
form is also a substituted form. Cover {{ date }} substitution in
TestInterpolateTemplate.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
fe1ccb19c9 |
Revert "MUL-2324 conditionally inject non-core rule blocks (#2771)" (#2802)
This reverts commit
|
||
|
|
fab0671332 |
feat(skills): support multi-select bulk import in Copy from runtime (#2686)
- Multi-select UI for batch importing skills from a local runtime - Server batch-dispatches up to 10 import requests per heartbeat cycle - WS heartbeat now reads supports_batch_import from daemon payload instead of hardcoding true, so old daemons correctly fall back to one-at-a-time dispatch - Raised server pending timeout to 3min and client poll timeout to 4min to accommodate daemons that pop only one import per 15s heartbeat Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
46c1e2c889 |
feat(squads): show member working status on squad detail page (#2768)
* feat(squads): show member working status on squad detail page
Add a new GET /api/squads/{id}/members/status endpoint that returns each
member's derived working/idle/offline/unstable status, the issues each
agent is currently running, and the last observed activity timestamp.
The Squad detail page's Members tab consumes this snapshot to render a
status pill and an active-issue link next to each agent, with live
refresh wired through the existing task/agent/daemon WS events.
Human members are returned with status=null so the UI can keep them in
the same list without implying a presence signal. Archived agents stay
in the response and surface as offline rather than being filtered out.
Co-authored-by: multica-agent <github@multica.ai>
* fix(squads): address review feedback on member status endpoint
- i18n the "blocked" issue-status pill in squad members tab (was a
bare literal that failed `i18next/no-literal-string` lint).
- Treat any dispatched/running task as working, even when its
`agent_task_queue.issue_id` is NULL (chat / quick-create tasks).
The agent slot is occupied regardless of whether we can render an
issue link.
- Force `offline` for archived agents so they appear in the list
but never look like they're still on duty, matching the RFC
decision in MUL-2319.
- Include `workspaceKeys.squads` in the post-reconnect /
workspace-switch bulk invalidation so members-status recovers
after a disconnect during which task/runtime events were missed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
dfe2a57361 |
fix(autopilots): allow duplicate create_issue runs (#2789)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
6621231237 |
fix: improve search ranking and snippet support (MUL-2329)
Fixes MUL-2329 |
||
|
|
2323b72710 |
feat(autopilots): webhook delivery layer + idempotency/signature/replay (MUL-2334) [PR1] (#2774)
* feat(autopilots): webhook delivery layer + idempotency / signature / replay (MUL-2334)
Splits "inbound webhook receipt" from "autopilot run creation" so we can
record duplicate attempts, signature outcomes, and ignored/skipped
deliveries — and replay a delivery on demand. v1 ingress wrote straight
into autopilot_run.trigger_payload, which collapsed the two concerns and
left run_only autopilots vulnerable to provider retry storms.
Backend only (PR1). UI Deliveries tab follows in PR2.
Schema (migration 093):
- autopilot_trigger.provider: 'generic' | 'github' (default 'generic').
- autopilot_trigger.signing_secret: nullable plaintext (HMAC needs it
cleartext; mirrors how webhook_token is stored).
- webhook_delivery: one row per inbound POST. Carries raw_body,
selected_headers, dedupe_key/source, signature_status,
autopilot_run_id, replayed_from_delivery_id, response_status / body.
- Partial unique index on (trigger_id, dedupe_key) excludes NULL and
'rejected' rows, so a wrong-secret 401 does NOT permanently block a
future retry with the same X-GitHub-Delivery once the operator fixes
the secret.
Ingress flow (autopilot_webhook.go), persist-first + sync dispatch:
1. IP rate limit -> 2. token lookup -> 3. token rate limit ->
4. read raw body -> 5. autopilot/workspace cross-check ->
6. normalize JSON (400 without persistence on parse failure) ->
7. compute dedupe key + signature status ->
8. INSERT delivery (status=queued). On (trigger_id, dedupe_key)
unique-violation: bump attempt_count on existing row and return
the original delivery_id + autopilot_run_id with 200 ->
9. invalid/missing signature: UPDATE -> rejected, return 401 with
delivery_id (no dispatch, not replayable) ->
10. trigger disabled / autopilot paused/archived: UPDATE -> ignored,
return 200 ->
11. DispatchAutopilot synchronously, UPDATE -> dispatched/skipped/failed
with autopilot_run_id and the response body we returned ->
12. TouchAutopilotTriggerFiredAt and return 200.
No new long-running worker. A stale 'queued' row only happens if the
process dies between INSERT and UPDATE; that's a follow-up sweeper, not
this PR.
Authenticated API:
- GET /api/autopilots/{id}/deliveries (slim list)
- GET /api/autopilots/{id}/deliveries/{deliveryId} (with raw_body)
- POST /api/autopilots/{id}/deliveries/{deliveryId}/replay -> creates
a new delivery row (replayed_from_delivery_id set), dispatches a
new run, never collapses onto the original via dedupe.
- PUT /api/autopilots/{id}/triggers/{triggerId}/signing-secret
Write-only; trigger response surfaces has_signing_secret +
signing_secret_hint (last 4 chars), never the secret itself.
Signature verification reuses the GitHub-compatible
X-Hub-Signature-256: sha256=<hex(hmac(body, secret))> scheme; the
HMAC helper is constant-time. Invalid/missing signatures still count
against per-IP and per-token rate limits.
autopilot_run.trigger_payload is intentionally preserved — delivery
records the HTTP receipt; run records the normalized envelope handed
to the agent. They are two different views.
Tests (Postgres-backed):
- delivery persistence on accept
- dedupe via Idempotency-Key and X-GitHub-Delivery; run_only retry
storm pin (3 retries -> 1 run)
- invalid signature: 401 + rejected row + no run linkage
- missing signature when secret configured: 401 + 'missing' state
- valid signature dispatches
- signing secret never echoed in trigger responses; hint shows last 4
- min-length and clear-by-empty for signing secret PUT
- replay creates a NEW delivery + new run; rejected deliveries cannot
be replayed
- list omits raw_body; detail includes it; cross-autopilot ID returns
404 (workspace isolation defense in depth)
- provider validation: unknown -> 400, github -> 201 round-trips
- bad-signature stream still counts against per-token rate limit
Co-authored-by: multica-agent <github@multica.ai>
* fix(autopilots): address PR review on webhook delivery layer (MUL-2334)
- Exclude `failed` from the (trigger_id, dedupe_key) partial unique index
alongside `rejected`, so a transient ingress failure does not strand the
provider's stable X-GitHub-Delivery / Idempotency-Key retry. Update the
dedupe lookup to prefer non-terminal rows under the same predicate.
- Tighten delivery status enum: drop `skipped` from the CHECK constraint
and from the handler. A run that was admission-skipped (e.g. runtime
offline) is now recorded as delivery=`dispatched` linked to the
skipped run, with the response payload carrying status=`skipped`.
Source of truth for skipped-ness is autopilot_run.status, not the
delivery row — keeps the Deliveries UI enum unambiguous.
- On dispatch error, link the (possibly non-nil) autopilot_run returned
by DispatchAutopilot to the failed delivery so Deliveries UI can
navigate to the run row for debugging.
- Slim list projection: ListWebhookDeliveriesByAutopilot no longer pulls
raw_body / selected_headers / response_body — a 100-row page × 256 KiB
would otherwise round-trip ~25 MiB from Postgres per Deliveries reload.
Detail endpoint continues to return the full row.
- Fix backend CI: TestGetDelivery_ReturnsFullPayload now decodes the
response and asserts on the parsed raw_body instead of substring-
matching against an escaped JSON string; raise the test-suite default
webhook rate limits in TestMain so the shared 192.0.2.1 IP bucket
doesn't fill across the suite and leak 429s into unrelated tests.
- Add regression coverage for the dedupe-after-failure path.
cd server && go test ./... is green locally.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
15152c6ccd |
feat(auth): cache workspace membership for daemon heartbeat path (MUL-2247) (#2638)
* feat(auth): cache workspace membership for daemon heartbeat path Cache workspace membership existence (not role) in Redis to eliminate a DB round-trip on every PAT-authenticated daemon heartbeat. Follows the existing PATCache nil-safe pattern. Key design decisions per reviewer feedback: - Cache existence only (sentinel "1"), not role string. Authorization decisions that depend on role always hit the DB directly. This eliminates the cache-aside race where a stale elevated role could persist after a downgrade. - Proactive invalidation on UpdateMember, DeleteMember, LeaveWorkspace, and DeleteWorkspace (iterates members before cascade delete). - 5 min TTL. Combined with PATCache (10 min), worst-case revocation delay is max(10m, 5m) = 10 min — consistent with original PATCache design decision. Limitations: - Non-members still hit DB on every request (negative caching not implemented — the scenario is rare for daemon endpoints which require valid workspace-scoped tokens). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * test(auth): drive membership cache invalidation through real handlers - TestRequireDaemonWorkspaceAccess_CacheHit now uses a ghost user with no member row, so the only path to a granted access is the cache short-circuit. Without priming the cache the access check must fail; with priming it must succeed. A future change that bypasses the cache would fail the second assertion. - Replaces the cache-only InvalidatedOnMemberRemoval test (which only re-exercised the auth-package primitive) with four handler-driven tests that exercise DeleteMember, UpdateMember, LeaveWorkspace and DeleteWorkspace via their real HTTP handlers. Each test prepares a real member, primes the cache, calls the handler, and asserts the cache entry is gone — so a refactor that drops one of the Invalidate(...) calls in workspace.go will fail CI. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Jiang Bohan <bhjiang@outlook.com> |
||
|
|
e8fb0efe3d |
MUL-2324 conditionally inject non-core rule blocks (#2771)
* feat(runtime): conditionally inject non-core rule blocks Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): tighten mention rule triggers Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9418d2a2c1 |
feat(autopilots): webhook triggers (server + CLI + UI + docs) MUL-2049 (#2348)
* feat(server): add webhook trigger DB migration + sqlc queries
Lays the foundation for webhook autopilot triggers:
- partial unique index on autopilot_trigger.webhook_token (kind=webhook only)
so the public ingress route can resolve a trigger in O(1)
- GetWebhookTriggerByToken / TouchAutopilotTriggerFiredAt /
RotateAutopilotTriggerWebhookToken / SetAutopilotTriggerWebhookToken
queries, regenerated with sqlc
* feat(server): webhook token generator + payload normalizer
Two pure helpers for the webhook autopilot work:
- generateWebhookToken: 32 random bytes -> base64-url, "awt_" prefix.
256 bits of entropy keeps brute-force off the table; the prefix makes
leaked tokens recognisable in logs.
- normalizeWebhookPayload: turns arbitrary JSON into the WebhookEnvelope
shape (event/eventPayload/request) used by trigger_payload. Header- and
body-based event inference covers GitHub, GitLab, X-Event-Type, and
caller-provided envelopes; scalar/empty/invalid bodies are rejected so
the handler can answer 400.
* feat(server): generate webhook tokens and expose rotate endpoint
- New handler.Config.PublicURL fed by MULTICA_PUBLIC_URL env so
/api/autopilots/.../triggers responses can include an absolute
webhook_url alongside the always-present webhook_path.
- CreateAutopilotTrigger now mints a webhook_token via crypto/rand
for kind=webhook and ignores cron/timezone for non-schedule kinds.
api triggers stay accepted-but-inert per PLAN.md.
- New POST /api/autopilots/{id}/triggers/{triggerId}/rotate-webhook-token
protected by the existing workspace auth group; old tokens stop
working immediately because the unique-index lookup keys on the
current row value.
* feat(server): public webhook ingress route + per-token rate limiter
- New POST /api/webhooks/autopilots/{token} route, mounted outside the
authenticated group: the path token is the credential. Workspace
context is derived from the joined autopilot row, never headers.
- Body capped at 256 KiB via http.MaxBytesReader; oversized payloads
return 413 mid-read instead of being fully buffered.
- Disabled triggers / paused / archived autopilots return
200 {"status":"ignored"} so providers stop retrying.
- Skipped-runtime dispatches surface 200 {"status":"skipped"} with the
reason from the autopilot service's pre-flight admission check.
- WebhookRateLimiter interface with sliding-window in-memory + Redis
Lua-script implementations. Default 60 req/min per token. Test
coverage on the in-memory path; Redis variant fails open on cache
errors so a Redis hiccup never blocks ingress.
- Integration tests exercise token generation, dispatch, payload
envelope persistence, GitHub-header inference, paused/disabled
short-circuits, oversized rejection, and rotate-then-old-token-404.
* feat(server): include webhook payload in create_issue description
When an autopilot run is triggered by a webhook and execution_mode is
create_issue, the agent only sees the issue body — never the run's
trigger_payload. Append a 'Webhook event:' line and a fenced JSON block
with the normalized eventPayload so the agent has the inbound context
inline. Schedule / manual runs are unchanged.
Tests cover:
- schedule path keeps existing italic note, no webhook block
- webhook path emits event line + payload block, italic before block
- non-envelope JSON falls back to raw body (defensive)
- non-webhook source with payload still gets no webhook block
* feat(core): types, API client and mutations for webhook triggers
- AutopilotRunStatus gains 'skipped' so the run-list UI handles the
admission-skipped state explicitly instead of falling through to a
generic case (the backend already emits it via MUL-1899).
- AutopilotTrigger picks up optional webhook_path / webhook_url. Both
are optional so older self-hosted servers that pre-date this change
still parse cleanly.
- buildAutopilotWebhookUrl helper composes a usable absolute URL with
the priority webhook_url > apiBaseUrl + path > origin + path > path.
Tested with seven cases covering each branch.
- ApiClient.rotateAutopilotTriggerWebhookToken posts to
/api/autopilots/{id}/triggers/{triggerId}/rotate-webhook-token; the
HTTP-contract test pins URL + method.
- useRotateAutopilotTriggerWebhookToken mutation invalidates
autopilotKeys.detail on settle, mirroring the existing trigger-mutation
pattern.
* feat(views): webhook trigger UI in Add Trigger dialog and trigger row
Add Trigger dialog gains a Schedule/Webhook segmented toggle:
- Schedule reuses TriggerConfigSection unchanged.
- Webhook hides the cron config and shows a help line; the trigger is
created with kind=webhook and the URL is generated server-side.
- Toast text differentiates schedule vs webhook on success.
TriggerRow grows a webhook branch:
- Webhook icon, kind translated via trigger_kind.
- URL shown in a truncating monospace pill, with copy + rotate
buttons. Copy uses navigator.clipboard with toast feedback; rotate
uses an AlertDialog confirm because the old URL stops working
immediately.
- api triggers render a Deprecated badge and skip URL/copy/rotate
affordances.
RunRow gains a 'skipped' RUN_VISUAL entry (muted dash) so admission-
skipped runs don't fall through to a generic case. Source label uses the
new run_source i18n key instead of capitalize.
Locales: en + zh-Hans gain run_status.skipped, run_source.*,
trigger_kind.*, trigger_row.{copy_url,rotate_url,*_confirm_*,toast_*},
add_trigger_dialog.{type_*,webhook_help,toast_added_{schedule,webhook}}.
* feat(cli): support webhook trigger creation and URL rotation
- multica autopilot trigger-add now takes --kind schedule|webhook
(default schedule for backward compatibility). For webhook it skips
--cron / --timezone validation and prints the resulting webhook URL,
preferring the server-provided webhook_url and falling back to
client.BaseURL + webhook_path.
- New multica autopilot trigger-rotate-url <autopilot-id> <trigger-id>
command for rotating the bearer URL of a webhook trigger.
* docs(autopilots): add webhook trigger guide (en + zh)
Replaces the 'Webhook and API triggers are not available yet' section
with end-to-end webhook documentation: how the URL is generated, what
payload shapes are accepted, the inferred-event rules, the bearer-secret
warning + rotate flow, status-code semantics for accepted/skipped/
ignored/4xx/5xx outcomes, and the MULTICA_PUBLIC_URL self-host
configuration.
Run history list now mentions skipped status. The 'unavailable
features' section narrows to api-kind triggers, HMAC signing, IP
allowlists, and provider presets.
* feat(views): add Schedule/Webhook toggle to the create autopilot dialog
Closes the gap where a brand-new autopilot could only be created with a
schedule trigger. The right-column config now has a Trigger section
with a segmented Schedule/Webhook control:
- Schedule keeps the existing cron/timezone UI.
- Webhook hides the cron UI and shows a help line; on submit, a
kind=webhook trigger is created right after the autopilot.
In edit mode the toggle is intentionally hidden (PLAN.md treats trigger-
type changes as delete-old + create-new, not in-place updates), but the
panel still picks the right kind based on props.triggers[0].kind so a
webhook autopilot doesn't render an irrelevant cron form.
Locales: section_trigger_kind, trigger_kind_{schedule,webhook},
section_webhook, webhook_help_{create,edit} added in en + zh-Hans.
* feat(views): show webhook URL inline after creating a webhook autopilot
After a successful create with kind=webhook, the dialog stays open and
swaps to a confirmation panel showing the freshly minted URL with a
copy button + 'Treat this URL like a password' warning + Done button.
Avoids the friction of "create the autopilot, then go find it in the
list, click in, scroll to triggers, copy URL."
Locales: dialog.webhook_created_{title,description,warning,done} added
in en + zh-Hans.
Schedule create flow is unchanged (toast + close). The success panel is
gated on the trigger returned from the create mutation, so a partial
failure (autopilot created, trigger creation errored) still falls
through to the toast_create_partial path.
* feat(views): show webhook payload in run detail dialog
The agent transcript dialog now accepts an optional headerSlot that
sits above the event list. The autopilot RunRow drops a
WebhookPayloadPreview into that slot when the run came from a webhook
and trigger_payload is non-empty.
The preview is collapsed by default (the transcript itself is the main
event), shows the inferred event name + receivedAt in the header, and
reveals the eventPayload as pretty-printed JSON with a copy button on
expand. Falls back gracefully if the row's trigger_payload doesn't
match the WebhookEnvelope shape — the whole value is shown instead so
nothing is hidden.
Closes the "agent didn't echo the payload, now I can't see what
triggered the run" gap. PLAN.md tracked this as
"Payload preview in run history" under follow-ups.
Locales: webhook_payload.{label, unknown_event, payload, content_type,
copy, copied, copied_short, copy_failed} added in en + zh-Hans.
* chore(server): wire MULTICA_PUBLIC_URL through self-host compose
Two small follow-ups split out of the webhook trigger PR:
- docker-compose.selfhost.yml passes MULTICA_PUBLIC_URL into the
backend container so a self-hosted deployment behind a real domain
gets absolute webhook URLs in the trigger response. Documented in
.env.example with the rationale for not deriving the public host
from request headers.
- Drop a duplicated 'invalid json:' prefix in the webhook ingress
400 error path. normalizeWebhookPayload already prefixes its
errors, so the handler doesn't need to re-prefix.
* fix(migrations): renumber webhook trigger migration 081 → 089 to avoid collision
The branch's 081_autopilot_webhook_triggers.{up,down}.sql collided
numerically with 081_runtime_timezone.{up,down}.sql that landed on
main, making migration apply order undefined. Renumber to 089 so the
file slots after the latest main migration (088_squad_instructions).
The SQL itself doesn't conflict — it only creates a partial unique
index on autopilot_trigger.webhook_token — but the duplicate prefix
is what the migration runner sees, so the filename must move.
* fix(autopilot-webhook): address PR review blocking issues
- Redact bearer tokens from request logs: paths matching
/api/webhooks/autopilots/<token> now log "[redacted]" instead of the
token. The resolved trigger ID is plumbed via context so audit lines
stay useful for debugging. (Review item Blocking #1.)
- Distinguish pgx.ErrNoRows from transient DB errors in token lookup:
no-row stays 404 (so providers don't retry on a deleted webhook),
other errors return 500 (which providers DO retry, avoiding silent
drops on DB blips). (Review item Blocking #2.)
- Add per-IP sliding-window rate limiter that runs BEFORE the token
lookup, so spraying random tokens can no longer probe the
autopilot_trigger index unboundedly. Reuses the existing Lua script
with a separate Redis key namespace; falls open on Redis errors.
Default budget 30 req/min/IP. (Review item Blocking #3.)
The webhook handler now applies the gates in the order: per-IP rate
limit → token lookup → per-token rate limit → handler logic.
* fix(autopilot): atomic webhook trigger creation + strict kind/timezone validation
- Mint the webhook bearer token BEFORE the INSERT and pass it via
CreateAutopilotTriggerParams so the row never exists in a half-written
kind=webhook + webhook_token=NULL state. On the (vanishingly rare)
unique-index collision the whole INSERT is retried with a fresh token
— no UPDATE second step. Removes the now-dead attachFreshWebhookToken
helper. (Review item Recommended #4.)
- Add new GET /api/autopilots/{id}/runs/{runId} endpoint that returns a
single run including the full trigger_payload. The list response is
now slim (omits trigger_payload) so worst-case payload size drops
from ~5 MB to ~5 KB. (Review item Recommended #5, server side.)
- Reject kind=api with 400 ("kind=api is deprecated; use schedule or
webhook") and reject kind=webhook with --timezone with 400 — both
surfaces stragglers loudly instead of silently dropping fields.
CLI mirrors the check so --timezone with --kind webhook errors
client-side. (Review nits.)
- Add --yes (-y) flag and an interactive y/N confirmation prompt to
`multica autopilot trigger-rotate-url` so the destructive rotate
matches the UI's AlertDialog safety. (Review item Recommended #6.)
* fix(views): fetch webhook payload on-demand and truncate at 4 KiB
- Add useAutopilotRun query hook + getAutopilotRun API client method
paired with the new server endpoint. The run-detail dialog now mounts
a WebhookPayloadSlot that fetches the full run (incl. trigger_payload)
lazily — list responses no longer carry up to 256 KiB × N runs of
envelope data.
- WebhookPayloadPreview truncates its in-DOM <pre> at 4 KiB with a
localized marker so jank-y machines aren't asked to render a 256 KiB
JSON blob. The Copy button still yields the full string.
- Adds the truncated_marker i18n string to en + zh-Hans.
Review items Recommended #5 (frontend) and a nit on the preview's
unbounded <pre>.
* test(autopilot-webhook): close coverage gaps flagged in PR review
- request_logger: redactWebhookPath unit tests + integration test
proving the bearer token never lands in slog output, plus the
webhook_trigger_id context plumbing.
- autopilot_webhook_handler: empty body → 400, archived autopilot →
200 ignored, per-IP rate limiter trips before DB lookup, kind=api
and webhook+timezone are rejected at 400, slim list + full detail
endpoint round-trip.
- webhook_rate_limiter: Lua script structure guard (catches reordering
even without a live Redis), plus live-Redis tests for both per-token
and per-IP limiters (REDIS_TEST_URL gated, matching the existing
Redis test pattern in the package).
- WebhookPayloadPreview: envelope rendering, fallback shape, and the
>4 KiB truncation path with full-payload-on-Copy guarantee.
Two branches are documented as code-review-protected rather than
covered by tests: the 500-on-DB-error path requires injecting a stub
Queries (no interface here), and the cross-workspace defense-in-depth
check is unreachable from valid SQL state.
* fix(middleware): SetWebhookTriggerID must mutate request in place
The round-1 helper returned a fresh *http.Request from WithContext, and
the webhook handler did `r = SetWebhookTriggerID(r, ...)`. That swaps
the handler's local pointer but doesn't propagate the new context back
to RequestLogger, which is still holding the original *http.Request —
so the audit line never actually included webhook_trigger_id in
production. The round-1 test happened to pass because it pre-stashed
the value on the request before calling ServeHTTP, bypassing the bug
it was meant to verify.
Switch to in-place mutation via `*r = *r.WithContext(...)` so the
wrapping middleware sees the new context after next.ServeHTTP returns,
and update the test to exercise the real call pattern (set the context
from inside the handler, assert the surrounding logger reads it).
Verified live: an accepted webhook now logs
path=/api/webhooks/autopilots/[redacted] webhook_trigger_id=<uuid>
* fix(autopilot-webhook): symmetric ErrNoRows split + trusted-proxy gate
Round-2 review (Bohan-J, PR #2348 follow-up):
- Must-fix #1: the second lookup at autopilot_webhook.go:258
(GetAutopilot after the token resolves) was folding every error into
404. A transient DB blip would tell a webhook sender "not found" and
it would never retry. Apply the same errors.Is(err, pgx.ErrNoRows)
→ 404 / else → 500 split as the first lookup got in round 1.
- Must-fix #2: clientIPForRateLimit was honoring X-Forwarded-For /
X-Real-IP from any caller. An attacker spraying random tokens could
just rotate the XFF header and the per-IP bucket became per-request,
so the limiter that's specifically supposed to gate spraying before
it hits the DB unique index was bypassed.
New shape — matches Bohan's suggestion exactly:
* Default: r.RemoteAddr only, headers ignored.
* Operator opt-in via MULTICA_TRUSTED_PROXIES (comma-separated
CIDRs). XFF/X-Real-IP are honored only when r.RemoteAddr is
inside one of the listed prefixes; otherwise they're dropped.
Wired through .env.example and docker-compose.selfhost.yml so
self-host operators can configure their reverse-proxy's CIDR.
Invalid CIDRs in the env var are dropped with a single slog.Warn at
startup rather than crashing the server. Uses net/netip (stdlib,
value-typed) for parsing and containment checks.
Verified live on the rebuilt self-host backend: a 35-request spray
from one source with rotating XFF gets the expected 30× 404 + 5× 429,
proving the per-IP bucket is keyed on the real connection IP.
* fix(autopilot): reject cron/timezone PATCH on non-schedule triggers
Round-2 review should-fix. CreateAutopilotTrigger already 400s on
kind=webhook + timezone/cron_expression, but UpdateAutopilotTrigger
silently wrote those fields regardless of prev.Kind. The values then
sat in the DB visible to nobody and read by nothing — a back door that
left the API contract fuzzy across create vs update.
Mirror the create-path discipline: after loading prev, if prev.Kind
!= "schedule" and the PATCH body sets cron_expression or timezone,
return 400 with a clear message. enabled and label remain accepted on
every kind.
The existing prev.Kind == "schedule" guard on next_run_at recompute
stays as belt-and-braces, but with this gate in place the recompute
branch is now reachable only for the kind it was meant for.
* test(autopilot-webhook): close round-2 coverage gaps
- IPRateLimitNotBypassedByXFFSpoof: drives the must-fix #2 invariant
by rotating XFF across three calls from the same RemoteAddr and
asserting the third gets 429. Pre-round-2 this test would have
passed for the wrong reason (limiter trusted XFF, so per-bucket
collision was incidental); now it pins the bypass-closed property.
- IPRateLimitReturns429BeforeDBLookup: updated to set RemoteAddr
explicitly and drop the XFF header it was leaning on. With
TrustedProxies empty (test default) the limiter keys on the real
connection IP, which is what the test wants to assert anyway.
- UpdateAutopilotTrigger_RejectsCronExpressionOnWebhookKind +
UpdateAutopilotTrigger_RejectsTimezoneOnWebhookKind: drive the
round-2 should-fix from the handler boundary.
- UpdateAutopilotTrigger_AcceptsEnabledAndLabelOnWebhookKind: counter
test so a regression to a blanket reject is caught.
* fix(migrations): bump webhook trigger migration 089 → 091
origin/main added 089_squad_no_action_activity_index (and 090_task_is_leader)
since our last rebase, re-colliding with our 089_autopilot_webhook_triggers.
Bump to 091 so the filename ordering is unambiguous again. The SQL is
unchanged — same partial unique index on autopilot_trigger.webhook_token —
only the filename moves.
* fix(views): dedupe skipped icon in autopilot RUN_VISUAL after rebase
The rebase against origin/main merged main's add of `Ban` for the
skipped status next to our round-1 `MinusCircle` entry, leaving the
RUN_VISUAL map with two `skipped` keys (only the last would have been
read at runtime, and MinusCircle had been dropped from the imports
during conflict resolution — so the file would not compile).
Keep main's `Ban` icon (latest design) and a single `skipped` entry.
Carry over the round-1 comment about why the muted styling matters
for failure-ratio readability.
---------
Co-authored-by: Kerim Incedayi <kerim.incedayi@digitalchargingsolutions.com>
|
||
|
|
3645bdb5b6 |
feat(issues): add start_date field with progressive disclosure (MUL-2274) (#2696)
* feat(issues): add start_date field with progressive disclosure (MUL-2274) Mirrors the existing due_date implementation end-to-end so an issue can express a planned start in addition to a deadline. Surfaces start_date as an optional sidebar property alongside priority / due_date / labels (added in MUL-2275), with consistent picker, board/list/sort, activity, and inbox plumbing. Backs the Project Gantt work (parent MUL-1881) and keeps the progressive-disclosure attribute experience consistent. - DB: migration 091 adds issue.start_date TIMESTAMPTZ. - sqlc: ListIssues / CreateIssue / UpdateIssue / CreateIssueWithOrigin / ListOpenIssues read & write start_date. - Backend: IssueResponse + create/update/batch-update handlers parse and emit start_date with RFC3339 validation; new start_date_changed activity event + subscriber notification (with prev_start_date in event payload). - CLI: --start-date flag on `multica issue create` / `issue update`. - Frontend: StartDatePicker component, start_date wired into Issue type, Zod schema, draft / view stores, sort util, header sort + card-property options, list-row / board-card display, create-issue modal, and the issue-detail progressive-disclosure "+ Add property" surface (visibility rule, picker row, add-property menu icon + label). - i18n: en + zh-Hans for sort_start_date / card_start_date / prop_start_date / activity start_date_set / start_date_removed / picker start_date.trigger_label / clear_action / inbox labels. - Tests: new TestNotification_StartDateChanged; existing Issue / draft / modal fixtures extended with start_date. Co-authored-by: multica-agent <github@multica.ai> * feat(issues): align start_date with due_date in actions menu and CLI table - Add Start Date submenu (today / tomorrow / next week / clear) in actions menu, mirroring Due Date — parity with the Due Date quick setters in list/board context and 3-dot menus. - Add corresponding en / zh-Hans i18n keys (actions.start_date / start_today / start_tomorrow / start_next_week / start_clear). - CLI human table for `multica issue list` and `multica issue get` now shows a START DATE column next to DUE DATE; --full-id variant too. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
668cab6022 |
feat(github): mirror PR CI checks and merge conflict status (MUL-2228) (#2632)
* feat(github): mirror PR CI checks and merge conflict status (MUL-2228)
Surface "checks passed/failed" and "conflicts/no conflicts" badges under
each linked PR on the issue page so users can judge readiness without
flipping over to GitHub. CI state is fed by check_suite webhooks
(GitHub Actions + apps using the Checks API; legacy status events are
out of scope for MVP); conflicts are read from pull_request.mergeable_state.
Data model:
* github_pull_request: add head_sha + mergeable_state
* github_pull_request_check_suite: per-suite rows keyed by (pr_id, suite_id)
* Aggregation done at query time, filtering by current head_sha so
late-arriving suites for a stale head can't contaminate the new head's
pending view; per-app latest suite chosen first so a single app firing
multiple suites isn't counted N times.
Webhook hardening:
* synchronize/opened/reopened/edited(base) explicitly clear mergeable_state
* single-row ordering protection on the check_suite upsert prevents a
late-delivered older event from overwriting a newer one
* check_suite.pull_requests is iterated; unknown PRs are logged and dropped
UI:
* PR row shows Checks + Conflicts badges; opaque mergeable values
(blocked/behind/unstable/...) render as no badge, not as conflicts.
* Terminal PR states (merged/closed) suppress the status row entirely.
Tests: * Pure unit coverage for derivePRMergeableState + aggregateChecksConclusion
* Webhook integration tests: multi-app aggregation, old-head ignore,
late-older-event ignore, synchronize clears mergeable_state
* Vitest coverage for pull-request-list badge rendering across CI/conflict
combinations and the legacy (null) fallback.
Co-authored-by: multica-agent <github@multica.ai>
* fix(github): scope check_suite PR lookup; preserve mergeable on metadata
Addresses code review on PR #2632.
1. check_suite handler now resolves the PR through the workspace-scoped
GetGitHubPullRequest query instead of GetGitHubPullRequestByRepoNumber.
The (workspace_id, repo_owner, repo_name, pr_number) tuple is the real
uniqueness key, so a bare (owner, repo, number) lookup could return a
stale row from another workspace and either land the suite on the wrong
PR or skip the right one when the installation ids drifted. The old
unscoped query is removed.
2. derivePRMergeableState now returns (value, clear) and the upsert SQL
distinguishes three cases: state-changing actions clear the column to
NULL, non-empty payloads write the value, and metadata events with an
empty payload preserve the existing column. Previously every empty
payload became NULL, so a labeled/assigned event silently wiped a
known clean/dirty verdict in violation of the RFC's "metadata empty
payload preserves" rule.
3. ListPullRequestsByIssue narrows to the issue's PR ids before running
the per-app check_suite aggregation, avoiding a full-table scan over
github_pull_request_check_suite when only a handful of rows belong to
the requested issue.
New helper test covers labeled+empty preserves; new integration test
verifies a metadata event after a known mergeable_state keeps the value.
Co-authored-by: multica-agent <github@multica.ai>
* feat(github): PR card layout v3 increment — stats + segmented progress bar
Replaces the row + badge layout under "Pull requests" on the issue
detail sidebar with a card that mirrors the GitHub PR summary look:
title, author/avatar, +N −M · K files diff stats, segmented progress
bar (failed → pending → passed, failure leftmost), and a one-line
status caption following an explicit priority pass-through.
Backend
- Migration 092: github_pull_request adds additions / deletions /
changed_files (INT NOT NULL DEFAULT 0). Zero defaults are what the
new frontend treats as "legacy backend — hide the stats row" so old
PR rows that pre-date this migration don't render "+0 −0 · 0 files".
- pull_request webhook handler reads stats off the top-level payload.
- ListPullRequestsByIssue now surfaces per-suite counts
(checks_passed / failed / pending) alongside the existing aggregate
conclusion, so the segmented bar reuses the already-computed counts
with no new aggregation.
Frontend (packages)
- core/github/pull-request-status.{ts,test.ts}: pure-function module
for the status-kind priority table and the segment derivation; 15
cases covered, includes the "all-zero → hide stats" guard.
- views/issues/components/pull-request-list.tsx: PullRequestCard plus
a compact-row fallback used when count > 4 (first 3 as cards, the
remainder collapsed behind a Show more toggle).
- i18n: new `pull_request_card_*` keys in en + zh-Hans.
Tests
- 12 component tests covering each rule of the priority table, the
legacy-zero stats fallback, and the collapse threshold.
- Reuse of the v3 webhook handler tests confirmed.
Verification
- pnpm typecheck + pnpm test green (60 test files, 536 tests).
- go build ./... + go vet ./... clean.
- 6 demo issues (DEV-2..DEV-7) screenshotted via Playwright; see the
PR comments for the visual check matrix.
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): collapse PR cards at N>=4, not N>4
The card-vs-collapse threshold used `>` so 4 PRs slipped past it and
all rendered as full cards, contrary to RFC v3 (N >= 4 collapses to
3 cards + compact tail). Switch to `>=` and update the threshold-
boundary test to expect "Show 1 more".
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): align PR sidebar rows with existing list style
Co-authored-by: multica-agent <github@multica.ai>
* fix(views): hide terminal PR status badges
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
4c7a990a25 |
fix(autopilot): attribute autopilot-created issue to assignee agent (MUL-2293) (#2719)
Before: dispatchCreateIssue copied autopilot.created_by_type/id onto the new issue's creator_type/creator_id, and the same fields were used as the ActorType/ActorID of the issue:created event. Result: any issue spawned by an autopilot was reported as created by the human who first configured the autopilot, not by the agent that actually owns the work. Downstream subscriber/activity/notification listeners inherited the same wrong actor. After: creator and actor are both the autopilot's assignee agent (creator_type=agent, creator_id=ap.assignee_id). The human owner is still recoverable via origin_type=autopilot + origin_id. Audited the other ap.created_by_* usages: analytics attribution (autopilotActorID, task.go user-id), and the private-agent visibility gate in shouldSkipDispatch — all correctly read the autopilot's owner, not the executor, so they stay as-is. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
380c6b5122 |
feat(usage): add Time and Tasks to daily-trend toggle (MUL-2283) (#2709)
Extends the workspace /usage page Daily tokens chart toggle from Tokens | Cost to Tokens | Cost | Time | Tasks, so users see daily run-time and task-count trends alongside spend without leaving the page. - New SQL `ListDashboardRunTimeDaily`: per-date totals from agent_task_queue (terminal tasks only), scoped to workspace and optionally project. Same time anchor as ListDashboardAgentRunTime so day boundaries line up. - New handler GET /api/dashboard/runtime/daily + TanStack Query option. - New DailyTimeChart (single-series, smart h/m/s unit) and DailyTasksChart (completed + failed stacked). - Empty-state is per-metric so a workspace with tokens but no terminal runs (or vice-versa) doesn't get a false "no data". - i18n: en + zh-Hans daily.metric_time / metric_tasks + titles. Co-authored-by: multica-agent <github@multica.ai> |